disagreement 0.1.0rc1__tar.gz → 0.1.0rc2__tar.gz

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.
Files changed (142) hide show
  1. {disagreement-0.1.0rc1/disagreement.egg-info → disagreement-0.1.0rc2}/PKG-INFO +6 -5
  2. {disagreement-0.1.0rc1 → disagreement-0.1.0rc2}/README.md +1 -1
  3. {disagreement-0.1.0rc1 → disagreement-0.1.0rc2}/disagreement/__init__.py +2 -2
  4. {disagreement-0.1.0rc1 → disagreement-0.1.0rc2}/disagreement/client.py +41 -2
  5. {disagreement-0.1.0rc1 → disagreement-0.1.0rc2}/disagreement/color.py +28 -0
  6. {disagreement-0.1.0rc1 → disagreement-0.1.0rc2}/disagreement/enums.py +5 -0
  7. {disagreement-0.1.0rc1 → disagreement-0.1.0rc2}/disagreement/ext/app_commands/__init__.py +2 -0
  8. {disagreement-0.1.0rc1 → disagreement-0.1.0rc2}/disagreement/ext/app_commands/commands.py +13 -99
  9. {disagreement-0.1.0rc1 → disagreement-0.1.0rc2}/disagreement/ext/app_commands/decorators.py +1 -1
  10. disagreement-0.1.0rc2/disagreement/ext/app_commands/hybrid.py +61 -0
  11. {disagreement-0.1.0rc1 → disagreement-0.1.0rc2}/disagreement/ext/commands/core.py +8 -2
  12. {disagreement-0.1.0rc1 → disagreement-0.1.0rc2}/disagreement/ext/tasks.py +46 -0
  13. {disagreement-0.1.0rc1 → disagreement-0.1.0rc2}/disagreement/http.py +115 -13
  14. {disagreement-0.1.0rc1 → disagreement-0.1.0rc2}/disagreement/interactions.py +17 -14
  15. {disagreement-0.1.0rc1 → disagreement-0.1.0rc2}/disagreement/models.py +124 -6
  16. {disagreement-0.1.0rc1 → disagreement-0.1.0rc2}/disagreement/ui/modal.py +1 -1
  17. disagreement-0.1.0rc2/disagreement/utils.py +73 -0
  18. {disagreement-0.1.0rc1 → disagreement-0.1.0rc2/disagreement.egg-info}/PKG-INFO +6 -5
  19. {disagreement-0.1.0rc1 → disagreement-0.1.0rc2}/disagreement.egg-info/SOURCES.txt +11 -0
  20. {disagreement-0.1.0rc1 → disagreement-0.1.0rc2}/disagreement.egg-info/requires.txt +2 -2
  21. {disagreement-0.1.0rc1 → disagreement-0.1.0rc2}/docs/caching.md +1 -1
  22. {disagreement-0.1.0rc1 → disagreement-0.1.0rc2}/docs/converters.md +3 -0
  23. {disagreement-0.1.0rc1 → disagreement-0.1.0rc2}/docs/gateway.md +6 -0
  24. disagreement-0.1.0rc2/docs/message_history.md +16 -0
  25. {disagreement-0.1.0rc1 → disagreement-0.1.0rc2}/docs/task_loop.md +16 -0
  26. {disagreement-0.1.0rc1 → disagreement-0.1.0rc2}/docs/using_components.md +1 -1
  27. {disagreement-0.1.0rc1 → disagreement-0.1.0rc2}/docs/webhooks.md +9 -0
  28. {disagreement-0.1.0rc1 → disagreement-0.1.0rc2}/examples/basic_bot.py +5 -4
  29. disagreement-0.1.0rc2/examples/extension_management.py +45 -0
  30. disagreement-0.1.0rc2/examples/message_history.py +31 -0
  31. {disagreement-0.1.0rc1 → disagreement-0.1.0rc2}/examples/modal_send.py +1 -0
  32. disagreement-0.1.0rc2/examples/sample_extension.py +16 -0
  33. {disagreement-0.1.0rc1 → disagreement-0.1.0rc2}/examples/sharded_bot.py +3 -0
  34. {disagreement-0.1.0rc1 → disagreement-0.1.0rc2}/pyproject.toml +5 -4
  35. disagreement-0.1.0rc2/tests/test_color_acceptance.py +29 -0
  36. {disagreement-0.1.0rc1 → disagreement-0.1.0rc2}/tests/test_command_checks.py +2 -2
  37. disagreement-0.1.0rc2/tests/test_gateway_intent.py +7 -0
  38. disagreement-0.1.0rc2/tests/test_http_rate_limit.py +70 -0
  39. {disagreement-0.1.0rc1 → disagreement-0.1.0rc2}/tests/test_http_reactions.py +51 -6
  40. disagreement-0.1.0rc2/tests/test_member.py +22 -0
  41. disagreement-0.1.0rc2/tests/test_message_pager.py +37 -0
  42. {disagreement-0.1.0rc1 → disagreement-0.1.0rc2}/tests/test_modals.py +2 -2
  43. {disagreement-0.1.0rc1 → disagreement-0.1.0rc2}/tests/test_tasks_extension.py +24 -0
  44. disagreement-0.1.0rc2/tests/test_textchannel_history.py +24 -0
  45. {disagreement-0.1.0rc1 → disagreement-0.1.0rc2}/tests/test_webhooks.py +43 -0
  46. disagreement-0.1.0rc1/disagreement/utils.py +0 -10
  47. {disagreement-0.1.0rc1 → disagreement-0.1.0rc2}/LICENSE +0 -0
  48. {disagreement-0.1.0rc1 → disagreement-0.1.0rc2}/MANIFEST.in +0 -0
  49. {disagreement-0.1.0rc1 → disagreement-0.1.0rc2}/disagreement/audio.py +0 -0
  50. {disagreement-0.1.0rc1 → disagreement-0.1.0rc2}/disagreement/cache.py +0 -0
  51. {disagreement-0.1.0rc1 → disagreement-0.1.0rc2}/disagreement/components.py +0 -0
  52. {disagreement-0.1.0rc1 → disagreement-0.1.0rc2}/disagreement/error_handler.py +0 -0
  53. {disagreement-0.1.0rc1 → disagreement-0.1.0rc2}/disagreement/errors.py +0 -0
  54. {disagreement-0.1.0rc1 → disagreement-0.1.0rc2}/disagreement/event_dispatcher.py +0 -0
  55. {disagreement-0.1.0rc1 → disagreement-0.1.0rc2}/disagreement/ext/__init__.py +0 -0
  56. {disagreement-0.1.0rc1 → disagreement-0.1.0rc2}/disagreement/ext/app_commands/context.py +0 -0
  57. {disagreement-0.1.0rc1 → disagreement-0.1.0rc2}/disagreement/ext/app_commands/converters.py +0 -0
  58. {disagreement-0.1.0rc1 → disagreement-0.1.0rc2}/disagreement/ext/app_commands/handler.py +0 -0
  59. {disagreement-0.1.0rc1 → disagreement-0.1.0rc2}/disagreement/ext/commands/__init__.py +0 -0
  60. {disagreement-0.1.0rc1 → disagreement-0.1.0rc2}/disagreement/ext/commands/cog.py +0 -0
  61. {disagreement-0.1.0rc1 → disagreement-0.1.0rc2}/disagreement/ext/commands/converters.py +0 -0
  62. {disagreement-0.1.0rc1 → disagreement-0.1.0rc2}/disagreement/ext/commands/decorators.py +0 -0
  63. {disagreement-0.1.0rc1 → disagreement-0.1.0rc2}/disagreement/ext/commands/errors.py +0 -0
  64. {disagreement-0.1.0rc1 → disagreement-0.1.0rc2}/disagreement/ext/commands/help.py +0 -0
  65. {disagreement-0.1.0rc1 → disagreement-0.1.0rc2}/disagreement/ext/commands/view.py +0 -0
  66. {disagreement-0.1.0rc1 → disagreement-0.1.0rc2}/disagreement/ext/loader.py +0 -0
  67. {disagreement-0.1.0rc1 → disagreement-0.1.0rc2}/disagreement/gateway.py +0 -0
  68. {disagreement-0.1.0rc1 → disagreement-0.1.0rc2}/disagreement/hybrid_context.py +0 -0
  69. {disagreement-0.1.0rc1 → disagreement-0.1.0rc2}/disagreement/i18n.py +0 -0
  70. {disagreement-0.1.0rc1 → disagreement-0.1.0rc2}/disagreement/logging_config.py +0 -0
  71. {disagreement-0.1.0rc1 → disagreement-0.1.0rc2}/disagreement/oauth.py +0 -0
  72. {disagreement-0.1.0rc1 → disagreement-0.1.0rc2}/disagreement/permissions.py +0 -0
  73. {disagreement-0.1.0rc1 → disagreement-0.1.0rc2}/disagreement/rate_limiter.py +0 -0
  74. {disagreement-0.1.0rc1 → disagreement-0.1.0rc2}/disagreement/shard_manager.py +0 -0
  75. {disagreement-0.1.0rc1 → disagreement-0.1.0rc2}/disagreement/typing.py +0 -0
  76. {disagreement-0.1.0rc1 → disagreement-0.1.0rc2}/disagreement/ui/__init__.py +0 -0
  77. {disagreement-0.1.0rc1 → disagreement-0.1.0rc2}/disagreement/ui/button.py +0 -0
  78. {disagreement-0.1.0rc1 → disagreement-0.1.0rc2}/disagreement/ui/item.py +0 -0
  79. {disagreement-0.1.0rc1 → disagreement-0.1.0rc2}/disagreement/ui/select.py +0 -0
  80. {disagreement-0.1.0rc1 → disagreement-0.1.0rc2}/disagreement/ui/view.py +0 -0
  81. {disagreement-0.1.0rc1 → disagreement-0.1.0rc2}/disagreement/voice_client.py +0 -0
  82. {disagreement-0.1.0rc1 → disagreement-0.1.0rc2}/disagreement.egg-info/dependency_links.txt +0 -0
  83. {disagreement-0.1.0rc1 → disagreement-0.1.0rc2}/disagreement.egg-info/top_level.txt +0 -0
  84. {disagreement-0.1.0rc1 → disagreement-0.1.0rc2}/docs/commands.md +0 -0
  85. {disagreement-0.1.0rc1 → disagreement-0.1.0rc2}/docs/context_menus.md +0 -0
  86. {disagreement-0.1.0rc1 → disagreement-0.1.0rc2}/docs/events.md +0 -0
  87. {disagreement-0.1.0rc1 → disagreement-0.1.0rc2}/docs/extension_loader.md +0 -0
  88. {disagreement-0.1.0rc1 → disagreement-0.1.0rc2}/docs/i18n.md +0 -0
  89. {disagreement-0.1.0rc1 → disagreement-0.1.0rc2}/docs/oauth2.md +0 -0
  90. {disagreement-0.1.0rc1 → disagreement-0.1.0rc2}/docs/permissions.md +0 -0
  91. {disagreement-0.1.0rc1 → disagreement-0.1.0rc2}/docs/presence.md +0 -0
  92. {disagreement-0.1.0rc1 → disagreement-0.1.0rc2}/docs/reactions.md +0 -0
  93. {disagreement-0.1.0rc1 → disagreement-0.1.0rc2}/docs/sharding.md +0 -0
  94. {disagreement-0.1.0rc1 → disagreement-0.1.0rc2}/docs/slash_commands.md +0 -0
  95. {disagreement-0.1.0rc1 → disagreement-0.1.0rc2}/docs/typing_indicator.md +0 -0
  96. {disagreement-0.1.0rc1 → disagreement-0.1.0rc2}/docs/voice_client.md +0 -0
  97. {disagreement-0.1.0rc1 → disagreement-0.1.0rc2}/docs/voice_features.md +0 -0
  98. {disagreement-0.1.0rc1 → disagreement-0.1.0rc2}/examples/component_bot.py +0 -0
  99. {disagreement-0.1.0rc1 → disagreement-0.1.0rc2}/examples/context_menus.py +0 -0
  100. {disagreement-0.1.0rc1 → disagreement-0.1.0rc2}/examples/hybrid_bot.py +0 -0
  101. {disagreement-0.1.0rc1 → disagreement-0.1.0rc2}/examples/modal_command.py +0 -0
  102. {disagreement-0.1.0rc1 → disagreement-0.1.0rc2}/examples/task_loop.py +0 -0
  103. {disagreement-0.1.0rc1 → disagreement-0.1.0rc2}/examples/voice_bot.py +0 -0
  104. {disagreement-0.1.0rc1 → disagreement-0.1.0rc2}/setup.cfg +0 -0
  105. {disagreement-0.1.0rc1 → disagreement-0.1.0rc2}/tests/test_additional_converters.py +0 -0
  106. {disagreement-0.1.0rc1 → disagreement-0.1.0rc2}/tests/test_cache.py +0 -0
  107. {disagreement-0.1.0rc1 → disagreement-0.1.0rc2}/tests/test_channel_permissions.py +0 -0
  108. {disagreement-0.1.0rc1 → disagreement-0.1.0rc2}/tests/test_client_context_manager.py +0 -0
  109. {disagreement-0.1.0rc1 → disagreement-0.1.0rc2}/tests/test_color.py +0 -0
  110. {disagreement-0.1.0rc1 → disagreement-0.1.0rc2}/tests/test_components_factory.py +0 -0
  111. {disagreement-0.1.0rc1 → disagreement-0.1.0rc2}/tests/test_context.py +0 -0
  112. {disagreement-0.1.0rc1 → disagreement-0.1.0rc2}/tests/test_context_menus.py +0 -0
  113. {disagreement-0.1.0rc1 → disagreement-0.1.0rc2}/tests/test_converter_registration.py +0 -0
  114. {disagreement-0.1.0rc1 → disagreement-0.1.0rc2}/tests/test_converters.py +0 -0
  115. {disagreement-0.1.0rc1 → disagreement-0.1.0rc2}/tests/test_error_handler.py +0 -0
  116. {disagreement-0.1.0rc1 → disagreement-0.1.0rc2}/tests/test_errors.py +0 -0
  117. {disagreement-0.1.0rc1 → disagreement-0.1.0rc2}/tests/test_event_dispatcher.py +0 -0
  118. {disagreement-0.1.0rc1 → disagreement-0.1.0rc2}/tests/test_event_error_hook.py +0 -0
  119. {disagreement-0.1.0rc1 → disagreement-0.1.0rc2}/tests/test_extension_loader.py +0 -0
  120. {disagreement-0.1.0rc1 → disagreement-0.1.0rc2}/tests/test_gateway_backoff.py +0 -0
  121. {disagreement-0.1.0rc1 → disagreement-0.1.0rc2}/tests/test_help_command.py +0 -0
  122. {disagreement-0.1.0rc1 → disagreement-0.1.0rc2}/tests/test_hybrid_context.py +0 -0
  123. {disagreement-0.1.0rc1 → disagreement-0.1.0rc2}/tests/test_i18n.py +0 -0
  124. {disagreement-0.1.0rc1 → disagreement-0.1.0rc2}/tests/test_interaction.py +0 -0
  125. {disagreement-0.1.0rc1 → disagreement-0.1.0rc2}/tests/test_logging_config.py +0 -0
  126. {disagreement-0.1.0rc1 → disagreement-0.1.0rc2}/tests/test_modal_send.py +0 -0
  127. {disagreement-0.1.0rc1 → disagreement-0.1.0rc2}/tests/test_oauth.py +0 -0
  128. {disagreement-0.1.0rc1 → disagreement-0.1.0rc2}/tests/test_permissions.py +0 -0
  129. {disagreement-0.1.0rc1 → disagreement-0.1.0rc2}/tests/test_presence_and_typing.py +0 -0
  130. {disagreement-0.1.0rc1 → disagreement-0.1.0rc2}/tests/test_presence_update.py +0 -0
  131. {disagreement-0.1.0rc1 → disagreement-0.1.0rc2}/tests/test_rate_limiter.py +0 -0
  132. {disagreement-0.1.0rc1 → disagreement-0.1.0rc2}/tests/test_reactions.py +0 -0
  133. {disagreement-0.1.0rc1 → disagreement-0.1.0rc2}/tests/test_send_files.py +0 -0
  134. {disagreement-0.1.0rc1 → disagreement-0.1.0rc2}/tests/test_sharding.py +0 -0
  135. {disagreement-0.1.0rc1 → disagreement-0.1.0rc2}/tests/test_slash_contexts.py +0 -0
  136. {disagreement-0.1.0rc1 → disagreement-0.1.0rc2}/tests/test_textchannel_purge.py +0 -0
  137. {disagreement-0.1.0rc1 → disagreement-0.1.0rc2}/tests/test_typing_indicator.py +0 -0
  138. {disagreement-0.1.0rc1 → disagreement-0.1.0rc2}/tests/test_ui.py +0 -0
  139. {disagreement-0.1.0rc1 → disagreement-0.1.0rc2}/tests/test_utils.py +0 -0
  140. {disagreement-0.1.0rc1 → disagreement-0.1.0rc2}/tests/test_view_layout.py +0 -0
  141. {disagreement-0.1.0rc1 → disagreement-0.1.0rc2}/tests/test_voice_client.py +0 -0
  142. {disagreement-0.1.0rc1 → disagreement-0.1.0rc2}/tests/test_wait_for.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: disagreement
