disagreement 0.0.1__py3-none-any.whl → 0.0.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,155 @@
1
+ # disagreement/ext/commands/cog.py
2
+
3
+ import inspect
4
+ from typing import TYPE_CHECKING, List, Tuple, Callable, Awaitable, Any, Dict, Union
5
+
6
+ if TYPE_CHECKING:
7
+ from disagreement.client import Client
8
+ from .core import Command
9
+ from disagreement.ext.app_commands.commands import (
10
+ AppCommand,
11
+ AppCommandGroup,
12
+ ) # Added
13
+ else: # pragma: no cover - runtime imports for isinstance checks
14
+ from disagreement.ext.app_commands.commands import AppCommand, AppCommandGroup
15
+
16
+ # EventDispatcher might be needed if cogs register listeners directly
17
+ # from disagreement.event_dispatcher import EventDispatcher
18
+
19
+
20
+ class Cog:
21
+ """
22
+ The base class for cogs, which are collections of commands and listeners.
23
+ """
24
+
25
+ def __init__(self, client: "Client"):
26
+ self._client: "Client" = client
27
+ self._cog_name: str = self.__class__.__name__
28
+ self._commands: Dict[str, "Command"] = {}
29
+ self._listeners: List[Tuple[str, Callable[..., Awaitable[None]]]] = []
30
+ self._app_commands_and_groups: List[Union["AppCommand", "AppCommandGroup"]] = (
31
+ []
32
+ ) # Added
33
+
34
+ # Discover commands and listeners defined in this cog instance
35
+ self._inject()
36
+
37
+ @property
38
+ def client(self) -> "Client":
39
+ return self._client
40
+
41
+ @property
42
+ def cog_name(self) -> str:
43
+ return self._cog_name
44
+
45
+ def _inject(self) -> None:
46
+ """
47
+ Called to discover and prepare commands and listeners within this cog.
48
+ This is typically called by the CommandHandler when adding the cog.
49
+ """
50
+ # Clear any previously injected state (e.g., if re-injecting)
51
+ self._commands.clear()
52
+ self._listeners.clear()
53
+ self._app_commands_and_groups.clear() # Added
54
+
55
+ for member_name, member in inspect.getmembers(self):
56
+ if hasattr(member, "__command_object__"):
57
+ # This is a prefix or hybrid command object
58
+ cmd: "Command" = getattr(member, "__command_object__")
59
+ cmd.cog = self # Assign the cog instance to the command
60
+ if cmd.name in self._commands:
61
+ # This should ideally be caught earlier or handled by CommandHandler
62
+ print(
63
+ f"Warning: Duplicate command name '{cmd.name}' in cog '{self.cog_name}'. Overwriting."
64
+ )
65
+ self._commands[cmd.name.lower()] = cmd
66
+ # Also register aliases
67
+ for alias in cmd.aliases:
68
+ self._commands[alias.lower()] = cmd
69
+
70
+ # If this command is also an application command (HybridCommand)
71
+ if isinstance(cmd, (AppCommand, AppCommandGroup)):
72
+ self._app_commands_and_groups.append(cmd)
73
+
74
+ elif hasattr(member, "__app_command_object__"): # Added for app commands
75
+ app_cmd_obj = getattr(member, "__app_command_object__")
76
+ if isinstance(app_cmd_obj, (AppCommand, AppCommandGroup)):
77
+ if isinstance(app_cmd_obj, AppCommand):
78
+ app_cmd_obj.cog = self # Associate cog
79
+ # For AppCommandGroup, its commands will have cog set individually if they are AppCommands
80
+ self._app_commands_and_groups.append(app_cmd_obj)
81
+ else:
82
+ print(
83
+ f"Warning: Member '{member_name}' in cog '{self.cog_name}' has '__app_command_object__' but it's not an AppCommand or AppCommandGroup."
84
+ )
85
+
86
+ elif isinstance(member, (AppCommand, AppCommandGroup)):
87
+ if isinstance(member, AppCommand):
88
+ member.cog = self
89
+ self._app_commands_and_groups.append(member)
90
+
91
+ elif hasattr(member, "__listener_name__"):
92
+ # This is a method decorated with @commands.Cog.listener or @commands.listener
93
+ if not inspect.iscoroutinefunction(member):
94
+ # Decorator should have caught this, but double check
95
+ print(
96
+ f"Warning: Listener '{member_name}' in cog '{self.cog_name}' is not a coroutine. Skipping."
97
+ )
98
+ continue
99
+
100
+ event_name: str = getattr(member, "__listener_name__")
101
+ # The callback needs to be the bound method from this cog instance
102
+ self._listeners.append((event_name, member))
103
+
104
+ def _eject(self) -> None:
105
+ """
106
+ Called when the cog is being removed.
107
+ The CommandHandler will handle unregistering commands/listeners.
108
+ This method is for any cog-specific cleanup before that.
109
+ """
110
+ # For now, just clear local collections. Actual unregistration is external.
111
+ self._commands.clear()
112
+ self._listeners.clear()
113
+ self._app_commands_and_groups.clear() # Added
114
+
115
+ def get_commands(self) -> List["Command"]:
116
+ """Returns a list of commands in this cog."""
117
+ # Avoid duplicates if aliases point to the same command object
118
+ return list(dict.fromkeys(self._commands.values()))
119
+
120
+ def get_listeners(self) -> List[Tuple[str, Callable[..., Awaitable[None]]]]:
121
+ """Returns a list of (event_name, callback) tuples for listeners in this cog."""
122
+ return self._listeners
123
+
124
+ def get_app_commands_and_groups(
125
+ self,
126
+ ) -> List[Union["AppCommand", "AppCommandGroup"]]:
127
+ """Returns a list of application commands and groups in this cog."""
128
+ return self._app_commands_and_groups
129
+
130
+ async def cog_load(self) -> None:
131
+ """
132
+ A special method that is called when the cog is loaded.
133
+ This is a good place for any asynchronous setup.
134
+ Subclasses should override this if they need async setup.
135
+ """
136
+ pass
137
+
138
+ async def cog_unload(self) -> None:
139
+ """
140
+ A special method that is called when the cog is unloaded.
141
+ This is a good place for any asynchronous cleanup.
142
+ Subclasses should override this if they need async cleanup.
143
+ """
144
+ pass
145
+
146
+ # Example of how a listener might be defined within a Cog using the decorator
147
+ # from .decorators import listener # Would be imported at module level
148
+ #
149
+ # @listener(name="ON_MESSAGE_CREATE_CUSTOM") # Explicit name
150
+ # async def on_my_event(self, message: 'Message'):
151
+ # print(f"Cog '{self.cog_name}' received event with message: {message.content}")
152
+ #
153
+ # @listener() # Name derived from method: on_ready
154
+ # async def on_ready(self):
155
+ # print(f"Cog '{self.cog_name}' is ready.")
@@ -0,0 +1,175 @@
1
+ # disagreement/ext/commands/converters.py
2
+
3
+ from typing import TYPE_CHECKING, Any, Awaitable, Callable, TypeVar, Generic
4
+ from abc import ABC, abstractmethod
5
+ import re
6
+
7
+ from .errors import BadArgument
8
+ from disagreement.models import Member, Guild, Role
9
+
10
+ if TYPE_CHECKING:
11
+ from .core import CommandContext
12
+
13
+ T = TypeVar("T")
14
+
15
+
16
+ class Converter(ABC, Generic[T]):
17
+ """
18
+ Base class for custom command argument converters.
19
+ Subclasses must implement the `convert` method.
20
+ """
21
+
22
+ async def convert(self, ctx: "CommandContext", argument: str) -> T:
23
+ """
24
+ Converts the argument to the desired type.
25
+
26
+ Args:
27
+ ctx: The invocation context.
28
+ argument: The string argument to convert.
29
+
30
+ Returns:
31
+ The converted argument.
32
+
33
+ Raises:
34
+ BadArgument: If the conversion fails.
35
+ """
36
+ raise NotImplementedError("Converter subclass must implement convert method.")
37
+
38
+
39
+ # --- Built-in Type Converters ---
40
+
41
+
42
+ class IntConverter(Converter[int]):
43
+ async def convert(self, ctx: "CommandContext", argument: str) -> int:
44
+ try:
45
+ return int(argument)
46
+ except ValueError:
47
+ raise BadArgument(f"'{argument}' is not a valid integer.")
48
+
49
+
50
+ class FloatConverter(Converter[float]):
51
+ async def convert(self, ctx: "CommandContext", argument: str) -> float:
52
+ try:
53
+ return float(argument)
54
+ except ValueError:
55
+ raise BadArgument(f"'{argument}' is not a valid number.")
56
+
57
+
58
+ class BoolConverter(Converter[bool]):
59
+ async def convert(self, ctx: "CommandContext", argument: str) -> bool:
60
+ lowered = argument.lower()
61
+ if lowered in ("yes", "y", "true", "t", "1", "on", "enable", "enabled"):
62
+ return True
63
+ elif lowered in ("no", "n", "false", "f", "0", "off", "disable", "disabled"):
64
+ return False
65
+ raise BadArgument(f"'{argument}' is not a valid boolean-like value.")
66
+
67
+
68
+ class StringConverter(Converter[str]):
69
+ async def convert(self, ctx: "CommandContext", argument: str) -> str:
70
+ # For basic string, no conversion is needed, but this provides a consistent interface
71
+ return argument
72
+
73
+
74
+ # --- Discord Model Converters ---
75
+
76
+
77
+ class MemberConverter(Converter["Member"]):
78
+ async def convert(self, ctx: "CommandContext", argument: str) -> "Member":
79
+ if not ctx.message.guild_id:
80
+ raise BadArgument("Member converter requires guild context.")
81
+
82
+ match = re.match(r"<@!?(\d+)>$", argument)
83
+ member_id = match.group(1) if match else argument
84
+
85
+ guild = ctx.bot.get_guild(ctx.message.guild_id)
86
+ if guild:
87
+ member = guild.get_member(member_id)
88
+ if member:
89
+ return member
90
+
91
+ member = await ctx.bot.fetch_member(ctx.message.guild_id, member_id)
92
+ if member:
93
+ return member
94
+ raise BadArgument(f"Member '{argument}' not found.")
95
+
96
+
97
+ class RoleConverter(Converter["Role"]):
98
+ async def convert(self, ctx: "CommandContext", argument: str) -> "Role":
99
+ if not ctx.message.guild_id:
100
+ raise BadArgument("Role converter requires guild context.")
101
+
102
+ match = re.match(r"<@&(?P<id>\d+)>$", argument)
103
+ role_id = match.group("id") if match else argument
104
+
105
+ guild = ctx.bot.get_guild(ctx.message.guild_id)
106
+ if guild:
107
+ role = guild.get_role(role_id)
108
+ if role:
109
+ return role
110
+
111
+ role = await ctx.bot.fetch_role(ctx.message.guild_id, role_id)
112
+ if role:
113
+ return role
114
+ raise BadArgument(f"Role '{argument}' not found.")
115
+
116
+
117
+ class GuildConverter(Converter["Guild"]):
118
+ async def convert(self, ctx: "CommandContext", argument: str) -> "Guild":
119
+ guild_id = argument.strip("<>") # allow <id> style
120
+
121
+ guild = ctx.bot.get_guild(guild_id)
122
+ if guild:
123
+ return guild
124
+
125
+ guild = await ctx.bot.fetch_guild(guild_id)
126
+ if guild:
127
+ return guild
128
+ raise BadArgument(f"Guild '{argument}' not found.")
129
+
130
+
131
+ # Default converters mapping
132
+ DEFAULT_CONVERTERS: dict[type, Converter[Any]] = {
133
+ int: IntConverter(),
134
+ float: FloatConverter(),
135
+ bool: BoolConverter(),
136
+ str: StringConverter(),
137
+ Member: MemberConverter(),
138
+ Guild: GuildConverter(),
139
+ Role: RoleConverter(),
140
+ # User: UserConverter(), # Add when User model and converter are ready
141
+ }
142
+
143
+
144
+ async def run_converters(ctx: "CommandContext", annotation: Any, argument: str) -> Any:
145
+ """
146
+ Attempts to run a converter for the given annotation and argument.
147
+ """
148
+ converter = DEFAULT_CONVERTERS.get(annotation)
149
+ if converter:
150
+ return await converter.convert(ctx, argument)
151
+
152
+ # If no direct converter, check if annotation itself is a Converter subclass
153
+ if inspect.isclass(annotation) and issubclass(annotation, Converter):
154
+ try:
155
+ instance = annotation() # type: ignore
156
+ return await instance.convert(ctx, argument)
157
+ except Exception as e: # Catch instantiation errors or other issues
158
+ raise BadArgument(
159
+ f"Failed to use custom converter {annotation.__name__}: {e}"
160
+ )
161
+
162
+ # If it's a custom class that's not a Converter, we can't handle it by default
163
+ # Or if it's a complex type hint like Union, Optional, Literal etc.
164
+ # This part needs more advanced logic for those.
165
+
166
+ # For now, if no specific converter, and it's not 'str', raise error or return as str?
167
+ # Let's be strict for now if an annotation is given but no converter found.
168
+ if annotation is not str and annotation is not inspect.Parameter.empty:
169
+ raise BadArgument(f"No converter found for type annotation '{annotation}'.")
170
+
171
+ return argument # Default to string if no annotation or annotation is str
172
+
173
+
174
+ # Need to import inspect for the run_converters function
175
+ import inspect