Skip to content

Commit

Permalink
[commands] Fix nested hybrid groups inserting manual app commands
Browse files Browse the repository at this point in the history
  • Loading branch information
Rapptz committed Aug 24, 2023
1 parent c7f6e95 commit 69e9bc9
Show file tree
Hide file tree
Showing 2 changed files with 98 additions and 0 deletions.
16 changes: 16 additions & 0 deletions discord/ext/commands/cog.py
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,7 @@ def __new__(cls, *args: Any, **kwargs: Any) -> Self:

# Register the application commands
children: List[Union[app_commands.Group, app_commands.Command[Self, ..., Any]]] = []
app_command_refs: Dict[str, Union[app_commands.Group, app_commands.Command[Self, ..., Any]]] = {}

if cls.__cog_is_app_commands_group__:
group = app_commands.Group(
Expand All @@ -331,6 +332,16 @@ def __new__(cls, *args: Any, **kwargs: Any) -> Self:
# Get the latest parent reference
parent = lookup[parent.qualified_name] # type: ignore

# Hybrid commands already deal with updating the reference
# Due to the copy below, so we need to handle them specially
if hasattr(parent, '__commands_is_hybrid__') and hasattr(command, '__commands_is_hybrid__'):
app_command: Optional[Union[app_commands.Group, app_commands.Command[Self, ..., Any]]] = getattr(

Check failure on line 338 in discord/ext/commands/cog.py

View workflow job for this annotation

GitHub Actions / check 3.x

Declaration "app_command" is obscured by a declaration of the same name (reportGeneralTypeIssues)
command, 'app_command', None
)
updated = app_command_refs.get(command.qualified_name)
if app_command and updated:
command.app_command = updated # type: ignore # Safe attribute access

# Update our parent's reference to our self
parent.remove_command(command.name) # type: ignore
parent.add_command(command) # type: ignore
Expand All @@ -345,6 +356,11 @@ def __new__(cls, *args: Any, **kwargs: Any) -> Self:
# The type checker does not see the app_command attribute even though it exists
command.app_command = app_command # type: ignore

# Update all the references to point to the new copy
if isinstance(app_command, app_commands.Group):
for child in app_command.walk_commands():
app_command_refs[child.qualified_name] = child

if self.__cog_app_commands_group__:
children.append(app_command) # type: ignore # Somehow it thinks it can be None here

Expand Down
82 changes: 82 additions & 0 deletions tests/test_app_commands_group.py
Original file line number Diff line number Diff line change
Expand Up @@ -397,3 +397,85 @@ async def my_inner_command(self, interaction: discord.Interaction) -> None:
assert cog.inner.my_command.parent is cog.inner
assert cog.my_inner_command.parent is cog.inner
assert cog.my_inner_command.binding is cog


def test_cog_hybrid_group_manual_command():
class MyCog(commands.Cog):
@commands.hybrid_group()
async def first(self, ctx: commands.Context) -> None:
...

@first.command(name='both')
async def second_both(self, ctx: commands.Context) -> None:
...

@first.app_command.command(name='second')
async def second_app(self, interaction: discord.Interaction) -> None:
...

client = discord.Client(intents=discord.Intents.default())
tree = app_commands.CommandTree(client)

cog = MyCog()
tree.add_command(cog.first.app_command)

assert cog.first is not MyCog.first
assert cog.second_both is not MyCog.second_both
assert cog.second_app is not MyCog.second_app
assert cog.first.parent is None
assert cog.second_both.parent is cog.first
assert cog.second_app.parent is cog.first.app_command
assert cog.second_app.binding is cog
assert tree.get_command('first') is cog.first.app_command

first = tree.get_command('first')
assert isinstance(first, app_commands.Group)
both = first.get_command('both')
assert isinstance(both, app_commands.Command)
assert both.parent is first
assert both.binding is cog

second = first.get_command('second')
assert isinstance(second, app_commands.Command)
assert second.parent is first
assert second.binding is cog


def test_cog_hybrid_group_manual_nested_command():
class MyCog(commands.Cog):
@commands.hybrid_group()
async def first(self, ctx: commands.Context) -> None:
pass

@first.group()
async def second(self, ctx: commands.Context) -> None:
pass

@second.app_command.command()
async def third(self, interaction: discord.Interaction) -> None:
pass

client = discord.Client(intents=discord.Intents.default())
tree = app_commands.CommandTree(client)

cog = MyCog()
tree.add_command(cog.first.app_command)

assert cog.first is not MyCog.first
assert cog.second is not MyCog.second
assert cog.third is not MyCog.third
assert cog.first.parent is None
assert cog.second.parent is cog.first
assert cog.third.parent is cog.second.app_command
assert cog.third.binding is cog

first = tree.get_command('first')
assert isinstance(first, app_commands.Group)

second = first.get_command('second')
assert isinstance(second, app_commands.Group)

third = second.get_command('third')
assert isinstance(third, app_commands.Command)
assert third.parent is second
assert third.binding is cog

0 comments on commit 69e9bc9

Please sign in to comment.