3
- Version: 0.1.0rc1
3
+ Version: 0.1.0rc2
4
4
  Summary: A Python library for the Discord API.
5
5
  Author-email: Slipstream <me@slipstreamm.dev>
6
6
  License: BSD 3-Clause
@@ -12,22 +12,23 @@ Classifier: Intended Audience :: Developers
12
12
  Classifier: License :: OSI Approved :: BSD License
13
13
  Classifier: Operating System :: OS Independent
14
14
  Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.10
15
16
  Classifier: Programming Language :: Python :: 3.11
16
17
  Classifier: Programming Language :: Python :: 3.12
17
18
  Classifier: Programming Language :: Python :: 3.13
18
19
  Classifier: Topic :: Software Development :: Libraries
19
20
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
20
21
  Classifier: Topic :: Internet
21
- Requires-Python: >=3.11
22
+ Requires-Python: >=3.10
22
23
  Description-Content-Type: text/markdown
23
24
  License-File: LICENSE
24
25
  Requires-Dist: aiohttp<4.0.0,>=3.9.0
25
26
  Provides-Extra: test
26
27
  Requires-Dist: pytest>=8.0.0; extra == "test"
27
28
  Requires-Dist: pytest-asyncio>=1.0.0; extra == "test"
28
- Requires-Dist: hypothesis>=6.89.0; extra == "test"
29
+ Requires-Dist: hypothesis>=6.132.0; extra == "test"
29
30
  Provides-Extra: dev
