From 103fe90d8bc36f7d9b119fffe6c2be162f98c131 Mon Sep 17 00:00:00 2001 From: Rapptz Date: Thu, 5 Feb 2026 08:56:58 -0500 Subject: [PATCH] [commands] Fix decorator order mattering for hybrid commands --- discord/app_commands/commands.py | 102 ++++++++++++++++++------------- discord/ext/commands/hybrid.py | 8 +++ 2 files changed, 66 insertions(+), 44 deletions(-) diff --git a/discord/app_commands/commands.py b/discord/app_commands/commands.py index 36d07d41c..cc0aaef9b 100644 --- a/discord/app_commands/commands.py +++ b/discord/app_commands/commands.py @@ -2180,8 +2180,9 @@ def describe(**parameters: Union[str, locale_str]) -> Callable[[T], T]: ''' def decorator(inner: T) -> T: - if isinstance(inner, Command): - _populate_descriptions(inner._params, parameters) + unwrapped = getattr(inner, '__discord_app_commands_unwrap__', inner) or inner + if isinstance(unwrapped, Command): + _populate_descriptions(unwrapped._params, parameters) else: try: inner.__discord_app_commands_param_description__.update(parameters) # type: ignore # Runtime attribute access @@ -2223,8 +2224,9 @@ def rename(**parameters: Union[str, locale_str]) -> Callable[[T], T]: """ def decorator(inner: T) -> T: - if isinstance(inner, Command): - _populate_renames(inner._params, parameters) + unwrapped = getattr(inner, '__discord_app_commands_unwrap__', inner) or inner + if isinstance(unwrapped, Command): + _populate_renames(unwrapped._params, parameters) else: try: inner.__discord_app_commands_param_rename__.update(parameters) # type: ignore # Runtime attribute access @@ -2292,8 +2294,9 @@ def choices(**parameters: List[Choice[ChoiceT]]) -> Callable[[T], T]: """ def decorator(inner: T) -> T: - if isinstance(inner, Command): - _populate_choices(inner._params, parameters) + unwrapped = getattr(inner, '__discord_app_commands_unwrap__', inner) or inner + if isinstance(unwrapped, Command): + _populate_choices(unwrapped._params, parameters) else: try: inner.__discord_app_commands_param_choices__.update(parameters) # type: ignore # Runtime attribute access @@ -2351,8 +2354,9 @@ def autocomplete(**parameters: AutocompleteCallback[GroupT, ChoiceT]) -> Callabl """ def decorator(inner: T) -> T: - if isinstance(inner, Command): - _populate_autocomplete(inner._params, parameters) + unwrapped = getattr(inner, '__discord_app_commands_unwrap__', inner) or inner + if isinstance(unwrapped, Command): + _populate_autocomplete(unwrapped._params, parameters) else: try: inner.__discord_app_commands_param_autocomplete__.update(parameters) # type: ignore # Runtime attribute access @@ -2408,13 +2412,14 @@ def guilds(*guild_ids: Union[Snowflake, int]) -> Callable[[T], T]: defaults: List[int] = [g if isinstance(g, int) else g.id for g in guild_ids] def decorator(inner: T) -> T: - if isinstance(inner, (Group, ContextMenu)): - inner._guild_ids = defaults - elif isinstance(inner, Command): - if inner.parent is not None: + unwrapped = getattr(inner, '__discord_app_commands_unwrap__', inner) or inner + if isinstance(unwrapped, (Group, ContextMenu)): + unwrapped._guild_ids = defaults + elif isinstance(unwrapped, Command): + if unwrapped.parent is not None: raise ValueError('child commands of a group cannot have default guilds set') - inner._guild_ids = defaults + unwrapped._guild_ids = defaults else: # Runtime attribute assignment inner.__discord_app_commands_default_guilds__ = defaults # type: ignore @@ -2470,13 +2475,14 @@ def check(predicate: Check) -> Callable[[T], T]: """ def decorator(func: CheckInputParameter) -> CheckInputParameter: - if isinstance(func, (Command, ContextMenu)): - func.checks.append(predicate) + unwrapped = getattr(func, '__discord_app_commands_unwrap__', func) or func + if isinstance(unwrapped, (Command, ContextMenu)): + unwrapped.checks.append(predicate) else: if not hasattr(func, '__discord_app_commands_checks__'): - func.__discord_app_commands_checks__ = [] + func.__discord_app_commands_checks__ = [] # type: ignore # Runtime attribute assignment - func.__discord_app_commands_checks__.append(predicate) + func.__discord_app_commands_checks__.append(predicate) # type: ignore # Runtime attribute access return func @@ -2513,10 +2519,11 @@ def guild_only(func: Optional[T] = None) -> Union[T, Callable[[T], T]]: """ def inner(f: T) -> T: - if isinstance(f, (Command, Group, ContextMenu)): - f.guild_only = True - allowed_contexts = f.allowed_contexts or AppCommandContext() - f.allowed_contexts = allowed_contexts + unwrapped = getattr(f, '__discord_app_commands_unwrap__', f) or f + if isinstance(unwrapped, (Command, Group, ContextMenu)): + unwrapped.guild_only = True + allowed_contexts = unwrapped.allowed_contexts or AppCommandContext() + unwrapped.allowed_contexts = allowed_contexts else: f.__discord_app_commands_guild_only__ = True # type: ignore # Runtime attribute assignment @@ -2567,10 +2574,11 @@ def private_channel_only(func: Optional[T] = None) -> Union[T, Callable[[T], T]] """ def inner(f: T) -> T: - if isinstance(f, (Command, Group, ContextMenu)): - f.guild_only = False - allowed_contexts = f.allowed_contexts or AppCommandContext() - f.allowed_contexts = allowed_contexts + unwrapped = getattr(f, '__discord_app_commands_unwrap__', f) or f + if isinstance(unwrapped, (Command, Group, ContextMenu)): + unwrapped.guild_only = False + allowed_contexts = unwrapped.allowed_contexts or AppCommandContext() + unwrapped.allowed_contexts = allowed_contexts else: allowed_contexts = getattr(f, '__discord_app_commands_contexts__', None) or AppCommandContext() f.__discord_app_commands_contexts__ = allowed_contexts # type: ignore # Runtime attribute assignment @@ -2617,10 +2625,11 @@ def dm_only(func: Optional[T] = None) -> Union[T, Callable[[T], T]]: """ def inner(f: T) -> T: - if isinstance(f, (Command, Group, ContextMenu)): - f.guild_only = False - allowed_contexts = f.allowed_contexts or AppCommandContext() - f.allowed_contexts = allowed_contexts + unwrapped = getattr(f, '__discord_app_commands_unwrap__', f) or f + if isinstance(unwrapped, (Command, Group, ContextMenu)): + unwrapped.guild_only = False + allowed_contexts = unwrapped.allowed_contexts or AppCommandContext() + unwrapped.allowed_contexts = allowed_contexts else: allowed_contexts = getattr(f, '__discord_app_commands_contexts__', None) or AppCommandContext() f.__discord_app_commands_contexts__ = allowed_contexts # type: ignore # Runtime attribute assignment @@ -2658,10 +2667,11 @@ def allowed_contexts(guilds: bool = MISSING, dms: bool = MISSING, private_channe """ def inner(f: T) -> T: - if isinstance(f, (Command, Group, ContextMenu)): - f.guild_only = False - allowed_contexts = f.allowed_contexts or AppCommandContext() - f.allowed_contexts = allowed_contexts + unwrapped = getattr(f, '__discord_app_commands_unwrap__', f) or f + if isinstance(unwrapped, (Command, Group, ContextMenu)): + unwrapped.guild_only = False + allowed_contexts = unwrapped.allowed_contexts or AppCommandContext() + unwrapped.allowed_contexts = allowed_contexts else: allowed_contexts = getattr(f, '__discord_app_commands_contexts__', None) or AppCommandContext() f.__discord_app_commands_contexts__ = allowed_contexts # type: ignore # Runtime attribute assignment @@ -2709,9 +2719,10 @@ def guild_install(func: Optional[T] = None) -> Union[T, Callable[[T], T]]: """ def inner(f: T) -> T: - if isinstance(f, (Command, Group, ContextMenu)): - allowed_installs = f.allowed_installs or AppInstallationType() - f.allowed_installs = allowed_installs + unwrapped = getattr(f, '__discord_app_commands_unwrap__', f) or f + if isinstance(unwrapped, (Command, Group, ContextMenu)): + allowed_installs = unwrapped.allowed_installs or AppInstallationType() + unwrapped.allowed_installs = allowed_installs else: allowed_installs = getattr(f, '__discord_app_commands_installation_types__', None) or AppInstallationType() f.__discord_app_commands_installation_types__ = allowed_installs # type: ignore # Runtime attribute assignment @@ -2757,9 +2768,10 @@ def user_install(func: Optional[T] = None) -> Union[T, Callable[[T], T]]: """ def inner(f: T) -> T: - if isinstance(f, (Command, Group, ContextMenu)): - allowed_installs = f.allowed_installs or AppInstallationType() - f.allowed_installs = allowed_installs + unwrapped = getattr(f, '__discord_app_commands_unwrap__', f) or f + if isinstance(unwrapped, (Command, Group, ContextMenu)): + allowed_installs = unwrapped.allowed_installs or AppInstallationType() + unwrapped.allowed_installs = allowed_installs else: allowed_installs = getattr(f, '__discord_app_commands_installation_types__', None) or AppInstallationType() f.__discord_app_commands_installation_types__ = allowed_installs # type: ignore # Runtime attribute assignment @@ -2801,9 +2813,10 @@ def allowed_installs( """ def inner(f: T) -> T: - if isinstance(f, (Command, Group, ContextMenu)): - allowed_installs = f.allowed_installs or AppInstallationType() - f.allowed_installs = allowed_installs + unwrapped = getattr(f, '__discord_app_commands_unwrap__', f) or f + if isinstance(unwrapped, (Command, Group, ContextMenu)): + allowed_installs = unwrapped.allowed_installs or AppInstallationType() + unwrapped.allowed_installs = allowed_installs else: allowed_installs = getattr(f, '__discord_app_commands_installation_types__', None) or AppInstallationType() f.__discord_app_commands_installation_types__ = allowed_installs # type: ignore # Runtime attribute assignment @@ -2874,8 +2887,9 @@ def default_permissions(perms_obj: Optional[Permissions] = None, /, **perms: Unp permissions = Permissions(**perms) def decorator(func: T) -> T: - if isinstance(func, (Command, Group, ContextMenu)): - func.default_permissions = permissions + unwrapped = getattr(func, '__discord_app_commands_unwrap__', func) or func + if isinstance(unwrapped, (Command, Group, ContextMenu)): + unwrapped.default_permissions = permissions else: func.__discord_app_commands_default_permissions__ = permissions # type: ignore # Runtime attribute assignment diff --git a/discord/ext/commands/hybrid.py b/discord/ext/commands/hybrid.py index 6687104cb..e7366c6e8 100644 --- a/discord/ext/commands/hybrid.py +++ b/discord/ext/commands/hybrid.py @@ -534,6 +534,10 @@ class HybridCommand(Command[CogT, P, T]): HybridAppCommand(self) if self.with_app_command else None ) + @property + def __discord_app_commands_unwrap__(self) -> Optional[HybridAppCommand[CogT, Any, T]]: + return self.app_command + @property def cog(self) -> CogT: return self._cog @@ -702,6 +706,10 @@ class HybridGroup(Group[CogT, P, T]): return None return self.app_command.get_command(self.fallback) # type: ignore + @property + def __discord_app_commands_unwrap__(self) -> Optional[app_commands.Group]: + return self.app_command + @property def cog(self) -> CogT: return self._cog