Skip to content

Commit 30912d6

Browse files
refactor: ♻️ Refactor weird checks in CogMeta and fix some typing and other qol things (#2730)
* 🏷️ Type stuff * ♻️ Refactor weird checks and don't redefine filter every time * ♻️ Extract name validation logic to helper method `_validate_name_prefix` * ♻️ Rename `elem, value` to `attr_name, attr_value` * ♻️ Extract attributes processing to `_process_attributes` and simplify its logic * 🐛 Fix wrong order broke static listeners * ♻️ Use a list comprehension for __cog_listeners__ * ♻️ Extract command update logic to `_update_command` * ♻️ Avoid repeating staticmethod check * ♻️ Import `Generator` & `Mapping` from `collections.abc` instead of `typing` Deprecated since python 3.9, py-cord supports 3.9+ * 🏷️ Fix ignore comments * 📝 Change comment wording --------- Signed-off-by: Paillat <[email protected]> Co-authored-by: Lala Sabathil <[email protected]>
1 parent 218e065 commit 30912d6

File tree

3 files changed

+147
-107
lines changed

3 files changed

+147
-107
lines changed

discord/cog.py

Lines changed: 143 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,17 @@
3131
import pathlib
3232
import sys
3333
import types
34-
from typing import Any, Callable, ClassVar, Generator, Mapping, TypeVar, overload
34+
from collections.abc import Generator, Mapping
35+
from typing import (
36+
TYPE_CHECKING,
37+
Any,
38+
Callable,
39+
ClassVar,
40+
TypeVar,
41+
overload,
42+
)
43+
44+
from typing_extensions import TypeGuard
3545

3646
import discord.utils
3747

@@ -43,6 +53,10 @@
4353
_BaseCommand,
4454
)
4555