30
- Requires-Dist: dotenv>=0.0.5; extra == "dev"
31
+ Requires-Dist: python-dotenv>=1.0.0; extra == "dev"
31
32
  Dynamic: license-file
32
33
 
33
34
  # Disagreement
@@ -53,7 +54,7 @@ pip install disagreement
53
54
  pip install -e .
54
55
  ```
55
56
 
56
- Requires Python 3.11 or newer.
57
+ Requires Python 3.10 or newer.
57
58
 
58
59
  ## Basic Usage
59
60
 
@@ -21,7 +21,7 @@ pip install disagreement
21
21
  pip install -e .
22
22
  ```
23
23
 
24
- Requires Python 3.11 or newer.
24
+ Requires Python 3.10 or newer.
25
25
 
26
26
  ## Basic Usage
27
27
 
@@ -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.0rc1"
17
+ __version__ = "0.1.0rc2"
18
18
 
19
19
  from .client import Client, AutoShardedClient
20
20
  from .models import Message, User, Reaction
@@ -30,7 +30,7 @@ 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
@@ -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
@@ -638,6 +640,23 @@ class Client:
638
640
  # import traceback
639
641
  # traceback.print_exception(type(error.original), error.original, error.original.__traceback__)
640
642
 
643
+ # --- Extension Management Methods ---
644
+
645
+ def load_extension(self, name: str) -> ModuleType:
646
+ """Load an extension by name using :mod:`disagreement.ext.loader`."""
647
+
648
+ return ext_loader.load_extension(name)
649
+
650
+ def unload_extension(self, name: str) -> None:
651
+ """Unload a previously loaded extension."""
652
+
653
+ ext_loader.unload_extension(name)
654
+
655
+ def reload_extension(self, name: str) -> ModuleType:
656
+ """Reload an extension by name."""
657
+
658
+ return ext_loader.reload_extension(name)
659
+
641
660
  # --- Model Parsing and Fetching ---
