Skip to content

Commit b190b7f

Browse files
feat: ✨ Allow for functools.partial and functions returning an awaitable as autocomplete (#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 (#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 <[email protected]> * 📝 CHANGELOG.md Signed-off-by: Paillat <[email protected]> * Update CHANGELOG.md Signed-off-by: Paillat <[email protected]> * 📝 Update CHANGELOG.md * 🐛 Missing description kwarg in autocomplete example --------- Signed-off-by: Paillat <[email protected]> Signed-off-by: Paillat <[email protected]> Signed-off-by: Paillat <[email protected]> Signed-off-by: Lala Sabathil <[email protected]> Co-authored-by: Lala Sabathil <[email protected]>
1 parent aef54eb commit b190b7f

File tree

4 files changed

+103
-11
lines changed

4 files changed

+103
-11
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ These changes are available on the `master` branch, but have not yet been releas
3030
- Adds pre-typed and pre-constructed with select_type `ui.Select` aliases for the
3131
different select types: `ui.StringSelect`, `ui.UserSelect`, `ui.RoleSelect`,
3232
`ui.MentionableSelect`, and `ui.ChannelSelect`.
33+
- Added the ability to use functions with any number of optional arguments and functions
34+
returning an awaitable as `Option.autocomplete`.
35+
([#2914](https://github.com/Pycord-Development/pycord/pull/2914))
3336
- Added `ui.FileUpload` for modals and the `FileUpload` component.
3437
([#2938](https://github.com/Pycord-Development/pycord/pull/2938))
3538
- 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
@@ -1119,7 +1119,7 @@ async def invoke_autocomplete_callback(self, ctx: AutocompleteContext):
11191119
ctx.value = op.get("value")
11201120
ctx.options = values
11211121

1122-
if len(inspect.signature(option.autocomplete).parameters) == 2:
1122+
if option.autocomplete._is_instance_method:
11231123
instance = getattr(option.autocomplete, "__self__", ctx.cog)
11241124
result = option.autocomplete(instance, ctx)
11251125
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,13 +57,14 @@
5457
Thread,
5558
VoiceChannel,
5659
)
57-
from ..commands import ApplicationContext
60+
from ..commands import ApplicationContext, AutocompleteContext
5861
from ..enums import ChannelType
5962
from ..enums import Enum as DiscordEnum
6063
from ..enums import SlashCommandOptionType
6164
from ..utils import MISSING, basic_autocomplete
6265

6366
if TYPE_CHECKING:
67+
from ..cog import Cog
6468
from ..ext.commands import Converter
6569
from ..member import Member
6670
from ..message import Attachment
@@ -86,6 +90,25 @@
8690
Type[DiscordEnum],
8791
]
8892

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

309+
self._autocomplete: AutocompleteFunction | None = None
295310
self.autocomplete = kwargs.pop("autocomplete", None)
296311
if len(enum_choices) > 25:
297312
self.choices: list[OptionChoice] = []
@@ -443,6 +458,43 @@ def to_dict(self) -> dict:
443458
def __repr__(self):
444459
return f"<discord.commands.{self.__class__.__name__} name={self.name}>"
445460

461+
@property
462+
def autocomplete(self) -> AutocompleteFunction | None:
463+
"""
464+
The autocomplete handler for the option. Accepts a callable (sync or async)
465+
that takes a single required argument of :class:`AutocompleteContext` or two arguments
466+
of :class:`discord.Cog` (being the command's cog) and :class:`AutocompleteContext`.
467+
The callable must return an iterable of :class:`str` or :class:`OptionChoice`.
468+
Alternatively, :func:`discord.utils.basic_autocomplete` may be used in place of the callable.
469+
470+
Returns
471+
-------
472+
Optional[AutocompleteFunction]
473+
474+
.. versionchanged:: 2.7
475+
476+
.. note::
477+
Does not validate the input value against the autocomplete results.
478+
"""
479+
return self._autocomplete
480+
481+
@autocomplete.setter
482+
def autocomplete(self, value: AutocompleteFunction | None) -> None:
483+
self._autocomplete = value
484+
# this is done here so it does not have to be computed every time the autocomplete is invoked
485+
if self._autocomplete is not None:
486+
self._autocomplete._is_instance_method = ( # pyright: ignore [reportFunctionMemberAccess]
487+
sum(
488+
1
489+
for param in inspect.signature(
490+
self._autocomplete
491+
).parameters.values()
492+
if param.default == param.empty # pyright: ignore[reportAny]
493+
and param.kind not in (param.VAR_POSITIONAL, param.VAR_KEYWORD)
494+
)
495+
== 2
496+
)
497+
446498

447499
class OptionChoice:
448500
"""

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

@@ -196,4 +198,39 @@ async def autocomplete_basic_example(
196198
await ctx.respond(f"You picked {color} as your color, and {animal} as your animal!")
197199

198200

201+
FRUITS = ["Apple", "Banana", "Orange"]
202+
VEGETABLES = ["Carrot", "Lettuce", "Potato"]
203+
204+
205+
async def food_autocomplete(
206+
ctx: discord.AutocompleteContext, food_type: str
207+
) -> list[discord.OptionChoice]:
208+
items = FRUITS if food_type == "fruit" else VEGETABLES
209+
return [
210+
discord.OptionChoice(name=item)
211+
for item in items
212+
if ctx.value.lower() in item.lower()
213+
]
214+
215+
216+
@bot.slash_command(name="fruit")
217+
@option(
218+
"choice",
219+
description="Pick a fruit",
220+
autocomplete=partial(food_autocomplete, food_type="fruit"),
221+
)
222+
async def get_fruit(ctx: discord.ApplicationContext, choice: str):
223+
await ctx.respond(f'You picked "{choice}"')
224+
225+
226+
@bot.slash_command(name="vegetable")
227+
@option(
228+
"choice",
229+
description="Pick a vegetable",
230+
autocomplete=partial(food_autocomplete, food_type="vegetable"),
231+
)
232+
async def get_vegetable(ctx: discord.ApplicationContext, choice: str):
233+
await ctx.respond(f'You picked "{choice}"')
234+
235+
199236
bot.run("TOKEN")

0 commit comments

Comments
 (0)