diff --git a/changelog/982.bugfix.rst b/changelog/982.bugfix.rst new file mode 100644 index 0000000000..548e13e497 --- /dev/null +++ b/changelog/982.bugfix.rst @@ -0,0 +1 @@ +|commands| Fix autocompletion not working for options of nested :func:`~ext.commands.injection`\s. diff --git a/disnake/ext/commands/params.py b/disnake/ext/commands/params.py index e72f7f5b4f..55ed5b716e 100644 --- a/disnake/ext/commands/params.py +++ b/disnake/ext/commands/params.py @@ -1103,13 +1103,31 @@ def collect_params( ) +def apply_injection_autocompleters( + injection: Injection, params: list[ParamInfo], location: str +) -> None: + """Assign an injection's autocompleters to the matching collected params *in-place*""" + if not injection.autocompleters: + return + + lookup = {p.name: p for p in params} + for name, func in injection.autocompleters.items(): + param = lookup.get(name) + if param is None: + msg = f"Option '{name}' doesn't exist in '{location}'" + raise ValueError(msg) + param.autocomplete = func + + def collect_nested_params(function: Callable[..., Any]) -> list[ParamInfo]: """Collect all options from a function""" # TODO: Have these be actually sorted properly and not have injections always at the end _, _, paraminfos, injections = collect_params(function) for injection in injections.values(): - paraminfos += collect_nested_params(injection.function) + nested = collect_nested_params(injection.function) + apply_injection_autocompleters(injection, nested, injection.function.__name__) + paraminfos += nested return sorted(paraminfos, key=lambda param: not param.required) @@ -1198,14 +1216,7 @@ def expand_params(command: AnySlashCommand) -> list[Option]: for injection in injections.values(): collected = collect_nested_params(injection.function) - if injection.autocompleters: - lookup = {p.name: p for p in collected} - for name, func in injection.autocompleters.items(): - param = lookup.get(name) - if param is None: - msg = f"Option '{name}' doesn't exist in '{command.qualified_name}'" - raise ValueError(msg) - param.autocomplete = func + apply_injection_autocompleters(injection, collected, command.qualified_name) params += collected params = sorted(params, key=lambda param: not param.required) diff --git a/tests/ext/commands/test_params.py b/tests/ext/commands/test_params.py index 41c5b04787..b4c4f645a1 100644 --- a/tests/ext/commands/test_params.py +++ b/tests/ext/commands/test_params.py @@ -311,3 +311,56 @@ def func( assert cog is None assert inter is not None assert params.keys() == {"a"} + + +class TestNestedInjectionAutocomplete: + def test_nested_injection_autocomplete(self) -> None: + @commands.injection() + def inner(a: str, b: str) -> str: + return a + b + + @inner.autocomplete("a") + async def autocomp_a(inter, value) -> list[str]: + return [value] + + @commands.injection() + def outer(c: str, d: str = inner) -> str: # type: ignore[assignment] + return c + d + + @outer.autocomplete("c") + async def autocomp_c(inter, value) -> list[str]: + return [value] + + @commands.slash_command() + async def cmd( + inter: disnake.ApplicationCommandInteraction, + arg: str = outer, # type: ignore[assignment] + ) -> None: ... + + assert cmd.autocompleters.keys() == {"a", "c"} + assert {o.name: o.autocomplete for o in cmd.body.options} == { + "a": True, + "b": False, + "c": True, + } + + def test_nested_injection_unknown_option(self) -> None: + @commands.injection() + def inner(a: str) -> str: + return a + + @inner.autocomplete("nonexistent") + async def autocomp(inter, value) -> list[str]: + return [value] + + @commands.injection() + def outer(b: str, c: str = inner) -> str: # type: ignore[assignment] + return b + c + + with pytest.raises(ValueError, match="Option 'nonexistent' doesn't exist"): + + @commands.slash_command() + async def cmd( + inter: disnake.ApplicationCommandInteraction, + arg: str = outer, # type: ignore[assignment] + ) -> None: ...