642
661
 
643
662
  def parse_user(self, data: Dict[str, Any]) -> "User":
@@ -920,12 +939,12 @@ class Client:
920
939
  await view._start(self)
921
940
  components_payload = view.to_components_payload()
922
941
  elif components:
923
- from .models import ActionRow as ActionRowModel
942
+ from .models import Component as ComponentModel
924
943
 
925
944
  components_payload = [
926
945
  comp.to_dict()
927
946
  for comp in components
928
- if isinstance(comp, ActionRowModel)
947
+ if isinstance(comp, ComponentModel)
929
948
  ]
930
949
 
931
950
  message_data = await self._http.send_message(
@@ -1016,6 +1035,26 @@ class Client:
1016
1035
  self._voice_clients[guild_id] = voice
1017
1036
  return voice
1018
1037
 
1038
+ async def add_reaction(self, channel_id: str, message_id: str, emoji: str) -> None:
1039
+ """|coro| Add a reaction to a message."""
1040
+
1041
+ await self.create_reaction(channel_id, message_id, emoji)
1042
+
1043
+ async def remove_reaction(
1044
+ self, channel_id: str, message_id: str, emoji: str
1045
+ ) -> None:
1046
+ """|coro| Remove the bot's reaction from a message."""
1047
+
1048
+ await self.delete_reaction(channel_id, message_id, emoji)
1049
+
1050
+ async def clear_reactions(self, channel_id: str, message_id: str) -> None:
1051
+ """|coro| Remove all reactions from a message."""
1052
+
1053
+ if self._closed:
1054
+ raise DisagreementException("Client is closed.")
1055
+
1056
+ await self._http.clear_reactions(channel_id, message_id)
1057
+
1019
1058
  async def create_reaction(
1020
1059
  self, channel_id: str, message_id: str, emoji: str
1021
1060
  ) -> None:
@@ -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")
@@ -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)."""
@@ -44,3 +44,5 @@ __all__ = [
44
44
  "OptionMetadata",
45
45
  "AppCommandContext", # To be defined
46
46
  ]
47
+
48
+ from .hybrid import *
@@ -1,58 +1,25 @@
1
1
  # disagreement/ext/app_commands/commands.py
2
2
 
3
3
  import inspect
4
- from typing import Callable, Optional, List, Dict, Any, Union, TYPE_CHECKING
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
- Command as PrefixCommand,
10
- ) # Alias to avoid name clash
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 Exception: # pragma: no cover - safeguard against unusual import issues
39
- PrefixCommand = Any # type: ignore
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:
@@ -26,8 +26,8 @@ from .commands import (
26
26
  MessageCommand,
27
27
  AppCommand,
28
28
  AppCommandGroup,
29
- HybridCommand,
30
29
  )
30
+ from .hybrid import HybridCommand
31
31
  from disagreement.interactions import (
32
32
  ApplicationCommandOption,
33
33
  ApplicationCommandOptionChoice,
@@ -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.
@@ -1,5 +1,7 @@
1
1
  # disagreement/ext/commands/core.py
2
2
 
3
+ from __future__ import annotations
4
+
3
5
  import asyncio
4
6
  import inspect
5
7
  from typing import (
@@ -27,10 +29,10 @@ from .errors import (
27
29
  CommandInvokeError,
28
30
  )
29
31
  from .converters import run_converters, DEFAULT_CONVERTERS, Converter
30
- from .cog import Cog
31
32
  from disagreement.typing import Typing
32
33
 
33
34
  if TYPE_CHECKING:
35
+ from .cog import Cog
34
36
  from disagreement.client import Client
35
37
  from disagreement.models import Message, User
36
38
 
@@ -86,6 +88,9 @@ class Command:
86
88
  await self.callback(ctx, *args, **kwargs)
87
89
 
88
90
 
91
+ PrefixCommand = Command # Alias for clarity in hybrid commands
92
+
93
+
89
94
  class CommandContext:
90
95
  """
