Repository: Pycord-Development/pycord

Issue:
'BridgeCommand' object has no attribute 'hidden' when calling "filter_commands()"
### Summary

I noticed that my custom help command broke recently. Specifically calling filter_commands(mapping[cog]) seems to be the problem here.

### Reproduction Steps

1. Run the code below.
2. Try to run the !help command.
3. See it fail.

### Minimal Reproducible Code

```python
import discord
from discord.ext import bridge, commands

intents = discord.Intents.default()
intents.message_content = True

bot = bridge.Bot(command_prefix="!", intents=intents)


class TestCog(commands.Cog, name="test cog"):
    def __init__(self, bot):
        self.bot: commands.Bot = bot
    @bridge.bridge_command()
    async def hello(self, ctx):
        """hello"""
        await ctx.respond("Hello!")

class BetterHelpCommand(commands.HelpCommand):
    """
    Custom help command for the bot.
    """
    async def send_embed(self, embed):
        await self.get_destination().send(embed=embed)

    def blank_line(self, embed):
        embed.add_field(name='_ _', value='_ _', inline=False)

    async def send_bot_help(self, mapping):
        e = discord.Embed(title="test")
        e.add_field(name='Contribute at', value="test", inline=False)
        cogs = []
        for cog in mapping.keys():
            n = await self.filter_commands(mapping[cog])
            cogs.append((cog, n))
        cogs = [x for x in cogs if len(x[1]) > 0]
        for i, (cog, cmds) in enumerate(cogs):
            if i % 2 == 0:
                self.blank_line(e)
            h = '\n'.join([cmd.name for cmd in cmds])
            if cog is None:
                e.add_field(name='builtin', value=h, inline=True)
            else:
                e.add_field(name=cog.qualified_name, value=h, inline=True)
        await self.send_embed(e)

bot.help_command = BetterHelpCommand()
bot.add_cog(TestCog(bot))

bot.run("token")
```


### Expected Results

The `filter_commands(mapping[cog])` line returns a filtered list of commands.

### Actual Results

The following error is generated:
```
Ignoring exception in command help:
Traceback (most recent call last):
  File "/home/dmytro/.local/lib/python3.10/site-packages/discord/ext/commands/core.py", line 178, in wrapped
    ret = await coro(*args, **kwargs)
  File "/home/dmytro/.local/lib/python3.10/site-packages/discord/ext/commands/help.py", line 858, in command_callback
    return await self.send_bot_help(mapping)
  File "/home/dmytro/Documents/projects/minimal bot/./main.py", line 33, in send_bot_help
    n = await self.filter_commands(mapping[cog])
  File "/home/dmytro/.local/lib/python3.10/site-packages/discord/ext/commands/help.py", line 604, in filter_commands
    for cmd in iterator:
  File "/home/dmytro/.local/lib/python3.10/site-packages/discord/ext/commands/help.py", line 584, in <lambda>
    else filter(lambda c: not c.hidden, prefix_commands)
AttributeError: 'BridgeCommand' object has no attribute 'hidden'

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "/home/dmytro/.local/lib/python3.10/site-packages/discord/ext/commands/bot.py", line 347, in invoke
    await ctx.command.invoke(ctx)
  File "/home/dmytro/.local/lib/python3.10/site-packages/discord/ext/commands/core.py", line 950, in invoke
    await injected(*ctx.args, **ctx.kwargs)
  File "/home/dmytro/.local/lib/python3.10/site-packages/discord/ext/commands/core.py", line 187, in wrapped
    raise CommandInvokeError(exc) from exc
discord.ext.commands.errors.CommandInvokeError: Command raised an exception: AttributeError: 'BridgeCommand' object has no attribute 'hidden'
```

### Intents

discord.Intents.default()

### System Information

```
/home/dmytro/.local/lib/python3.10/site-packages/discord/__main__.py:47: DeprecationWarning: VersionInfo.releaselevel is deprecated since version 2.3, consider using release_level instead.
  "- py-cord v{0.major}.{0.minor}.{0.micro}-{0.releaselevel}".format(version_info)
/home/dmytro/.local/lib/python3.10/site-packages/discord/__main__.py:49: DeprecationWarning: VersionInfo.releaselevel is deprecated since version 2.3, consider using release_level instead.
  if version_info.releaselevel != "final":
- Python v3.10.6-final
- py-cord v2.3.2-final
- aiohttp v3.8.3
- system info: Linux 5.15.0-56-generic #62-Ubuntu SMP Tue Nov 22 19:54:14 UTC 2022
```

### Checklist

- [x] I have searched the open issues for duplicates.
- [X] I have shown the entire traceback, if possible.
- [X] I have removed my token from display, if visible.

### Additional Context

_No response_


Discussion:
Also when trying to call `reload_extension()` the methods under the `bridge_command` decorator trigger an error:
```
  File "/usr/local/lib/python3.10/site-packages/discord/cog.py", line 1118, in reload_extension
    self._remove_module_references(lib.__name__)
  File "/usr/local/lib/python3.10/site-packages/discord/cog.py", line 722, in _remove_module_references
    self.remove_cog(cog_name)
  File "/usr/local/lib/python3.10/site-packages/discord/cog.py", line 706, in remove_cog
    cog._eject(self)
  File "/usr/local/lib/python3.10/site-packages/discord/cog.py", line 592, in _eject
    bot.remove_command(command.name)
AttributeError: 'BridgeCommand' object has no attribute 'name'
```

