3131import pathlib
3232import sys
3333import 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
3646import discord .utils
3747
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+
62188class 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 ):
0 commit comments