91
96
  Represents the context in which a command is being invoked.
@@ -123,7 +128,7 @@ class CommandContext:
123
128
 
124
129
  async def reply(
125
130
  self,
126
- content: str,
131
+ content: Optional[str] = None,
127
132
  *,
128
133
  mention_author: Optional[bool] = None,
129
134
  **kwargs: Any,
@@ -235,6 +240,7 @@ class CommandHandler:
235
240
  return self.commands.get(name.lower())
236
241
 
237
242
  def add_cog(self, cog_to_add: "Cog") -> None:
243
+ from .cog import Cog
238
244
  if not isinstance(cog_to_add, Cog):
239
245
  raise TypeError("Argument must be a subclass of Cog.")
240
246
 
@@ -18,6 +18,8 @@ class Task:
18
18
  delta: Optional[datetime.timedelta] = None,
19
19
  time_of_day: Optional[datetime.time] = None,
20
20
  on_error: Optional[Callable[[Exception], Awaitable[None]]] = None,
21
+ before_loop: Optional[Callable[[], Awaitable[None] | None]] = None,
22
+ after_loop: Optional[Callable[[], Awaitable[None] | None]] = None,
21
23
  ) -> None:
22
24
  self._coro = coro
23
25
  self._task: Optional[asyncio.Task[None]] = None
