hikari-arc 0.5.0__py3-none-any.whl → 0.6.0__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.
arc/__init__.py CHANGED
@@ -11,9 +11,9 @@ https://arc.hypergonial.com
11
11
  from alluka import Client as Injector
12
12
  from alluka import inject
13
13
 
14
- from arc import abc, command
14
+ from arc import abc, command, utils
15
15
 
16
- from .abc import HookResult, Option, with_hook, with_post_hook
16
+ from .abc import HookResult, Option, OptionType, with_hook, with_post_hook
17
17
  from .client import (
18
18
  Client,
19
19
  GatewayClient,
@@ -51,7 +51,9 @@ from .errors import (
51
51
  ArcError,
52
52
  AutocompleteError,
53
53
  CommandInvokeError,
54
+ CommandPublishFailedError,
54
55
  DMOnlyError,
56
+ GuildCommandPublishFailedError,
55
57
  GuildOnlyError,
56
58
  InvokerMissingPermissionsError,
57
59
  NotOwnerError,
@@ -121,6 +123,7 @@ __all__ = (
121
123
  "RESTClientBase",
122
124
  "GatewayClient",
123
125
  "RESTClient",
126
+ "OptionType",
124
127
  "ArcError",
125
128
  "AutocompleteError",
126
129
  "UnderCooldownError",
@@ -129,6 +132,8 @@ __all__ = (
129
132
  "NotOwnerError",
130
133
  "DMOnlyError",
131
134
  "CommandInvokeError",
135
+ "GuildCommandPublishFailedError",
136
+ "CommandPublishFailedError",
132
137
  "PluginBase",
133
138
  "RESTPluginBase",
134
139
  "GatewayPluginBase",
@@ -149,6 +154,7 @@ __all__ = (
149
154
  "CommandLocaleRequest",
150
155
  "OptionLocaleRequest",
151
156
  "abc",
157
+ "utils",
152
158
  "command",
153
159
  "with_hook",
154
160
  "with_post_hook",
arc/abc/__init__.py CHANGED
@@ -2,7 +2,15 @@ from .client import Client
2
2
  from .command import CallableCommandBase, CallableCommandProto, CommandBase, CommandProto
3
3
  from .error_handler import HasErrorHandler
4
4
  from .hookable import Hookable, HookResult, with_hook, with_post_hook
5
- from .option import CommandOptionBase, Option, OptionBase, OptionParams, OptionWithChoices, OptionWithChoicesParams
5
+ from .option import (
6
+ CommandOptionBase,
7
+ Option,
8
+ OptionBase,
9
+ OptionParams,
10
+ OptionType,
11
+ OptionWithChoices,
12
+ OptionWithChoicesParams,
13
+ )
6
14
  from .plugin import PluginBase
7
15
 
8
16
  __all__ = (
@@ -13,6 +21,7 @@ __all__ = (
13
21
  "CallableCommandBase",
14
22
  "Option",
15
23
  "OptionBase",
24
+ "OptionType",
16
25
  "CommandOptionBase",
17
26
  "OptionParams",
18
27
  "OptionWithChoices",
arc/abc/client.py CHANGED
@@ -18,7 +18,7 @@ import hikari
18
18
  from arc.abc.command import _CommandSettings
19
19
  from arc.abc.plugin import PluginBase
20
20
  from arc.command.message import MessageCommand
21
- from arc.command.slash import SlashCommand, SlashGroup
21
+ from arc.command.slash import SlashCommand, SlashGroup, SlashSubCommand, SlashSubGroup
22
22
  from arc.command.user import UserCommand
23
23
  from arc.context import AutodeferMode, Context
24
24
  from arc.errors import ExtensionLoadError, ExtensionUnloadError
@@ -156,6 +156,20 @@ class Client(t.Generic[AppT], abc.ABC):
156
156
  will return interaction response builders to be sent back to Discord, otherwise they will return None.
157
157
  """
158
158
 
159
+ @property
160
+ def _commands(self) -> t.Mapping[hikari.CommandType, t.Mapping[str, CommandBase[te.Self, t.Any]]]:
161
+ """All top-level commands added to this client, categorized by command type.
162
+
163
+ !!! note
164
+ This does not include subcommands & subgroups due to implementation details, therefore
165
+ you should use [`Client.walk_commands()`][arc.abc.client.Client.walk_commands] instead.
166
+ """
167
+ return {
168
+ hikari.CommandType.SLASH: self._slash_commands,
169
+ hikari.CommandType.MESSAGE: self._message_commands,
170
+ hikari.CommandType.USER: self._user_commands,
171
+ }
172
+
159
173
  @property
160
174
  def app(self) -> AppT:
161
175
  """The application this client is for."""
@@ -181,30 +195,6 @@ class Client(t.Generic[AppT], abc.ABC):
181
195
  """The guilds that slash commands will be registered in by default."""
182
196
  return self._default_enabled_guilds
183
197
 
184
- @property
185
- def commands(self) -> t.Mapping[hikari.CommandType, t.Mapping[str, CommandBase[te.Self, t.Any]]]:
186
- """All commands added to this client, categorized by command type."""
187
- return {
188
- hikari.CommandType.SLASH: self.slash_commands,
189
- hikari.CommandType.MESSAGE: self._message_commands,
190
- hikari.CommandType.USER: self._user_commands,
191
- }
192
-
193
- @property
194
- def slash_commands(self) -> t.Mapping[str, SlashCommandLike[te.Self]]:
195
- """The slash commands added to this client. This only includes top-level commands and groups."""
196
- return self._slash_commands
197
-
198
- @property
199
- def message_commands(self) -> t.Mapping[str, MessageCommand[te.Self]]:
200
- """The message commands added to this client."""
201
- return self._message_commands
202
-
203
- @property
204
- def user_commands(self) -> t.Mapping[str, UserCommand[te.Self]]:
205
- """The user commands added to this client."""
206
- return self._user_commands
207
-
208
198
  @property
209
199
  def plugins(self) -> t.Mapping[str, PluginBase[te.Self]]:
210
200
  """The plugins added to this client."""
@@ -318,11 +308,11 @@ class Client(t.Generic[AppT], abc.ABC):
318
308
 
319
309
  match interaction.command_type:
320
310
  case hikari.CommandType.SLASH:
321
- command = self.slash_commands.get(interaction.command_name)
311
+ command = self._slash_commands.get(interaction.command_name)
322
312
  case hikari.CommandType.MESSAGE:
323
- command = self.message_commands.get(interaction.command_name)
313
+ command = self._message_commands.get(interaction.command_name)
324
314
  case hikari.CommandType.USER:
325
- command = self.user_commands.get(interaction.command_name)
315
+ command = self._user_commands.get(interaction.command_name)
326
316
  case _:
327
317
  pass
328
318
 
@@ -372,7 +362,7 @@ class Client(t.Generic[AppT], abc.ABC):
372
362
  hikari.api.InteractionAutocompleteBuilder | None
373
363
  The autocomplete builder to send back to Discord, if using a REST client.
374
364
  """
375
- command = self.slash_commands.get(interaction.command_name)
365
+ command = self._slash_commands.get(interaction.command_name)
376
366
 
377
367
  if command is None:
378
368
  logger.warning(f"Received autocomplete interaction for unknown command '{interaction.command_name}'.")
@@ -380,6 +370,98 @@ class Client(t.Generic[AppT], abc.ABC):
380
370
 
381
371
  return await command._on_autocomplete(interaction)
382
372
 
373
+ @t.overload
374
+ def walk_commands(
375
+ self, command_type: t.Literal[hikari.CommandType.USER], *, callable_only: bool = False
376
+ ) -> t.Iterator[UserCommand[te.Self]]:
377
+ ...
378
+
379
+ @t.overload
380
+ def walk_commands(
381
+ self, command_type: t.Literal[hikari.CommandType.MESSAGE], *, callable_only: bool = False
382
+ ) -> t.Iterator[MessageCommand[te.Self]]:
383
+ ...
384
+
385
+ @t.overload
386
+ def walk_commands(
387
+ self, command_type: t.Literal[hikari.CommandType.SLASH], *, callable_only: t.Literal[False] = False
388
+ ) -> t.Iterator[SlashCommand[te.Self] | SlashSubCommand[te.Self] | SlashGroup[te.Self] | SlashSubGroup[te.Self]]:
389
+ ...
390
+
391
+ @t.overload
392
+ def walk_commands(
393
+ self, command_type: t.Literal[hikari.CommandType.SLASH], *, callable_only: t.Literal[True] = True
394
+ ) -> t.Iterator[SlashCommand[te.Self] | SlashSubCommand[te.Self]]:
395
+ ...
396
+
397
+ def walk_commands( # noqa: C901
398
+ self, command_type: hikari.CommandType, *, callable_only: bool = False
399
+ ) -> t.Iterator[t.Any]:
400
+ """Iterate over all commands of a certain type added to this plugin.
401
+
402
+ Parameters
403
+ ----------
404
+ command_type : hikari.CommandType
405
+ The type of commands to return.
406
+ callable_only : bool
407
+ Whether to only return commands that are directly callable.
408
+ If True, command groups and subgroups will be skipped.
409
+ This is only applicable to slash commands.
410
+
411
+ Yields
412
+ ------
413
+ CommandT[te.Self]
414
+ The next command that matches the given criteria.
415
+
416
+ Usage
417
+ -----
418
+ ```py
419
+ for cmd in plugin.walk_commands(hikari.CommandType.SLASH):
420
+ print(cmd.name)
421
+ ```
422
+
423
+ !!! tip
424
+ To iterate over all types of commands, you may use [`itertools.chain()`][itertools.chain]:
425
+
426
+ ```py
427
+ import itertools
428
+
429
+ for cmd in itertools.chain(
430
+ plugin.walk_commands(hikari.CommandType.SLASH),
431
+ plugin.walk_commands(hikari.CommandType.MESSAGE),
432
+ plugin.walk_commands(hikari.CommandType.USER),
433
+ ):
434
+ print(cmd.name)
435
+ ```
436
+ """
437
+ if hikari.CommandType.SLASH is command_type:
438
+ for command in self._slash_commands.values():
439
+ if isinstance(command, SlashCommand):
440
+ yield command
441
+ continue
442
+
443
+ if not callable_only:
444
+ yield command
445
+
446
+ for sub in command.children.values():
447
+ if isinstance(sub, SlashSubCommand):
448
+ yield sub
449
+ continue
450
+
451
+ if not callable_only:
452
+ yield sub
453
+
454
+ for subsub in sub.children.values():
455
+ yield subsub
456
+
457
+ elif hikari.CommandType.MESSAGE is command_type:
458
+ for command in self._message_commands.values():
459
+ yield command
460
+
461
+ elif hikari.CommandType.USER is command_type:
462
+ for command in self._user_commands.values():
463
+ yield command
464
+
383
465
  @t.overload
384
466
  def include(self) -> t.Callable[[CommandBase[te.Self, BuilderT]], CommandBase[te.Self, BuilderT]]:
385
467
  ...
@@ -423,7 +505,7 @@ class Client(t.Generic[AppT], abc.ABC):
423
505
  f"\nYou should use '{type(self).__name__}.add_plugin()' to add the entire plugin to the client."
424
506
  )
425
507
 
426
- if existing := self.commands[command.command_type].get(command.name):
508
+ if existing := self._commands[command.command_type].get(command.name):
427
509
  logger.warning(
428
510
  f"Shadowing already registered command '{command.name}'. Did you define multiple commands with the same name?"
429
511
  )
@@ -663,6 +745,10 @@ class Client(t.Generic[AppT], abc.ABC):
663
745
  await ctx.respond(f"❌ Something went wrong: {exception}")
664
746
  ```
665
747
 
748
+ !!! warning
749
+ Errors that cannot be handled by the error handler should be re-raised.
750
+ Otherwise tracebacks will not be printed to stderr.
751
+
666
752
  Or, as a function:
667
753
 
668
754
  ```py
arc/abc/command.py CHANGED
@@ -13,6 +13,7 @@ from arc.abc.hookable import Hookable, HookResult
13
13
  from arc.abc.limiter import LimiterProto
14
14
  from arc.abc.option import OptionBase
15
15
  from arc.context import AutodeferMode
16
+ from arc.errors import CommandPublishFailedError
16
17
  from arc.internal.types import (
17
18
  BuilderT,
18
19
  ClientT,
@@ -49,6 +50,15 @@ class CommandProto(t.Protocol):
49
50
  def qualified_name(self) -> t.Sequence[str]:
50
51
  """The fully qualified name of this command."""
51
52
 
53
+ @property
54
+ @abc.abstractmethod
55
+ def display_name(self) -> str:
56
+ """The display name of this command. This is what is shown in the Discord client.
57
+
58
+ !!! note
59
+ Slash commands can also be mentioned, see [SlashCommand.make_mention][arc.command.SlashCommand.make_mention].
60
+ """
61
+
52
62
 
53
63
  class CallableCommandProto(CommandProto, t.Protocol[ClientT]):
54
64
  """A protocol for any command-like object that can be called directly.
@@ -74,6 +84,15 @@ class CallableCommandProto(CommandProto, t.Protocol[ClientT]):
74
84
  def qualified_name(self) -> t.Sequence[str]:
75
85
  """The fully qualified name of this command."""
76
86
 
87
+ @property
88
+ @abc.abstractmethod
89
+ def display_name(self) -> str:
90
+ """The display name of this command. This is what is shown in the Discord client.
91
+
92
+ !!! note
93
+ Slash commands can also be mentioned, see [SlashCommand.make_mention][arc.command.SlashCommand.make_mention].
94
+ """
95
+
77
96
  @property
78
97
  @abc.abstractmethod
79
98
  def hooks(self) -> t.MutableSequence[HookT[ClientT]]:
@@ -260,7 +279,7 @@ class CommandBase(HasErrorHandler[ClientT], Hookable[ClientT], t.Generic[ClientT
260
279
  """The client that is handling this command."""
261
280
  if self._client is None:
262
281
  raise RuntimeError(
263
- f"Command '{self.qualified_name}' was not included in a client, '{type(self).__name__}.client' cannot be accessed until it is included in a client."
282
+ f"Command '{self.display_name}' was not included in a client, '{type(self).__name__}.client' cannot be accessed until it is included in a client."
264
283
  )
265
284
  return self._client
266
285
 
@@ -292,6 +311,20 @@ class CommandBase(HasErrorHandler[ClientT], Hookable[ClientT], t.Generic[ClientT
292
311
  settings = self._resolve_settings()
293
312
  return settings.is_nsfw if settings.is_nsfw is not hikari.UNDEFINED else False
294
313
 
314
+ @property
315
+ def instances(self) -> t.Mapping[hikari.Snowflake | None, hikari.PartialCommand]:
316
+ """A mapping of guild IDs to command instances. None corresponds to the global instance, if any."""
317
+ return self._instances
318
+
319
+ @property
320
+ def display_name(self) -> str:
321
+ """The display name of this command. This is what is shown in the Discord client.
322
+
323
+ !!! note
324
+ Slash commands can also be mentioned, see [SlashCommand.make_mention][arc.command.SlashCommand.make_mention].
325
+ """
326
+ return self.name
327
+
295
328
  def _register_instance(
296
329
  self, instance: hikari.PartialCommand, guild: hikari.SnowflakeishOr[hikari.PartialGuild] | None = None
297
330
  ) -> None:
@@ -352,13 +385,15 @@ class CommandBase(HasErrorHandler[ClientT], Hookable[ClientT], t.Generic[ClientT
352
385
  raise RuntimeError("Cannot publish command without a client.")
353
386
 
354
387
  kwargs = self._to_dict()
355
-
356
- if self.command_type is hikari.CommandType.SLASH:
357
- created = await self.client.app.rest.create_slash_command(self.client.application, **kwargs)
358
- else:
359
- created = await self.client.app.rest.create_context_menu_command(
360
- self.client.application, type=self.command_type, **kwargs
361
- )
388
+ try:
389
+ if self.command_type is hikari.CommandType.SLASH:
390
+ created = await self.client.app.rest.create_slash_command(self.client.application, **kwargs)
391
+ else:
392
+ created = await self.client.app.rest.create_context_menu_command(
393
+ self.client.application, type=self.command_type, **kwargs
394
+ )
395
+ except Exception as e:
396
+ raise CommandPublishFailedError(self, f"Failed to publish command '{self.display_name}'") from e
362
397
 
363
398
  self._instances[hikari.Snowflake(guild) if guild else None] = created
364
399
 
@@ -427,8 +462,8 @@ class CommandBase(HasErrorHandler[ClientT], Hookable[ClientT], t.Generic[ClientT
427
462
 
428
463
  def _client_remove_hook(self, client: ClientT) -> None:
429
464
  """Called when the client requests the command be removed from it."""
430
- self._client = None
431
465
  self.client._remove_command(self)
466
+ self._client = None
432
467
 
433
468
  def _plugin_include_hook(self, plugin: PluginBase[ClientT]) -> None:
434
469
  """Called when the plugin requests the command be added to it."""
@@ -517,7 +552,7 @@ class CallableCommandBase(CommandBase[ClientT, BuilderT], CallableCommandProto[C
517
552
  callback: CommandCallbackT[ClientT]
518
553
  """The callback to invoke when this command is called."""
519
554
 
520
- _invoke_task: asyncio.Task[t.Any] | None = attr.field(init=False, default=None)
555
+ _invoke_task: asyncio.Task[t.Any] | None = attr.field(init=False, default=None, repr=False)
521
556
 
522
557
  def reset_all_limiters(self, context: Context[ClientT]) -> None:
523
558
  """Reset all limiters for this command.
@@ -563,6 +598,15 @@ class SubCommandBase(OptionBase[ClientT], HasErrorHandler[ClientT], Hookable[Cli
563
598
  _parent: ParentT | None = attr.field(default=None, init=False, alias="parent")
564
599
  """The parent of this subcommand or subgroup."""
565
600
 
601
+ @property
602
+ @abc.abstractmethod
603
+ def display_name(self) -> str:
604
+ """The display name of this command. This is what is shown in the Discord client.
605
+
606
+ !!! note
607
+ Slash commands can also be mentioned, see [SlashCommand.make_mention][arc.command.SlashCommand.make_mention].
608
+ """
609
+
566
610
  @property
567
611
  def error_handler(self) -> ErrorHandlerCallbackT[ClientT] | None:
568
612
  """The error handler for this object."""
arc/abc/error_handler.py CHANGED
@@ -45,12 +45,20 @@ class HasErrorHandler(abc.ABC, t.Generic[ClientT]):
45
45
  @client.include
46
46
  @arc.slash_command("foo", "Foo command description")
47
47
  async def foo(ctx: arc.GatewayContext) -> None:
48
- raise Exception("foo")
48
+ raise RuntimeError("foo")
49
49
 
50
50
  @foo.set_error_handler
51
51
  async def foo_error_handler(ctx: arc.GatewayContext, exc: Exception) -> None:
52
- await ctx.respond("foo failed")
52
+ if isinstance(exc, RuntimeError):
53
+ await ctx.respond("foo failed")
54
+ return
55
+
56
+ raise exc
53
57
  ```
58
+
59
+ !!! warning
60
+ Errors that cannot be handled by the error handler should be re-raised.
61
+ Otherwise they will not propagate to the next error handler.
54
62
  """
55
63
 
56
64
  def decorator(func: ErrorHandlerCallbackT[ClientT]) -> ErrorHandlerCallbackT[ClientT]:
arc/abc/limiter.py CHANGED
@@ -12,12 +12,15 @@ if t.TYPE_CHECKING:
12
12
 
13
13
  @t.runtime_checkable
14
14
  class LimiterProto(t.Protocol, t.Generic[ClientT]):
15
- """A protocol that all ratelimiters should implement.
15
+ """A protocol that all limiter hooks should implement.
16
16
  A limiter is simply a special type of hook with added methods.
17
17
 
18
18
  If you're looking to integrate your own ratelimiter implementation,
19
19
  you should make sure to implement all methods defined here.
20
- An easy (but not necessary) way to do this is to simply inherit from this class.
20
+
21
+ !!! tip
22
+ An easy (but not necessary) way to ensure you've implemented all methods
23
+ is to inherit from this protocol.
21
24
  """
22
25
 
23
26
  @abc.abstractmethod
@@ -32,20 +35,6 @@ class LimiterProto(t.Protocol, t.Generic[ClientT]):
32
35
  The context to evaluate the ratelimit under.
33
36
  """
34
37
 
35
- @abc.abstractmethod
36
- async def acquire(self, ctx: Context[ClientT], *, wait: bool = True) -> None:
37
- """Acquire the limiter with the given context.
38
- Implementations should raise an exception if the limiter is ratelimited
39
- and wait is False.
40
-
41
- Parameters
42
- ----------
43
- ctx : Context
44
- The context to evaluate the ratelimit under.
45
- wait : bool
46
- Whether or not to block until the limiter is available.
47
- """
48
-
49
38
  @abc.abstractmethod
50
39
  def reset(self, ctx: Context[ClientT]) -> None:
51
40
  """Reset the limiter for the given context.
arc/abc/option.py CHANGED
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import abc
4
+ import enum
4
5
  import typing as t
5
6
  from typing import Any
6
7
 
@@ -16,7 +17,15 @@ if t.TYPE_CHECKING:
16
17
  from arc.abc.client import Client
17
18
  from arc.abc.command import CommandProto
18
19
 
19
- __all__ = ("Option", "OptionParams", "OptionWithChoices", "OptionWithChoicesParams", "OptionBase", "CommandOptionBase")
20
+ __all__ = (
21
+ "Option",
22
+ "OptionParams",
23
+ "OptionWithChoices",
24
+ "OptionWithChoicesParams",
25
+ "OptionBase",
26
+ "CommandOptionBase",
27
+ "OptionType",
28
+ )
20
29
 
21
30
  T = t.TypeVar("T")
22
31
 
@@ -37,6 +46,65 @@ arc.Option[int, arc.IntParams(...)]
37
46
  """
38
47
 
39
48
 
49
+ class OptionType(enum.IntEnum):
50
+ """The type of a command option.
51
+
52
+ This is practically identical to `hikari.OptionType` at the moment.
53
+ It may however be used in the future to define custom option types.
54
+ """
55
+
56
+ SUB_COMMAND = 1
57
+ """Denotes a command option where the value will be a sub command."""
58
+
59
+ SUB_COMMAND_GROUP = 2
60
+ """Denotes a command option where the value will be a sub command group."""
61
+
62
+ STRING = 3
63
+ """Denotes a command option where the value will be a string."""
64
+
65
+ INTEGER = 4
66
+ """Denotes a command option where the value will be a int.
67
+
68
+ This is range limited between -2^53 and 2^53.
69
+ """
70
+
71
+ BOOLEAN = 5
72
+ """Denotes a command option where the value will be a bool."""
73
+
74
+ USER = 6
75
+ """Denotes a command option where the value will be resolved to a user."""
76
+
77
+ CHANNEL = 7
78
+ """Denotes a command option where the value will be resolved to a channel."""
79
+
80
+ ROLE = 8
81
+ """Denotes a command option where the value will be resolved to a role."""
82
+
83
+ MENTIONABLE = 9
84
+ """Denotes a command option where the value will be a snowflake ID."""
85
+
86
+ FLOAT = 10
87
+ """Denotes a command option where the value will be a float.
88
+
89
+ This is range limited between -2^53 and 2^53.
90
+ """
91
+
92
+ ATTACHMENT = 11
93
+ """Denotes a command option where the value will be an attachment."""
94
+
95
+ @classmethod
96
+ def from_hikari(cls, option_type: hikari.OptionType) -> OptionType:
97
+ """Convert a hikari.OptionType to an OptionType."""
98
+ return cls(option_type.value)
99
+
100
+ def to_hikari(self) -> hikari.OptionType:
101
+ """Convert an OptionType to a hikari.OptionType."""
102
+ # TODO: Map custom option types to their respective hikari.OptionType
103
+ return hikari.OptionType(self.value)
104
+
105
+ # TODO: When adding custom convertible option types, add them with an offset of 1000 or so
106
+
107
+
40
108
  class OptionParams(t.Generic[T]):
41
109
  """The base class for all option parameters objects.
42
110
 
@@ -153,13 +221,13 @@ class OptionBase(abc.ABC, t.Generic[T]):
153
221
 
154
222
  @property
155
223
  @abc.abstractmethod
156
- def option_type(self) -> hikari.OptionType:
224
+ def option_type(self) -> OptionType:
157
225
  """The type of the option. Used to register the command."""
158
226
 
159
227
  def _to_dict(self) -> dict[str, t.Any]:
160
228
  """Convert the option to a dictionary of kwargs that can be passed to hikari.CommandOption."""
161
229
  return {
162
- "type": self.option_type,
230
+ "type": self.option_type.to_hikari(),
163
231
  "name": self.name,
164
232
  "description": self.description,
165
233
  "autocomplete": False,
arc/abc/plugin.py CHANGED
@@ -12,6 +12,7 @@ from arc.abc.command import _CommandSettings
12
12
  from arc.abc.error_handler import HasErrorHandler
13
13
  from arc.abc.hookable import Hookable
14
14
  from arc.command import MessageCommand, SlashCommand, SlashGroup, UserCommand
15
+ from arc.command.slash import SlashSubCommand, SlashSubGroup
15
16
  from arc.context import AutodeferMode, Context
16
17
  from arc.internal.types import BuilderT, ClientT, ErrorHandlerCallbackT, HookT, PostHookT, SlashCommandLike
17
18
 
@@ -368,6 +369,97 @@ class PluginBase(HasErrorHandler[ClientT], Hookable[ClientT]):
368
369
 
369
370
  return decorator
370
371
 
372
+ @t.overload
373
+ def walk_commands(
374
+ self, command_type: t.Literal[hikari.CommandType.USER], *, callable_only: bool = False
375
+ ) -> t.Iterator[UserCommand[ClientT]]:
376
+ ...
377
+
378
+ @t.overload
379
+ def walk_commands(
380
+ self, command_type: t.Literal[hikari.CommandType.MESSAGE], *, callable_only: bool = False
381
+ ) -> t.Iterator[MessageCommand[ClientT]]:
382
+ ...
383
+
384
+ @t.overload
385
+ def walk_commands(
386
+ self, command_type: t.Literal[hikari.CommandType.SLASH], *, callable_only: t.Literal[False]
387
+ ) -> t.Iterator[SlashCommand[ClientT] | SlashSubCommand[ClientT] | SlashGroup[ClientT] | SlashSubGroup[ClientT]]:
388
+ ...
389
+
390
+ @t.overload
391
+ def walk_commands(
392
+ self, command_type: t.Literal[hikari.CommandType.SLASH], *, callable_only: t.Literal[True]
393
+ ) -> t.Iterator[SlashCommand[ClientT] | SlashSubCommand[ClientT]]:
394
+ ...
395
+
396
+ def walk_commands( # noqa: C901
397
+ self, command_type: hikari.CommandType, *, callable_only: bool = False
398
+ ) -> t.Iterator[t.Any]:
399
+ """Iterate over all commands of a certain type added to this plugin.
400
+
401
+ Parameters
402
+ ----------
403
+ command_type : hikari.CommandType
404
+ The type of commands to return.
405
+ callable_only : bool
406
+ Whether to only return commands that are directly callable.
407
+ If True, command groups and subgroups will be skipped.
408
+
409
+ Yields
410
+ ------
411
+ CommandT[ClientT]
412
+ The next command that matches the given criteria.
413
+
414
+ Usage
415
+ -----
416
+ ```py
417
+ for cmd in plugin.walk_commands(hikari.CommandType.SLASH):
418
+ print(cmd.name)
419
+ ```
420
+
421
+ !!! tip
422
+ To iterate over all types of commands, you may use [`itertools.chain()`][itertools.chain]:
423
+
424
+ ```py
425
+ import itertools
426
+
427
+ for cmd in itertools.chain(
428
+ plugin.walk_commands(hikari.CommandType.SLASH),
429
+ plugin.walk_commands(hikari.CommandType.MESSAGE),
430
+ plugin.walk_commands(hikari.CommandType.USER),
431
+ ):
432
+ print(cmd.name)
433
+ ```
434
+ """
435
+ if hikari.CommandType.SLASH is command_type:
436
+ for command in self._slash_commands.values():
437
+ if isinstance(command, SlashCommand):
438
+ yield command
439
+ continue
440
+
441
+ if not callable_only:
442
+ yield command
443
+
444
+ for sub in command.children.values():
445
+ if isinstance(sub, SlashSubCommand):
446
+ yield sub
447
+ continue
448
+
449
+ if not callable_only:
450
+ yield sub
451
+
452
+ for subsub in sub.children.values():
453
+ yield subsub
454
+
455
+ elif hikari.CommandType.MESSAGE is command_type:
456
+ for command in self._message_commands.values():
457
+ yield command
458
+
459
+ elif hikari.CommandType.USER is command_type:
460
+ for command in self._user_commands.values():
461
+ yield command
462
+
371
463
 
372
464
  # MIT License
373
465
  #