Skip to content

Commit cc5b419

Browse files
committed
feat: ✨ Allow for functools.partial and functions returning an awaitable as autocomplete (Pycord-Development#2914)
* ✨ Allow for `functools.partials` and such as autocomplete * 📝 CHANGELOG.md * 🏷️ Better typing * 🚚 Add partial autocomplete example * 🩹 Make CI pass * 📝 Move docstring to getter * 🏷️ Boring typing stuff * ✏️ Fix writing * chore: 👽 Update base max filesize to `10` Mb (Pycord-Development#2671) * 👽 Update base max filesize to `10` Mb * 📝 CHANGELOG.md * 📝 Requested changes * 📝 Grammar * ♻️ Merge the 2 examples * ⚰️ remove conflicting autocomplete attribute from `Option` * 🐛 Fix missing setting autocomplete * Copilot Signed-off-by: Paillat <jeremiecotti@ik.me> * 📝 CHANGELOG.md Signed-off-by: Paillat <paillat@pycord.dev> * Update CHANGELOG.md Signed-off-by: Paillat <jeremiecotti@ik.me> * 📝 Update CHANGELOG.md * 🐛 Missing description kwarg in autocomplete example --------- Signed-off-by: Paillat <me@paillat.dev> Signed-off-by: Paillat <paillat@pycord.dev> Signed-off-by: Paillat <jeremiecotti@ik.me> Signed-off-by: Lala Sabathil <lala@pycord.dev> Co-authored-by: Lala Sabathil <lala@pycord.dev> (cherry picked from commit b190b7f)
1 parent 3b45179 commit cc5b419

4 files changed

Lines changed: 103 additions & 11 deletions

File tree

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ These changes are available on the `master` branch, but have not yet been releas
2828
- Adds pre-typed and pre-constructed with select_type `ui.Select` aliases for the
2929
different select types: `ui.StringSelect`, `ui.UserSelect`, `ui.RoleSelect`,
3030
`ui.MentionableSelect`, and `ui.ChannelSelect`.
31+
- Added the ability to use functions with any number of optional arguments and functions
32+
returning an awaitable as `Option.autocomplete`.
33+
([#2914](https://github.com/Pycord-Development/pycord/pull/2914))
3134
- Added `ui.FileUpload` for modals and the `FileUpload` component.
3235
([#2938](https://github.com/Pycord-Development/pycord/pull/2938))
3336
- Added support for Guild Incidents via `Guild.incidents_data` and

discord/commands/core.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1053,7 +1053,7 @@ async def invoke_autocomplete_callback(self, ctx: AutocompleteContext):
10531053
ctx.value = op.get("value")
10541054
ctx.options = values
10551055

1056-
if len(inspect.signature(option.autocomplete).parameters) == 2:
1056+
if option.autocomplete._is_instance_method:
10571057
instance = getattr(option.autocomplete, "__self__", ctx.cog)
10581058
result = option.autocomplete(instance, ctx)
10591059
else:

discord/commands/options.py

Lines changed: 62 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,15 @@
2828
import logging
2929
import sys
3030
import types
31+
from collections.abc import Awaitable, Callable, Iterable
3132
from enum import Enum
3233
from typing import (
3334
TYPE_CHECKING,
35+
Any,
3436
Literal,
3537
Optional,
3638
Type,
39+
TypeVar,
3740
Union,
3841
get_args,
3942
)
@@ -54,12 +57,13 @@
5457
Thread,
5558
VoiceChannel,
5659
)
57-
from ..commands import ApplicationContext
60+
from ..commands import ApplicationContext, AutocompleteContext
5861
from ..enums import ChannelType, SlashCommandOptionType
5962
from ..enums import Enum as DiscordEnum
6063
from ..utils import MISSING, Undefined, basic_autocomplete
6164

6265
if TYPE_CHECKING:
66+
from ..cog import Cog
6367
from ..ext.commands import Converter
6468
from ..member import Member
6569
from ..message import Attachment
@@ -85,6 +89,25 @@
8589
| Type[DiscordEnum]
8690
)
8791

92+
AutocompleteReturnType = Union[
93+
Iterable["OptionChoice"], Iterable[str], Iterable[int], Iterable[float]
94+
]
95+
T = TypeVar("T", bound=AutocompleteReturnType)
96+
MaybeAwaitable = Union[T, Awaitable[T]]
97+
AutocompleteFunction = Union[
98+
Callable[[AutocompleteContext], MaybeAwaitable[AutocompleteReturnType]],
99+
Callable[[Cog, AutocompleteContext], MaybeAwaitable[AutocompleteReturnType]],
100+
Callable[
101+
[AutocompleteContext, Any], # pyright: ignore [reportExplicitAny]
102+
MaybeAwaitable[AutocompleteReturnType],
103+
],
104+
Callable[
105+
[Cog, AutocompleteContext, Any], # pyright: ignore [reportExplicitAny]
106+
MaybeAwaitable[AutocompleteReturnType],
107+
],
108+
]
109+
110+
88111
__all__ = (
89112
"ThreadOption",
90113
"Option",
@@ -161,15 +184,6 @@ class Option:
161184
max_length: Optional[:class:`int`]
162185
The maximum length of the string that can be entered. Must be between 1 and 6000 (inclusive).
163186
Only applies to Options with an :attr:`input_type` of :class:`str`.
164-
autocomplete: Optional[Callable[[:class:`.AutocompleteContext`], Awaitable[Union[Iterable[:class:`.OptionChoice`], Iterable[:class:`str`], Iterable[:class:`int`], Iterable[:class:`float`]]]]]
165-
The autocomplete handler for the option. Accepts a callable (sync or async)
166-
that takes a single argument of :class:`AutocompleteContext`.
167-
The callable must return an iterable of :class:`str` or :class:`OptionChoice`.
168-
Alternatively, :func:`discord.utils.basic_autocomplete` may be used in place of the callable.
169-
170-
.. note::
171-
172-
Does not validate the input value against the autocomplete results.
173187
channel_types: list[:class:`discord.ChannelType`] | None
174188
A list of channel types that can be selected in this option.
175189
Only applies to Options with an :attr:`input_type` of :class:`discord.SlashCommandOptionType.channel`.
@@ -273,6 +287,7 @@ def __init__(self, input_type: InputType = str, /, description: str | None = Non
273287
self.required: bool = kwargs.pop("required", True) if "default" not in kwargs else False
274288
self.default = kwargs.pop("default", None)
275289

290+
self._autocomplete: AutocompleteFunction | None = None
276291
self.autocomplete = kwargs.pop("autocomplete", None)
277292
if len(enum_choices) > 25:
278293
self.choices: list[OptionChoice] = []
@@ -407,6 +422,43 @@ def to_dict(self) -> dict:
407422
def __repr__(self):
408423
return f"<discord.commands.{self.__class__.__name__} name={self.name}>"
409424

425+
@property
426+
def autocomplete(self) -> AutocompleteFunction | None:
427+
"""
428+
The autocomplete handler for the option. Accepts a callable (sync or async)
429+
that takes a single required argument of :class:`AutocompleteContext` or two arguments
430+
of :class:`discord.Cog` (being the command's cog) and :class:`AutocompleteContext`.
431+
The callable must return an iterable of :class:`str` or :class:`OptionChoice`.
432+
Alternatively, :func:`discord.utils.basic_autocomplete` may be used in place of the callable.
433+
434+
Returns
435+
-------
436+
Optional[AutocompleteFunction]
437+
438+
.. versionchanged:: 2.7
439+
440+
.. note::
441+
Does not validate the input value against the autocomplete results.
442+
"""
443+
return self._autocomplete
444+
445+
@autocomplete.setter
446+
def autocomplete(self, value: AutocompleteFunction | None) -> None:
447+
self._autocomplete = value
448+
# this is done here so it does not have to be computed every time the autocomplete is invoked
449+
if self._autocomplete is not None:
450+
self._autocomplete._is_instance_method = ( # pyright: ignore [reportFunctionMemberAccess]
451+
sum(
452+
1
453+
for param in inspect.signature(
454+
self._autocomplete
455+
).parameters.values()
456+
if param.default == param.empty # pyright: ignore[reportAny]
457+
and param.kind not in (param.VAR_POSITIONAL, param.VAR_KEYWORD)
458+
)
459+
== 2
460+
)
461+
410462

411463
class OptionChoice:
412464
"""

examples/app_commands/slash_autocomplete.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from functools import partial
2+
13
import discord
24
from discord.commands import option
35

@@ -185,4 +187,39 @@ async def autocomplete_basic_example(
185187
await ctx.respond(f"You picked {color} as your color, and {animal} as your animal!")
186188

187189

190+
FRUITS = ["Apple", "Banana", "Orange"]
191+
VEGETABLES = ["Carrot", "Lettuce", "Potato"]
192+
193+
194+
async def food_autocomplete(
195+
ctx: discord.AutocompleteContext, food_type: str
196+
) -> list[discord.OptionChoice]:
197+
items = FRUITS if food_type == "fruit" else VEGETABLES
198+
return [
199+
discord.OptionChoice(name=item)
200+
for item in items
201+
if ctx.value.lower() in item.lower()
202+
]
203+
204+
205+
@bot.slash_command(name="fruit")
206+
@option(
207+
"choice",
208+
description="Pick a fruit",
209+
autocomplete=partial(food_autocomplete, food_type="fruit"),
210+
)
211+
async def get_fruit(ctx: discord.ApplicationContext, choice: str):
212+
await ctx.respond(f'You picked "{choice}"')
213+
214+
215+
@bot.slash_command(name="vegetable")
216+
@option(
217+
"choice",
218+
description="Pick a vegetable",
219+
autocomplete=partial(food_autocomplete, food_type="vegetable"),
220+
)
221+
async def get_vegetable(ctx: discord.ApplicationContext, choice: str):
222+
await ctx.respond(f'You picked "{choice}"')
223+
224+
188225
bot.run("TOKEN")

0 commit comments

Comments
 (0)