56+
if TYPE_CHECKING:
57+
from .ext.bridge import BridgeCommand
58+
59+
4660
__all__ = (
4761
"CogMeta",
4862
"Cog",
@@ -59,6 +73,118 @@ def _is_submodule(parent: str, child: str) -> bool:
5973
return parent == child or child.startswith(f"{parent}.")
6074

6175

76+
def _is_bridge_command(command: Any) -> TypeGuard[BridgeCommand]:
77+
return getattr(command, "__bridge__", False)
78+
79+
80+
def _name_filter(c: Any) -> str:
81+
return (
82+
"app"
83+
if isinstance(c, ApplicationCommand)
84+
else ("bridge" if not _is_bridge_command(c) else "ext")
85+
)
86+
87+
88+
def _validate_name_prefix(base_class: type, name: str) -> None:
89+
if name.startswith(("cog_", "bot_")):
90+
raise TypeError(
91+
f"Commands or listeners must not start with cog_ or bot_ (in method {base_class}.{name})"
92+
)
93+
94+
95+
def _process_attributes(
96+
base: type,
97+
) -> tuple[dict[str, Any], dict[str, Any]]: # pyright: ignore[reportExplicitAny]
98+
commands: dict[str, _BaseCommand | BridgeCommand] = {}
99+
listeners: dict[str, Callable[..., Any]] = {}
100+
101+
for attr_name, attr_value in base.__dict__.items():
102+
if attr_name in commands:
103+
del commands[attr_name]
104+
if attr_name in listeners:
105+
del listeners[attr_name]
106+
107+
if getattr(attr_value, "parent", None) and isinstance(
108+
attr_value, ApplicationCommand
109+
):
110+
# Skip application commands if they are a part of a group
111+
# Since they are already added when the group is added
112+
continue
113+
114+
is_static_method = isinstance(attr_value, staticmethod)
115+
if is_static_method:
116+
attr_value = attr_value.__func__
117+
118+
if inspect.iscoroutinefunction(attr_value) and getattr(
119+
attr_value, "__cog_listener__", False
120+
):
121+
_validate_name_prefix(base, attr_name)
122+
listeners[attr_name] = attr_value
123+
continue
124+
125+
if isinstance(attr_value, _BaseCommand) or _is_bridge_command(attr_value):
126+
if is_static_method:
127+
raise TypeError(
128+
f"Command in method {base}.{attr_name!r} must not be staticmethod."
129+
)
130+
_validate_name_prefix(base, attr_name)
131+
132+
if isinstance(attr_value, _BaseCommand):
133+
commands[attr_name] = attr_value
134+
135+
if _is_bridge_command(attr_value) and not attr_value.parent:
136+
commands[f"ext_{attr_name}"] = attr_value.ext_variant
137+
commands[f"app_{attr_name}"] = attr_value.slash_variant
138+
commands[attr_name] = attr_value
139+
for cmd in getattr(attr_value, "subcommands", []):
140+
commands[f"ext_{cmd.ext_variant.qualified_name}"] = cmd.ext_variant
141+
142+
return commands, listeners
143+
144+
145+
def _update_command(
146+
command: _BaseCommand | BridgeCommand,
147+
guild_ids: list[int],
148+
lookup_table: dict[str, _BaseCommand | BridgeCommand],
149+
new_cls: type[Cog],
150+
) -> None:
151+
if isinstance(command, ApplicationCommand) and not command.guild_ids and guild_ids:
152+
command.guild_ids = guild_ids
153+
154+
if not isinstance(command, SlashCommandGroup) and not _is_bridge_command(command):
155+
# ignore bridge commands
156+
cmd: BridgeCommand | _BaseCommand | None = getattr(
157+
new_cls,
158+
command.callback.__name__,
159+
None, # pyright: ignore[reportUnknownMemberType,reportUnknownArgumentType,reportAttributeAccessIssue]
160+
)
161+
if _is_bridge_command(cmd):
162+
setattr(
163+
cmd,
164+
f"{_name_filter(command).replace('app', 'slash')}_variant",
165+
command,
166+
)
167+
else:
168+
setattr(
169+
new_cls,
170+
command.callback.__name__,
171+
command, # pyright: ignore [reportAttributeAccessIssue, reportUnknownArgumentType, reportUnknownMemberType]
172+
)
173+
174+
parent: (
175+
BridgeCommand | _BaseCommand | None
176+
) = ( # pyright: ignore [reportUnknownMemberType, reportUnknownVariableType]
177+
command.parent # pyright: ignore [reportAttributeAccessIssue]
178+
)
179+
if parent is not None:
180+
# Get the latest parent reference
181+
parent = lookup_table[f"{_name_filter(command)}_{parent.qualified_name}"] # type: ignore # pyright: ignore[reportUnknownMemberType]
182+
183+
# Update the parent's reference to our self
184+
parent.remove_command(command.name) # type: ignore # pyright: ignore[reportAttributeAccessIssue, reportUnknownMemberType]
185+
parent.add_command(command) # type: ignore # pyright: ignore[reportAttributeAccessIssue, reportUnknownMemberType]
186+
187+
62188
class CogMeta(type):
63189
"""A metaclass for defining a cog.
64190
@@ -127,7 +253,7 @@ async def bar(self, ctx):
127253

128254
__cog_name__: str
129255
__cog_settings__: dict[str, Any]
130-
__cog_commands__: list[ApplicationCommand]
256+
__cog_commands__: list[_BaseCommand | BridgeCommand]
131257
__cog_listeners__: list[tuple[str, str]]
132258
__cog_guild_ids__: list[int]
133259

@@ -142,128 +268,38 @@ def __new__(cls: type[CogMeta], *args: Any, **kwargs: Any) -> CogMeta:
142268
description = inspect.cleandoc(attrs.get("__doc__", ""))
143269
attrs["__cog_description__"] = description
144270

145-
commands = {}
146-
listeners = {}
147-
no_bot_cog = (
148-
"Commands or listeners must not start with cog_ or bot_ (in method"
149-
" {0.__name__}.{1})"
150-
)
271+
commands: dict[str, _BaseCommand | BridgeCommand] = {}
272+
listeners: dict[str, Callable[..., Any]] = {}
151273

152274
new_cls = super().__new__(cls, name, bases, attrs, **kwargs)
153275

154276
for base in reversed(new_cls.__mro__):
155-
for elem, value in base.__dict__.items():
156-
if elem in commands:
157-
del commands[elem]
158-
if elem in listeners:
159-
del listeners[elem]
160-
161-
if getattr(value, "parent", None) and isinstance(
162-
value, ApplicationCommand
163-
):
164-
# Skip commands if they are a part of a group
165-
continue
166-
167-
is_static_method = isinstance(value, staticmethod)
168-
if is_static_method:
169-
value = value.__func__
170-
if isinstance(value, _BaseCommand):
171-
if is_static_method:
172-
raise TypeError(
173-
f"Command in method {base}.{elem!r} must not be"
174-
" staticmethod."
175-
)
176-
if elem.startswith(("cog_", "bot_")):
177-
raise TypeError(no_bot_cog.format(base, elem))
178-
commands[elem] = value
179-
180-
# a test to see if this value is a BridgeCommand
181-
if hasattr(value, "add_to") and not getattr(value, "parent", None):
182-
if is_static_method:
183-
raise TypeError(
184-
f"Command in method {base}.{elem!r} must not be"
185-
" staticmethod."
186-
)
187-
if elem.startswith(("cog_", "bot_")):
188-
raise TypeError(no_bot_cog.format(base, elem))
189-
190-
commands[f"ext_{elem}"] = value.ext_variant
191-
commands[f"app_{elem}"] = value.slash_variant
192-
commands[elem] = value
193-
for cmd in getattr(value, "subcommands", []):
194-
commands[f"ext_{cmd.ext_variant.qualified_name}"] = (
195-
cmd.ext_variant
196-
)
197-
198-
if inspect.iscoroutinefunction(value):
199-
try:
200-
getattr(value, "__cog_listener__")
201-
except AttributeError:
202-
continue
203-
else:
204-
if elem.startswith(("cog_", "bot_")):
205-
raise TypeError(no_bot_cog.format(base, elem))
206-
listeners[elem] = value
277+
new_commands, new_listeners = _process_attributes(base)
278+
commands.update(new_commands)
279+
listeners.update(new_listeners)
207280

208281
new_cls.__cog_commands__ = list(commands.values())
209282

210-
listeners_as_list = []
211-
for listener in listeners.values():
212-
for listener_name in listener.__cog_listener_names__:
213-
# I use __name__ instead of just storing the value, so I can inject
214-
# the self attribute when the time comes to add them to the bot
215-
listeners_as_list.append((listener_name, listener.__name__))
216-
217-
new_cls.__cog_listeners__ = listeners_as_list
283+
new_cls.__cog_listeners__ = [
284+
(listener_name, listener.__name__)
285+
for listener in listeners.values()
286+
for listener_name in listener.__cog_listener_names__
287+
]
218288

219289
cmd_attrs = new_cls.__cog_settings__
220290

221291
# Either update the command with the cog provided defaults or copy it.
222292
# r.e type ignore, type-checker complains about overriding a ClassVar
223-
new_cls.__cog_commands__ = tuple(c._update_copy(cmd_attrs) if not hasattr(c, "add_to") else c for c in new_cls.__cog_commands__) # type: ignore
224-
225-
name_filter = lambda c: (
226-
"app"
227-
if isinstance(c, ApplicationCommand)
228-
else ("bridge" if not hasattr(c, "add_to") else "ext")
229-
)
293+
new_cls.__cog_commands__ = list(tuple(c._update_copy(cmd_attrs) if not _is_bridge_command(c) else c for c in new_cls.__cog_commands__)) # type: ignore
230294

231295
lookup = {
232-
f"{name_filter(cmd)}_{cmd.qualified_name}": cmd
296+
f"{_name_filter(cmd)}_{cmd.qualified_name}": cmd
233297
for cmd in new_cls.__cog_commands__
234298
}
235299

236300
# Update the Command instances dynamically as well
237301
for command in new_cls.__cog_commands__:
238-
if (
239-
isinstance(command, ApplicationCommand)
240-
and not command.guild_ids
241-
and new_cls.__cog_guild_ids__
242-
):
243-
command.guild_ids = new_cls.__cog_guild_ids__
244-
245-
if not isinstance(command, SlashCommandGroup) and not hasattr(
246-
command, "add_to"
247-
):
248-
# ignore bridge commands
249-
cmd = getattr(new_cls, command.callback.__name__, None)
250-
if hasattr(cmd, "add_to"):
251-
setattr(
252-
cmd,
253-
f"{name_filter(command).replace('app', 'slash')}_variant",
254-
command,
255-
)
256-
else:
257-
setattr(new_cls, command.callback.__name__, command)
258-
259-
parent = command.parent
260-
if parent is not None:
261-
# Get the latest parent reference
262-
parent = lookup[f"{name_filter(command)}_{parent.qualified_name}"] # type: ignore
263-
264-
# Update our parent's reference to our self
265-
parent.remove_command(command.name) # type: ignore
266-
parent.add_command(command) # type: ignore
302+
_update_command(command, new_cls.__cog_guild_ids__, lookup, new_cls)
267303

268304
return new_cls
269305

@@ -537,7 +573,7 @@ def _inject(self: CogT, bot) -> CogT:
537573
# we've added so far for some form of atomic loading.
538574

539575
for index, command in enumerate(self.__cog_commands__):
540-
if hasattr(command, "add_to"):
576+
if _is_bridge_command(command):
541577
bot.bridge_commands.append(command)
542578
continue
543579

@@ -582,7 +618,7 @@ def _eject(self, bot) -> None:
582618

583619
try:
584620
for command in self.__cog_commands__:
585-
if hasattr(command, "add_to"):
621+
if _is_bridge_command(command):
586622
bot.bridge_commands.remove(command)
587623
continue
588624
elif isinstance(command, ApplicationCommand):

discord/commands/core.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -726,6 +726,8 @@ class SlashCommand(ApplicationCommand):
726726

727727
type = 1
728728

729+
parent: SlashCommandGroup | None
730+
729731
def __new__(cls, *args, **kwargs) -> SlashCommand:
730732
self = super().__new__(cls)
731733

discord/ext/bridge/core.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,8 @@ class BridgeCommand:
171171
The prefix-based version of this bridge command.
172172
"""
173173

174+
__bridge__: bool = True
175+
174176
__special_attrs__ = ["slash_variant", "ext_variant", "parent"]
175177

176178
def __init__(self, callback, **kwargs):

0 commit comments

Comments
 (0)