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.
- disagreement/__init__.py +1 -1
- disagreement/ext/__init__.py +0 -0
- disagreement/ext/app_commands/__init__.py +46 -0
- disagreement/ext/app_commands/commands.py +513 -0
- disagreement/ext/app_commands/context.py +556 -0
- disagreement/ext/app_commands/converters.py +478 -0
- disagreement/ext/app_commands/decorators.py +569 -0
- disagreement/ext/app_commands/handler.py +627 -0
- disagreement/ext/commands/__init__.py +49 -0
- disagreement/ext/commands/cog.py +155 -0
- disagreement/ext/commands/converters.py +175 -0
- disagreement/ext/commands/core.py +490 -0
- disagreement/ext/commands/decorators.py +150 -0
- disagreement/ext/commands/errors.py +76 -0
- disagreement/ext/commands/help.py +37 -0
- disagreement/ext/commands/view.py +103 -0
- disagreement/ext/loader.py +43 -0
- disagreement/ext/tasks.py +89 -0
- disagreement/gateway.py +11 -8
- {disagreement-0.0.1.dist-info → disagreement-0.0.2.dist-info}/METADATA +39 -32
- {disagreement-0.0.1.dist-info → disagreement-0.0.2.dist-info}/RECORD +24 -7
- {disagreement-0.0.1.dist-info → disagreement-0.0.2.dist-info}/WHEEL +0 -0
- {disagreement-0.0.1.dist-info → disagreement-0.0.2.dist-info}/licenses/LICENSE +0 -0
- {disagreement-0.0.1.dist-info → disagreement-0.0.2.dist-info}/top_level.txt +0 -0
@@ -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
|