disagreement 0.1.0rc1__py3-none-any.whl → 0.1.0rc3__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 +9 -5
- disagreement/client.py +41 -6
- disagreement/color.py +28 -0
- disagreement/enums.py +5 -0
- disagreement/ext/app_commands/__init__.py +2 -0
- disagreement/ext/app_commands/commands.py +13 -99
- disagreement/ext/app_commands/decorators.py +1 -1
- disagreement/ext/app_commands/handler.py +25 -12
- disagreement/ext/app_commands/hybrid.py +61 -0
- disagreement/ext/commands/cog.py +15 -6
- disagreement/ext/commands/core.py +28 -12
- disagreement/ext/tasks.py +46 -0
- disagreement/gateway.py +102 -63
- disagreement/http.py +134 -17
- disagreement/interactions.py +17 -14
- disagreement/models.py +124 -9
- disagreement/py.typed +0 -0
- disagreement/ui/modal.py +1 -1
- disagreement/utils.py +63 -0
- {disagreement-0.1.0rc1.dist-info → disagreement-0.1.0rc3.dist-info}/METADATA +6 -5
- {disagreement-0.1.0rc1.dist-info → disagreement-0.1.0rc3.dist-info}/RECORD +24 -22
- {disagreement-0.1.0rc1.dist-info → disagreement-0.1.0rc3.dist-info}/WHEEL +0 -0
- {disagreement-0.1.0rc1.dist-info → disagreement-0.1.0rc3.dist-info}/licenses/LICENSE +0 -0
- {disagreement-0.1.0rc1.dist-info → disagreement-0.1.0rc3.dist-info}/top_level.txt +0 -0
disagreement/__init__.py
CHANGED
@@ -14,7 +14,7 @@ __title__ = "disagreement"
|
|
14
14
|
__author__ = "Slipstream"
|
15
15
|
__license__ = "BSD 3-Clause License"
|
16
16
|
__copyright__ = "Copyright 2025 Slipstream"
|
17
|
-
__version__ = "0.1.
|
17
|
+
__version__ = "0.1.0rc3"
|
18
18
|
|
19
19
|
from .client import Client, AutoShardedClient
|
20
20
|
from .models import Message, User, Reaction
|
@@ -30,12 +30,16 @@ from .errors import (
|
|
30
30
|
NotFound,
|
31
31
|
)
|
32
32
|
from .color import Color
|
33
|
-
from .utils import utcnow
|
33
|
+
from .utils import utcnow, message_pager
|
34
34
|
from .enums import GatewayIntent, GatewayOpcode # Export enums
|
35
35
|
from .error_handler import setup_global_error_handler
|
36
36
|
from .hybrid_context import HybridContext
|
37
37
|
from .ext import tasks
|
38
|
+
from .logging_config import setup_logging
|
38
39
|
|
39
|
-
|
40
|
-
|
41
|
-
|
40
|
+
import logging
|
41
|
+
|
42
|
+
|
43
|
+
# Configure a default logger if none has been configured yet
|
44
|
+
if not logging.getLogger().hasHandlers():
|
45
|
+
setup_logging(logging.INFO)
|
disagreement/client.py
CHANGED
@@ -16,6 +16,7 @@ from typing import (
|
|
16
16
|
List,
|
17
17
|
Dict,
|
18
18
|
)
|
19
|
+
from types import ModuleType
|
19
20
|
|
20
21
|
from .http import HTTPClient
|
21
22
|
from .gateway import GatewayClient
|
@@ -28,6 +29,7 @@ from .ext.commands.core import CommandHandler
|
|
28
29
|
from .ext.commands.cog import Cog
|
29
30
|
from .ext.app_commands.handler import AppCommandHandler
|
30
31
|
from .ext.app_commands.context import AppCommandContext
|
32
|
+
from .ext import loader as ext_loader
|
31
33
|
from .interactions import Interaction, Snowflake
|
32
34
|
from .error_handler import setup_global_error_handler
|
33
35
|
from .voice_client import VoiceClient
|
@@ -121,14 +123,10 @@ class Client:
|
|
121
123
|
|
122
124
|
self._closed: bool = False
|
123
125
|
self._ready_event: asyncio.Event = asyncio.Event()
|
124
|
-
self.application_id: Optional[Snowflake] = None # For Application Commands
|
125
126
|
self.user: Optional["User"] = (
|
126
127
|
None # The bot's own user object, populated on READY
|
127
128
|
)
|
128
129
|
|
129
|
-
# Initialize AppCommandHandler
|
130
|
-
self.app_command_handler: AppCommandHandler = AppCommandHandler(client=self)
|
131
|
-
|
132
130
|
# Internal Caches
|
133
131
|
self._guilds: Dict[Snowflake, "Guild"] = {}
|
134
132
|
self._channels: Dict[Snowflake, "Channel"] = (
|
@@ -638,6 +636,23 @@ class Client:
|
|
638
636
|
# import traceback
|
639
637
|
# traceback.print_exception(type(error.original), error.original, error.original.__traceback__)
|
640
638
|
|
639
|
+
# --- Extension Management Methods ---
|
640
|
+
|
641
|
+
def load_extension(self, name: str) -> ModuleType:
|
642
|
+
"""Load an extension by name using :mod:`disagreement.ext.loader`."""
|
643
|
+
|
644
|
+
return ext_loader.load_extension(name)
|
645
|
+
|
646
|
+
def unload_extension(self, name: str) -> None:
|
647
|
+
"""Unload a previously loaded extension."""
|
648
|
+
|
649
|
+
ext_loader.unload_extension(name)
|
650
|
+
|
651
|
+
def reload_extension(self, name: str) -> ModuleType:
|
652
|
+
"""Reload an extension by name."""
|
653
|
+
|
654
|
+
return ext_loader.reload_extension(name)
|
655
|
+
|
641
656
|
# --- Model Parsing and Fetching ---
|
642
657
|
|
643
658
|
def parse_user(self, data: Dict[str, Any]) -> "User":
|
@@ -920,12 +935,12 @@ class Client:
|
|
920
935
|
await view._start(self)
|
921
936
|
components_payload = view.to_components_payload()
|
922
937
|
elif components:
|
923
|
-
from .models import
|
938
|
+
from .models import Component as ComponentModel
|
924
939
|
|
925
940
|
components_payload = [
|
926
941
|
comp.to_dict()
|
927
942
|
for comp in components
|
928
|
-
if isinstance(comp,
|
943
|
+
if isinstance(comp, ComponentModel)
|
929
944
|
]
|
930
945
|
|
931
946
|
message_data = await self._http.send_message(
|
@@ -1016,6 +1031,26 @@ class Client:
|
|
1016
1031
|
self._voice_clients[guild_id] = voice
|
1017
1032
|
return voice
|
1018
1033
|
|
1034
|
+
async def add_reaction(self, channel_id: str, message_id: str, emoji: str) -> None:
|
1035
|
+
"""|coro| Add a reaction to a message."""
|
1036
|
+
|
1037
|
+
await self.create_reaction(channel_id, message_id, emoji)
|
1038
|
+
|
1039
|
+
async def remove_reaction(
|
1040
|
+
self, channel_id: str, message_id: str, emoji: str
|
1041
|
+
) -> None:
|
1042
|
+
"""|coro| Remove the bot's reaction from a message."""
|
1043
|
+
|
1044
|
+
await self.delete_reaction(channel_id, message_id, emoji)
|
1045
|
+
|
1046
|
+
async def clear_reactions(self, channel_id: str, message_id: str) -> None:
|
1047
|
+
"""|coro| Remove all reactions from a message."""
|
1048
|
+
|
1049
|
+
if self._closed:
|
1050
|
+
raise DisagreementException("Client is closed.")
|
1051
|
+
|
1052
|
+
await self._http.clear_reactions(channel_id, message_id)
|
1053
|
+
|
1019
1054
|
async def create_reaction(
|
1020
1055
|
self, channel_id: str, message_id: str, emoji: str
|
1021
1056
|
) -> None:
|
disagreement/color.py
CHANGED
@@ -48,3 +48,31 @@ class Color:
|
|
48
48
|
|
49
49
|
def to_rgb(self) -> tuple[int, int, int]:
|
50
50
|
return ((self.value >> 16) & 0xFF, (self.value >> 8) & 0xFF, self.value & 0xFF)
|
51
|
+
|
52
|
+
@classmethod
|
53
|
+
def parse(cls, value: "Color | int | str | tuple[int, int, int] | None") -> "Color | None":
|
54
|
+
"""Convert ``value`` to a :class:`Color` instance.
|
55
|
+
|
56
|
+
Parameters
|
57
|
+
----------
|
58
|
+
value:
|
59
|
+
The value to convert. May be ``None``, an existing ``Color``, an
|
60
|
+
integer in the ``0xRRGGBB`` format, or a hex string like ``"#RRGGBB"``.
|
61
|
+
|
62
|
+
Returns
|
63
|
+
-------
|
64
|
+
Optional[Color]
|
65
|
+
A ``Color`` object if ``value`` is not ``None``.
|
66
|
+
"""
|
67
|
+
|
68
|
+
if value is None:
|
69
|
+
return None
|
70
|
+
if isinstance(value, cls):
|
71
|
+
return value
|
72
|
+
if isinstance(value, int):
|
73
|
+
return cls(value)
|
74
|
+
if isinstance(value, str):
|
75
|
+
return cls.from_hex(value)
|
76
|
+
if isinstance(value, tuple) and len(value) == 3:
|
77
|
+
return cls.from_rgb(*value)
|
78
|
+
raise TypeError("Color value must be Color, int, str, tuple, or None")
|
disagreement/enums.py
CHANGED
@@ -49,6 +49,11 @@ class GatewayIntent(IntEnum):
|
|
49
49
|
AUTO_MODERATION_CONFIGURATION = 1 << 20
|
50
50
|
AUTO_MODERATION_EXECUTION = 1 << 21
|
51
51
|
|
52
|
+
@classmethod
|
53
|
+
def none(cls) -> int:
|
54
|
+
"""Return a bitmask representing no intents."""
|
55
|
+
return 0
|
56
|
+
|
52
57
|
@classmethod
|
53
58
|
def default(cls) -> int:
|
54
59
|
"""Returns default intents (excluding privileged ones like members, presences, message content)."""
|
@@ -1,58 +1,25 @@
|
|
1
1
|
# disagreement/ext/app_commands/commands.py
|
2
2
|
|
3
3
|
import inspect
|
4
|
-
from typing import Callable,
|
4
|
+
from typing import Any, Callable, Dict, List, Optional, Union, TYPE_CHECKING
|
5
5
|
|
6
|
+
from disagreement.enums import (
|
7
|
+
ApplicationCommandType,
|
8
|
+
ApplicationCommandOptionType,
|
9
|
+
IntegrationType,
|
10
|
+
InteractionContextType,
|
11
|
+
)
|
12
|
+
from disagreement.interactions import ApplicationCommandOption, Snowflake
|
6
13
|
|
7
14
|
if TYPE_CHECKING:
|
8
|
-
from disagreement.ext.commands.core import
|
9
|
-
|
10
|
-
|
11
|
-
from disagreement.interactions import ApplicationCommandOption, Snowflake
|
12
|
-
from disagreement.enums import (
|
13
|
-
ApplicationCommandType,
|
14
|
-
IntegrationType,
|
15
|
-
InteractionContextType,
|
16
|
-
ApplicationCommandOptionType, # Added
|
17
|
-
)
|
18
|
-
from disagreement.ext.commands.cog import Cog # Corrected import path
|
19
|
-
|
20
|
-
# Placeholder for Cog if not using the existing one or if it needs adaptation
|
15
|
+
from disagreement.ext.commands.core import Command as PrefixCommand
|
16
|
+
from disagreement.ext.commands.cog import Cog
|
17
|
+
|
21
18
|
if not TYPE_CHECKING:
|
22
|
-
# This dynamic Cog = Any might not be ideal if Cog is used in runtime type checks.
|
23
|
-
# However, for type hinting purposes when TYPE_CHECKING is false, it avoids import.
|
24
|
-
# If Cog is needed at runtime by this module (it is, for AppCommand.cog type hint),
|
25
|
-
# it should be imported directly.
|
26
|
-
# For now, the TYPE_CHECKING block handles the proper import for static analysis.
|
27
|
-
# Let's ensure Cog is available at runtime if AppCommand.cog is accessed.
|
28
|
-
# A simple way is to import it outside TYPE_CHECKING too, if it doesn't cause circularity.
|
29
|
-
# Given its usage, a forward reference string 'Cog' might be better in AppCommand.cog type hint.
|
30
|
-
# Let's try importing it directly for runtime, assuming no circularity with this specific module.
|
31
|
-
try:
|
32
|
-
from disagreement.ext.commands.cog import Cog
|
33
|
-
except ImportError:
|
34
|
-
Cog = Any # Fallback if direct import fails (e.g. during partial builds/tests)
|
35
|
-
# Import PrefixCommand at runtime for HybridCommand
|
36
19
|
try:
|
37
20
|
from disagreement.ext.commands.core import Command as PrefixCommand
|
38
|
-
except
|
39
|
-
PrefixCommand = Any
|
40
|
-
# Import enums used at runtime
|
41
|
-
try:
|
42
|
-
from disagreement.enums import (
|
43
|
-
ApplicationCommandType,
|
44
|
-
IntegrationType,
|
45
|
-
InteractionContextType,
|
46
|
-
ApplicationCommandOptionType,
|
47
|
-
)
|
48
|
-
from disagreement.interactions import ApplicationCommandOption, Snowflake
|
49
|
-
except Exception: # pragma: no cover
|
50
|
-
ApplicationCommandType = ApplicationCommandOptionType = IntegrationType = (
|
51
|
-
InteractionContextType
|
52
|
-
) = Any # type: ignore
|
53
|
-
ApplicationCommandOption = Snowflake = Any # type: ignore
|
54
|
-
else: # When TYPE_CHECKING is true, Cog and PrefixCommand are already imported above.
|
55
|
-
pass
|
21
|
+
except ImportError:
|
22
|
+
PrefixCommand = Any
|
56
23
|
|
57
24
|
|
58
25
|
class AppCommand:
|
@@ -235,59 +202,6 @@ class MessageCommand(AppCommand):
|
|
235
202
|
super().__init__(callback, type=ApplicationCommandType.MESSAGE, **kwargs)
|
236
203
|
|
237
204
|
|
238
|
-
class HybridCommand(SlashCommand, PrefixCommand): # Inherit from both
|
239
|
-
"""
|
240
|
-
Represents a command that can be invoked as both a slash command
|
241
|
-
and a traditional prefix-based command.
|
242
|
-
"""
|
243
|
-
|
244
|
-
def __init__(self, callback: Callable[..., Any], **kwargs: Any):
|
245
|
-
# Initialize SlashCommand part (which calls AppCommand.__init__)
|
246
|
-
# We need to ensure 'type' is correctly passed for AppCommand
|
247
|
-
# kwargs for SlashCommand: name, description, guild_ids, default_member_permissions, nsfw, parent, cog, etc.
|
248
|
-
# kwargs for PrefixCommand: name, aliases, brief, description, cog
|
249
|
-
|
250
|
-
# Pop prefix-specific args before passing to SlashCommand constructor
|
251
|
-
prefix_aliases = kwargs.pop("aliases", [])
|
252
|
-
prefix_brief = kwargs.pop("brief", None)
|
253
|
-
# Description is used by both, AppCommand's constructor will handle it.
|
254
|
-
# Name is used by both. Cog is used by both.
|
255
|
-
|
256
|
-
# Call SlashCommand's __init__
|
257
|
-
# This will set up name, description, callback, type=CHAT_INPUT, options, etc.
|
258
|
-
super().__init__(callback, **kwargs) # This is SlashCommand.__init__
|
259
|
-
|
260
|
-
# Now, explicitly initialize the PrefixCommand parts that SlashCommand didn't cover
|
261
|
-
# or that need specific values for the prefix version.
|
262
|
-
# PrefixCommand.__init__(self, callback, name=self.name, aliases=prefix_aliases, brief=prefix_brief, description=self.description, cog=self.cog)
|
263
|
-
# However, PrefixCommand.__init__ also sets self.params, which AppCommand already did.
|
264
|
-
# We need to be careful not to re-initialize things unnecessarily or incorrectly.
|
265
|
-
# Let's manually set the distinct attributes for the PrefixCommand aspect.
|
266
|
-
|
267
|
-
# Attributes from PrefixCommand:
|
268
|
-
# self.callback is already set by AppCommand
|
269
|
-
# self.name is already set by AppCommand
|
270
|
-
self.aliases: List[str] = (
|
271
|
-
prefix_aliases # This was specific to HybridCommand before, now aligns with PrefixCommand
|
272
|
-
)
|
273
|
-
self.brief: Optional[str] = prefix_brief
|
274
|
-
# self.description is already set by AppCommand (SlashCommand ensures it exists)
|
275
|
-
# self.cog is already set by AppCommand
|
276
|
-
# self.params is already set by AppCommand
|
277
|
-
|
278
|
-
# Ensure the MRO is handled correctly. Python's MRO (C3 linearization)
|
279
|
-
# should call SlashCommand's __init__ then AppCommand's __init__.
|
280
|
-
# PrefixCommand.__init__ won't be called automatically unless we explicitly call it.
|
281
|
-
# By setting attributes directly, we avoid potential issues with multiple __init__ calls
|
282
|
-
# if their logic overlaps too much (e.g., both trying to set self.params).
|
283
|
-
|
284
|
-
# We might need to override invoke if the context or argument passing differs significantly
|
285
|
-
# between app command invocation and prefix command invocation.
|
286
|
-
# For now, SlashCommand.invoke and PrefixCommand.invoke are separate.
|
287
|
-
# The correct one will be called depending on how the command is dispatched.
|
288
|
-
# The AppCommandHandler will use AppCommand.invoke (via SlashCommand).
|
289
|
-
# The prefix CommandHandler will use PrefixCommand.invoke.
|
290
|
-
# This seems acceptable.
|
291
205
|
|
292
206
|
|
293
207
|
class AppCommandGroup:
|
@@ -1,6 +1,7 @@
|
|
1
1
|
# disagreement/ext/app_commands/handler.py
|
2
2
|
|
3
3
|
import inspect
|
4
|
+
import logging
|
4
5
|
from typing import (
|
5
6
|
TYPE_CHECKING,
|
6
7
|
Dict,
|
@@ -64,6 +65,9 @@ if not TYPE_CHECKING:
|
|
64
65
|
Message = Any
|
65
66
|
|
66
67
|
|
68
|
+
logger = logging.getLogger(__name__)
|
69
|
+
|
70
|
+
|
67
71
|
class AppCommandHandler:
|
68
72
|
"""
|
69
73
|
Manages application command registration, parsing, and dispatching.
|
@@ -544,7 +548,7 @@ class AppCommandHandler:
|
|
544
548
|
await command.invoke(ctx, *parsed_args, **parsed_kwargs)
|
545
549
|
|
546
550
|
except Exception as e:
|
547
|
-
|
551
|
+
logger.error("Error invoking app command '%s': %s", command.name, e)
|
548
552
|
await self.dispatch_app_command_error(ctx, e)
|
549
553
|
# else:
|
550
554
|
# # Default error reply if no handler on client
|
@@ -594,34 +598,43 @@ class AppCommandHandler:
|
|
594
598
|
payload = cmd_or_group.to_dict()
|
595
599
|
commands_to_sync.append(payload)
|
596
600
|
except AttributeError:
|
597
|
-
|
598
|
-
|
601
|
+
logger.warning(
|
602
|
+
"Command or group '%s' does not have a to_dict() method. Skipping.",
|
603
|
+
cmd_or_group.name,
|
599
604
|
)
|
600
605
|
except Exception as e:
|
601
|
-
|
602
|
-
|
606
|
+
logger.error(
|
607
|
+
"Error converting command/group '%s' to dict: %s. Skipping.",
|
608
|
+
cmd_or_group.name,
|
609
|
+
e,
|
603
610
|
)
|
604
611
|
|
605
612
|
if not commands_to_sync:
|
606
|
-
|
607
|
-
|
613
|
+
logger.info(
|
614
|
+
"No commands to sync for %s scope.",
|
615
|
+
f"guild {guild_id}" if guild_id else "global",
|
608
616
|
)
|
609
617
|
return
|
610
618
|
|
611
619
|
try:
|
612
620
|
if guild_id:
|
613
|
-
|
614
|
-
|
621
|
+
logger.info(
|
622
|
+
"Syncing %s commands for guild %s...",
|
623
|
+
len(commands_to_sync),
|
624
|
+
guild_id,
|
615
625
|
)
|
616
626
|
await self.client._http.bulk_overwrite_guild_application_commands(
|
617
627
|
application_id, guild_id, commands_to_sync
|
618
628
|
)
|
619
629
|
else:
|
620
|
-
|
630
|
+
logger.info(
|
631
|
+
"Syncing %s global commands...",
|
632
|
+
len(commands_to_sync),
|
633
|
+
)
|
621
634
|
await self.client._http.bulk_overwrite_global_application_commands(
|
622
635
|
application_id, commands_to_sync
|
623
636
|
)
|
624
|
-
|
637
|
+
logger.info("Command sync successful.")
|
625
638
|
except Exception as e:
|
626
|
-
|
639
|
+
logger.error("Error syncing application commands: %s", e)
|
627
640
|
# Consider re-raising or specific error handling
|
@@ -0,0 +1,61 @@
|
|
1
|
+
# disagreement/ext/app_commands/hybrid.py
|
2
|
+
|
3
|
+
from typing import Any, Callable, List, Optional
|
4
|
+
|
5
|
+
from .commands import SlashCommand
|
6
|
+
from disagreement.ext.commands.core import PrefixCommand
|
7
|
+
|
8
|
+
|
9
|
+
class HybridCommand(SlashCommand, PrefixCommand): # Inherit from both
|
10
|
+
"""
|
11
|
+
Represents a command that can be invoked as both a slash command
|
12
|
+
and a traditional prefix-based command.
|
13
|
+
"""
|
14
|
+
|
15
|
+
def __init__(self, callback: Callable[..., Any], **kwargs: Any):
|
16
|
+
# Initialize SlashCommand part (which calls AppCommand.__init__)
|
17
|
+
# We need to ensure 'type' is correctly passed for AppCommand
|
18
|
+
# kwargs for SlashCommand: name, description, guild_ids, default_member_permissions, nsfw, parent, cog, etc.
|
19
|
+
# kwargs for PrefixCommand: name, aliases, brief, description, cog
|
20
|
+
|
21
|
+
# Pop prefix-specific args before passing to SlashCommand constructor
|
22
|
+
prefix_aliases = kwargs.pop("aliases", [])
|
23
|
+
prefix_brief = kwargs.pop("brief", None)
|
24
|
+
# Description is used by both, AppCommand's constructor will handle it.
|
25
|
+
# Name is used by both. Cog is used by both.
|
26
|
+
|
27
|
+
# Call SlashCommand's __init__
|
28
|
+
# This will set up name, description, callback, type=CHAT_INPUT, options, etc.
|
29
|
+
super().__init__(callback, **kwargs) # This is SlashCommand.__init__
|
30
|
+
|
31
|
+
# Now, explicitly initialize the PrefixCommand parts that SlashCommand didn't cover
|
32
|
+
# or that need specific values for the prefix version.
|
33
|
+
# PrefixCommand.__init__(self, callback, name=self.name, aliases=prefix_aliases, brief=prefix_brief, description=self.description, cog=self.cog)
|
34
|
+
# However, PrefixCommand.__init__ also sets self.params, which AppCommand already did.
|
35
|
+
# We need to be careful not to re-initialize things unnecessarily or incorrectly.
|
36
|
+
# Let's manually set the distinct attributes for the PrefixCommand aspect.
|
37
|
+
|
38
|
+
# Attributes from PrefixCommand:
|
39
|
+
# self.callback is already set by AppCommand
|
40
|
+
# self.name is already set by AppCommand
|
41
|
+
self.aliases: List[str] = (
|
42
|
+
prefix_aliases # This was specific to HybridCommand before, now aligns with PrefixCommand
|
43
|
+
)
|
44
|
+
self.brief: Optional[str] = prefix_brief
|
45
|
+
# self.description is already set by AppCommand (SlashCommand ensures it exists)
|
46
|
+
# self.cog is already set by AppCommand
|
47
|
+
# self.params is already set by AppCommand
|
48
|
+
|
49
|
+
# Ensure the MRO is handled correctly. Python's MRO (C3 linearization)
|
50
|
+
# should call SlashCommand's __init__ then AppCommand's __init__.
|
51
|
+
# PrefixCommand.__init__ won't be called automatically unless we explicitly call it.
|
52
|
+
# By setting attributes directly, we avoid potential issues with multiple __init__ calls
|
53
|
+
# if their logic overlaps too much (e.g., both trying to set self.params).
|
54
|
+
|
55
|
+
# We might need to override invoke if the context or argument passing differs significantly
|
56
|
+
# between app command invocation and prefix command invocation.
|
57
|
+
# For now, SlashCommand.invoke and PrefixCommand.invoke are separate.
|
58
|
+
# The correct one will be called depending on how the command is dispatched.
|
59
|
+
# The AppCommandHandler will use AppCommand.invoke (via SlashCommand).
|
60
|
+
# The prefix CommandHandler will use PrefixCommand.invoke.
|
61
|
+
# This seems acceptable.
|
disagreement/ext/commands/cog.py
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
# disagreement/ext/commands/cog.py
|
2
2
|
|
3
3
|
import inspect
|
4
|
+
import logging
|
4
5
|
from typing import TYPE_CHECKING, List, Tuple, Callable, Awaitable, Any, Dict, Union
|
5
6
|
|
6
7
|
if TYPE_CHECKING:
|
@@ -16,6 +17,8 @@ else: # pragma: no cover - runtime imports for isinstance checks
|
|
16
17
|
# EventDispatcher might be needed if cogs register listeners directly
|
17
18
|
# from disagreement.event_dispatcher import EventDispatcher
|
18
19
|
|
20
|
+
logger = logging.getLogger(__name__)
|
21
|
+
|
19
22
|
|
20
23
|
class Cog:
|
21
24
|
"""
|
@@ -59,8 +62,10 @@ class Cog:
|
|
59
62
|
cmd.cog = self # Assign the cog instance to the command
|
60
63
|
if cmd.name in self._commands:
|
61
64
|
# This should ideally be caught earlier or handled by CommandHandler
|
62
|
-
|
63
|
-
|
65
|
+
logger.warning(
|
66
|
+
"Duplicate command name '%s' in cog '%s'. Overwriting.",
|
67
|
+
cmd.name,
|
68
|
+
self.cog_name,
|
64
69
|
)
|
65
70
|
self._commands[cmd.name.lower()] = cmd
|
66
71
|
# Also register aliases
|
@@ -79,8 +84,10 @@ class Cog:
|
|
79
84
|
# For AppCommandGroup, its commands will have cog set individually if they are AppCommands
|
80
85
|
self._app_commands_and_groups.append(app_cmd_obj)
|
81
86
|
else:
|
82
|
-
|
83
|
-
|
87
|
+
logger.warning(
|
88
|
+
"Member '%s' in cog '%s' has '__app_command_object__' but it's not an AppCommand or AppCommandGroup.",
|
89
|
+
member_name,
|
90
|
+
self.cog_name,
|
84
91
|
)
|
85
92
|
|
86
93
|
elif isinstance(member, (AppCommand, AppCommandGroup)):
|
@@ -92,8 +99,10 @@ class Cog:
|
|
92
99
|
# This is a method decorated with @commands.Cog.listener or @commands.listener
|
93
100
|
if not inspect.iscoroutinefunction(member):
|
94
101
|
# Decorator should have caught this, but double check
|
95
|
-
|
96
|
-
|
102
|
+
logger.warning(
|
103
|
+
"Listener '%s' in cog '%s' is not a coroutine. Skipping.",
|
104
|
+
member_name,
|
105
|
+
self.cog_name,
|
97
106
|
)
|
98
107
|
continue
|
99
108
|
|
@@ -1,6 +1,9 @@
|
|
1
1
|
# disagreement/ext/commands/core.py
|
2
2
|
|
3
|
+
from __future__ import annotations
|
4
|
+
|
3
5
|
import asyncio
|
6
|
+
import logging
|
4
7
|
import inspect
|
5
8
|
from typing import (
|
6
9
|
TYPE_CHECKING,
|
@@ -27,10 +30,12 @@ from .errors import (
|
|
27
30
|
CommandInvokeError,
|
28
31
|
)
|
29
32
|
from .converters import run_converters, DEFAULT_CONVERTERS, Converter
|
30
|
-
from .cog import Cog
|
31
33
|
from disagreement.typing import Typing
|
32
34
|
|
35
|
+
logger = logging.getLogger(__name__)
|
36
|
+
|
33
37
|
if TYPE_CHECKING:
|
38
|
+
from .cog import Cog
|
34
39
|
from disagreement.client import Client
|
35
40
|
from disagreement.models import Message, User
|
36
41
|
|
@@ -86,6 +91,9 @@ class Command:
|
|
86
91
|
await self.callback(ctx, *args, **kwargs)
|
87
92
|
|
88
93
|
|
94
|
+
PrefixCommand = Command # Alias for clarity in hybrid commands
|
95
|
+
|
96
|
+
|
89
97
|
class CommandContext:
|
90
98
|
"""
|
91
99
|
Represents the context in which a command is being invoked.
|
@@ -123,7 +131,7 @@ class CommandContext:
|
|
123
131
|
|
124
132
|
async def reply(
|
125
133
|
self,
|
126
|
-
content: str,
|
134
|
+
content: Optional[str] = None,
|
127
135
|
*,
|
128
136
|
mention_author: Optional[bool] = None,
|
129
137
|
**kwargs: Any,
|
@@ -219,8 +227,10 @@ class CommandHandler:
|
|
219
227
|
self.commands[command.name.lower()] = command
|
220
228
|
for alias in command.aliases:
|
221
229
|
if alias in self.commands:
|
222
|
-
|
223
|
-
|
230
|
+
logger.warning(
|
231
|
+
"Alias '%s' for command '%s' conflicts with an existing command or alias.",
|
232
|
+
alias,
|
233
|
+
command.name,
|
224
234
|
)
|
225
235
|
self.commands[alias.lower()] = command
|
226
236
|
|
@@ -235,6 +245,8 @@ class CommandHandler:
|
|
235
245
|
return self.commands.get(name.lower())
|
236
246
|
|
237
247
|
def add_cog(self, cog_to_add: "Cog") -> None:
|
248
|
+
from .cog import Cog
|
249
|
+
|
238
250
|
if not isinstance(cog_to_add, Cog):
|
239
251
|
raise TypeError("Argument must be a subclass of Cog.")
|
240
252
|
|
@@ -252,8 +264,9 @@ class CommandHandler:
|
|
252
264
|
for event_name, callback in cog_to_add.get_listeners():
|
253
265
|
self.client._event_dispatcher.register(event_name.upper(), callback)
|
254
266
|
else:
|
255
|
-
|
256
|
-
|
267
|
+
logger.warning(
|
268
|
+
"Client does not have '_event_dispatcher'. Listeners for cog '%s' not registered.",
|
269
|
+
cog_to_add.cog_name,
|
257
270
|
)
|
258
271
|
|
259
272
|
if hasattr(cog_to_add, "cog_load") and inspect.iscoroutinefunction(
|
@@ -261,7 +274,7 @@ class CommandHandler:
|
|
261
274
|
):
|
262
275
|
asyncio.create_task(cog_to_add.cog_load())
|
263
276
|
|
264
|
-
|
277
|
+
logger.info("Cog '%s' added.", cog_to_add.cog_name)
|
265
278
|
|
266
279
|
def remove_cog(self, cog_name: str) -> Optional["Cog"]:
|
267
280
|
cog_to_remove = self.cogs.pop(cog_name, None)
|
@@ -271,8 +284,11 @@ class CommandHandler:
|
|
271
284
|
|
272
285
|
if hasattr(self.client, "_event_dispatcher"):
|
273
286
|
for event_name, callback in cog_to_remove.get_listeners():
|
274
|
-
|
275
|
-
|
287
|
+
logger.debug(
|
288
|
+
"Listener '%s' for event '%s' from cog '%s' needs manual unregistration logic in EventDispatcher.",
|
289
|
+
callback.__name__,
|
290
|
+
event_name,
|
291
|
+
cog_name,
|
276
292
|
)
|
277
293
|
|
278
294
|
if hasattr(cog_to_remove, "cog_unload") and inspect.iscoroutinefunction(
|
@@ -281,7 +297,7 @@ class CommandHandler:
|
|
281
297
|
asyncio.create_task(cog_to_remove.cog_unload())
|
282
298
|
|
283
299
|
cog_to_remove._eject()
|
284
|
-
|
300
|
+
logger.info("Cog '%s' removed.", cog_name)
|
285
301
|
return cog_to_remove
|
286
302
|
|
287
303
|
async def get_prefix(self, message: "Message") -> Union[str, List[str], None]:
|
@@ -487,11 +503,11 @@ class CommandHandler:
|
|
487
503
|
ctx.kwargs = parsed_kwargs
|
488
504
|
await command.invoke(ctx, *parsed_args, **parsed_kwargs)
|
489
505
|
except CommandError as e:
|
490
|
-
|
506
|
+
logger.error("Command error for '%s': %s", command.name, e)
|
491
507
|
if hasattr(self.client, "on_command_error"):
|
492
508
|
await self.client.on_command_error(ctx, e)
|
493
509
|
except Exception as e:
|
494
|
-
|
510
|
+
logger.error("Unexpected error invoking command '%s': %s", command.name, e)
|
495
511
|
exc = CommandInvokeError(e)
|
496
512
|
if hasattr(self.client, "on_command_error"):
|
497
513
|
await self.client.on_command_error(ctx, exc)
|