@@ -36,6 +38,8 @@ class Task:
36
38
  self._seconds = float(interval_seconds)
37
39
  self._time_of_day = time_of_day
38
40
  self._on_error = on_error
41
+ self._before_loop = before_loop
42
+ self._after_loop = after_loop
39
43
 
40
44
  def _seconds_until_time(self) -> float:
41
45
  assert self._time_of_day is not None
@@ -47,6 +51,9 @@ class Task:
47
51
 
48
52
  async def _run(self, *args: Any, **kwargs: Any) -> None:
49
53
  try:
54
+ if self._before_loop is not None:
55
+ await _maybe_call_no_args(self._before_loop)
56
+
50
57
  first = True
51
58
  while True:
52
59
  if self._time_of_day is not None:
@@ -65,6 +72,9 @@ class Task:
65
72
  first = False
66
73
  except asyncio.CancelledError:
67
74
  pass
75
+ finally:
76
+ if self._after_loop is not None:
77
+ await _maybe_call_no_args(self._after_loop)
68
78
 
69
79
  def start(self, *args: Any, **kwargs: Any) -> asyncio.Task[None]:
70
80
  if self._task is None or self._task.done():
@@ -89,6 +99,12 @@ async def _maybe_call(
89
99
  await result
90
100
 
91
101
 
102
+ async def _maybe_call_no_args(func: Callable[[], Awaitable[None] | None]) -> None:
103
+ result = func()
104
+ if asyncio.iscoroutine(result):
105
+ await result
106
+
107
+
92
108
  class _Loop:
93
109
  def __init__(
94
110
  self,
@@ -110,6 +126,8 @@ class _Loop:
110
126
  self.on_error = on_error
111
127
  self._task: Optional[Task] = None
112
128
  self._owner: Any = None
129
+ self._before_loop: Optional[Callable[..., Awaitable[Any]]] = None
130
+ self._after_loop: Optional[Callable[..., Awaitable[Any]]] = None
113
131
 
114
132
  def __get__(self, obj: Any, objtype: Any) -> "_BoundLoop":
115
133
  return _BoundLoop(self, obj)
@@ -119,7 +137,33 @@ class _Loop:
119
137
  return self.func(*args, **kwargs)
120
138
  return self.func(self._owner, *args, **kwargs)
121
139
 
140
+ def before_loop(
141
+ self, func: Callable[..., Awaitable[Any]]
142
+ ) -> Callable[..., Awaitable[Any]]:
143
+ self._before_loop = func
144
+ return func
145
+
146
+ def after_loop(
147
+ self, func: Callable[..., Awaitable[Any]]
148
+ ) -> Callable[..., Awaitable[Any]]:
149
+ self._after_loop = func
150
+ return func
151
+
122
152
  def start(self, *args: Any, **kwargs: Any) -> asyncio.Task[None]:
153
+ def call_before() -> Awaitable[None] | None:
154
+ if self._before_loop is None:
155
+ return None
156
+ if self._owner is not None:
157
+ return self._before_loop(self._owner)
158
+ return self._before_loop()
159
+
160
+ def call_after() -> Awaitable[None] | None:
161
+ if self._after_loop is None:
162
+ return None
163
+ if self._owner is not None:
164
+ return self._after_loop(self._owner)
165
+ return self._after_loop()
166
+
123
167
  self._task = Task(
124
168
  self._coro,
125
169
  seconds=self.seconds,
@@ -128,6 +172,8 @@ class _Loop:
128
172
  delta=self.delta,
129
173
  time_of_day=self.time_of_day,
130
174
  on_error=self.on_error,
175
+ before_loop=call_before,
176
+ after_loop=call_after,
131
177
  )
132
178
  return self._task.start(*args, **kwargs)
133
179