Resolving patch:
diff --git a/discord/ext/bridge/bot.py b/discord/ext/bridge/bot.py
--- a/discord/ext/bridge/bot.py
+++ b/discord/ext/bridge/bot.py
@@ -25,6 +25,7 @@
 from __future__ import annotations
 
 from abc import ABC
+from collections.abc import Iterator
 
 from discord.commands import ApplicationContext
 from discord.errors import CheckFailure, DiscordException
@@ -60,6 +61,21 @@ def bridge_commands(self) -> list[BridgeCommand | BridgeCommandGroup]:
 
         return cmds
 
+    def walk_bridge_commands(
+        self,
+    ) -> Iterator[BridgeCommand | BridgeCommandGroup]:
+        """An iterator that recursively walks through all the bot's bridge commands.
+
+        Yields
+        ------
+        Union[:class:`.BridgeCommand`, :class:`.BridgeCommandGroup`]
+            A bridge command or bridge group of the bot.
+        """
+        for cmd in self._bridge_commands:
+            yield cmd
+            if isinstance(cmd, BridgeCommandGroup):
+                yield from cmd.walk_commands()
+
     async def get_application_context(
         self, interaction: Interaction, cls=None
     ) -> BridgeApplicationContext:
diff --git a/discord/ext/bridge/core.py b/discord/ext/bridge/core.py
--- a/discord/ext/bridge/core.py
+++ b/discord/ext/bridge/core.py
@@ -25,6 +25,7 @@
 from __future__ import annotations
 
 import inspect
+from collections.abc import Iterator
 from typing import TYPE_CHECKING, Any, Callable
 
 import discord.commands.options
@@ -38,7 +39,7 @@
     SlashCommandOptionType,
 )
 
-from ...utils import filter_params, find, get
+from ...utils import MISSING, find, get
 from ..commands import BadArgument
 from ..commands import Bot as ExtBot
 from ..commands import (
@@ -156,6 +157,8 @@ class BridgeCommand:
         The prefix-based version of this bridge command.
     """
 
+    __special_attrs__ = ["slash_variant", "ext_variant", "parent"]
+
     def __init__(self, callback, **kwargs):
         self.parent = kwargs.pop("parent", None)
         self.slash_variant: BridgeSlashCommand = kwargs.pop(
@@ -166,13 +169,10 @@ def __init__(self, callback, **kwargs):
         ) or BridgeExtCommand(callback, **kwargs)
 
     @property
-    def name_localizations(self) -> dict[str, str]:
+    def name_localizations(self) -> dict[str, str] | None:
         """Returns name_localizations from :attr:`slash_variant`
-
         You can edit/set name_localizations directly with
-
         .. code-block:: python3
-
             bridge_command.name_localizations["en-UK"] = ...  # or any other locale
             # or
             bridge_command.name_localizations = {"en-UK": ..., "fr-FR": ...}
@@ -184,13 +184,10 @@ def name_localizations(self, value):
         self.slash_variant.name_localizations = value
 
     @property
-    def description_localizations(self) -> dict[str, str]:
+    def description_localizations(self) -> dict[str, str] | None:
         """Returns description_localizations from :attr:`slash_variant`
-
         You can edit/set description_localizations directly with
-
         .. code-block:: python3
-
             bridge_command.description_localizations["en-UK"] = ...  # or any other locale
             # or
             bridge_command.description_localizations = {"en-UK": ..., "fr-FR": ...}
@@ -201,9 +198,34 @@ def description_localizations(self) -> dict[str, str]:
     def description_localizations(self, value):
         self.slash_variant.description_localizations = value
 
-    @property
-    def qualified_name(self) -> str:
-        return self.slash_variant.qualified_name
+    def __getattribute__(self, name):
+        try:
+            # first, look for the attribute on the bridge command
+            return super().__getattribute__(name)
+        except AttributeError as e:
+            # if it doesn't exist, check this list, if the name of
+            # the parameter is here
+            if name is self.__special_attrs__:
+                raise e
+
+            # looks up the result in the variants.
+            # slash cmd prioritized
+            result = getattr(self.slash_variant, name, MISSING)
+            try:
+                if result is MISSING:
+                    return getattr(self.ext_variant, name)
+                return result
+            except AttributeError:
+                raise AttributeError(
+                    f"'{self.__class__.__name__}' object has no attribute '{name}'"
+                )
+
+    def __setattr__(self, name, value) -> None:
+        if name not in self.__special_attrs__:
+            setattr(self.slash_variant, name, value)
+            setattr(self.ext_variant, name, value)
+
+        return super().__setattr__(name, value)
 
     def add_to(self, bot: ExtBot) -> None:
         """Adds the command to a bot. This method is inherited by :class:`.BridgeCommandGroup`.
@@ -321,6 +343,14 @@ class BridgeCommandGroup(BridgeCommand):
         If :func:`map_to` is used, the mapped slash command.
     """
 
+    __special_attrs__ = [
+        "slash_variant",
+        "ext_variant",
+        "parent",
+        "subcommands",
+        "mapped",
+    ]
+
     ext_variant: BridgeExtGroup
     slash_variant: BridgeSlashGroup
 
@@ -341,6 +371,16 @@ def __init__(self, callback, *args, **kwargs):
             kwargs.update(map_to)
             self.mapped = self.slash_variant.command(**kwargs)(callback)
 
+    def walk_commands(self) -> Iterator[BridgeCommand]:
+        """An iterator that recursively walks through all the bridge group's subcommands.
+
+        Yields
+        ------
+        :class:`.BridgeCommand`
+            A bridge command of this bridge group.
+        """
+        yield from self.subcommands
+
     def command(self, *args, **kwargs):
         """A decorator to register a function as a subcommand.
 


Test patch: