Skip to content

Commit a30f76d

Browse files
BobDotComVincentRPSkrittickEmmmaTechDorukyum
authored
discord.ext.bridge (#1131)
* fix: add sinks to packages * Change register_command to use RuntimeError * Initial ext.compat commit * Remove extra quot. mark * Add support for AutoShardedBot * Add ext.compat to packages * Fix imports * Fix AutoShardedBot docstring * Fix discord.Bot.application_command docstring * Fix docstring again * Fix imports * Documentation * Remove unused import * fix imports * Change register_command to use RuntimeError * Initial ext.compat commit * Remove extra quot. mark * Add support for AutoShardedBot * Add ext.compat to packages * Fix imports * Fix AutoShardedBot docstring * Fix discord.Bot.application_command docstring * Fix docstring again * Fix imports * Documentation * Remove unused import * fix imports * Remove followup alias * change typing to trigger_typing * add edit method * Fix typos * Fix typo * Fix typo * Fix mentionable option type * Support different option types besides str * Fix 3.10 support for Union in options * rename ext-compat to ext-bridge (and related objects) * *Actually* fix mentionable option types * chore: add cog support for Bridge Commands * chore: use a getattr test instead of a TypeVar test * Fix bridge option choice conversion * Search for choice names as well * Rename BridgeOption * Add example * feat: add ext.bridge docs (#1222) * feat: add ext.bridge docs * chore: requested changes * Update docs/ext/bridge/index.rst Co-authored-by: Dorukyum <[email protected]> Co-authored-by: Dorukyum <[email protected]> Co-authored-by: Vincent <[email protected]> Co-authored-by: Krittick <[email protected]> Co-authored-by: EmreTech <[email protected]> Co-authored-by: Dorukyum <[email protected]>
1 parent aab6623 commit a30f76d

File tree

13 files changed

+637
-5
lines changed

13 files changed

+637
-5
lines changed

discord/bot.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -376,7 +376,7 @@ async def register_command(
376376
The command that was registered
377377
"""
378378
# TODO: Write this
379-
raise NotImplementedError("This function has not been implemented yet")
379+
raise RuntimeError("This function has not been implemented yet")
380380

381381
async def register_commands(
382382
self,
@@ -899,7 +899,7 @@ def message_command(self, **kwargs):
899899

900900
def application_command(self, **kwargs):
901901
"""A shortcut decorator that invokes :func:`command` and adds it to
902-
the internal command list via :meth:`add_application_command`.
902+
the internal command list via :meth:`~.Bot.add_application_command`.
903903
904904
.. versionadded:: 2.0
905905

discord/cog.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,23 @@ def __new__(cls: Type[CogMeta], *args: Any, **kwargs: Any) -> CogMeta:
190190
if elem.startswith(("cog_", "bot_")):
191191
raise TypeError(no_bot_cog.format(base, elem))
192192
commands[elem] = value
193-
elif inspect.iscoroutinefunction(value):
193+
194+
try:
195+
# a test to see if this value is a BridgeCommand
196+
getattr(value, "add_to")
197+
198+
if is_static_method:
199+
raise TypeError(f"Command in method {base}.{elem!r} must not be staticmethod.")
200+
if elem.startswith(("cog_", "bot_")):
201+
raise TypeError(no_bot_cog.format(base, elem))
202+
203+
commands["ext_" + elem] = value.get_ext_command()
204+
commands["application_" + elem] = value.get_application_command()
205+
except AttributeError:
206+
# we are confident that the value is not a Bridge Command
207+
pass
208+
209+
if inspect.iscoroutinefunction(value):
194210
try:
195211
getattr(value, "__cog_listener__")
196212
except AttributeError:

discord/enums.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525

2626
import types
2727
from collections import namedtuple
28-
from typing import TYPE_CHECKING, Any, ClassVar, Dict, List, Optional, Type, TypeVar
28+
from typing import TYPE_CHECKING, Any, ClassVar, Dict, List, Optional, Type, TypeVar, Union
2929

3030
__all__ = (
3131
"Enum",
@@ -669,6 +669,11 @@ def from_datatype(cls, datatype):
669669
if datatype.__name__ == "Mentionable":
670670
return cls.mentionable
671671

672+
if isinstance(datatype, types.UnionType) or getattr(datatype, "__origin__", None) is Union:
673+
# Python 3.10+ "|" operator or typing.Union has been used. The __args__ attribute is a tuple of the types.
674+
# Type checking fails for this case, so ignore it.
675+
return cls.from_datatype(datatype.__args__) # type: ignore
676+
672677
if issubclass(datatype, str):
673678
return cls.string
674679
if issubclass(datatype, bool):

discord/ext/bridge/__init__.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
"""
2+
The MIT License (MIT)
3+
4+
Copyright (c) 2015-2021 Rapptz
5+
Copyright (c) 2021-present Pycord Development
6+
7+
Permission is hereby granted, free of charge, to any person obtaining a
8+
copy of this software and associated documentation files (the "Software"),
9+
to deal in the Software without restriction, including without limitation
10+
the rights to use, copy, modify, merge, publish, distribute, sublicense,
11+
and/or sell copies of the Software, and to permit persons to whom the
12+
Software is furnished to do so, subject to the following conditions:
13+
14+
The above copyright notice and this permission notice shall be included in
15+
all copies or substantial portions of the Software.
16+
17+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
18+
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
22+
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
23+
DEALINGS IN THE SOFTWARE.
24+
"""
25+
26+
from .core import *
27+
from .context import *
28+
from .bot import *

discord/ext/bridge/bot.py

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
"""
2+
The MIT License (MIT)
3+
4+
Copyright (c) 2015-2021 Rapptz
5+
Copyright (c) 2021-present Pycord Development
6+
7+
Permission is hereby granted, free of charge, to any person obtaining a
8+
copy of this software and associated documentation files (the "Software"),
9+
to deal in the Software without restriction, including without limitation
10+
the rights to use, copy, modify, merge, publish, distribute, sublicense,
11+
and/or sell copies of the Software, and to permit persons to whom the
12+
Software is furnished to do so, subject to the following conditions:
13+
14+
The above copyright notice and this permission notice shall be included in
15+
all copies or substantial portions of the Software.
16+
17+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
18+
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
22+
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
23+
DEALINGS IN THE SOFTWARE.
24+
"""
25+
from abc import ABC
26+
27+
from discord.interactions import Interaction
28+
from discord.message import Message
29+
30+
from ..commands import AutoShardedBot as ExtAutoShardedBot
31+
from ..commands import Bot as ExtBot
32+
from .context import BridgeApplicationContext, BridgeExtContext
33+
from .core import BridgeCommand, bridge_command
34+
35+
__all__ = ("Bot", "AutoShardedBot")
36+
37+
38+
class BotBase(ABC):
39+
async def get_application_context(self, interaction: Interaction, cls=None) -> BridgeApplicationContext:
40+
cls = cls if cls is not None else BridgeApplicationContext
41+
# Ignore the type hinting error here. BridgeApplicationContext is a subclass of ApplicationContext, and since
42+
# we gave it cls, it will be used instead.
43+
return await super().get_application_context(interaction, cls=cls) # type: ignore
44+
45+
async def get_context(self, message: Message, cls=None) -> BridgeExtContext:
46+
cls = cls if cls is not None else BridgeExtContext
47+
# Ignore the type hinting error here. BridgeExtContext is a subclass of Context, and since we gave it cls, it
48+
# will be used instead.
49+
return await super().get_context(message, cls=cls) # type: ignore
50+
51+
def add_bridge_command(self, command: BridgeCommand):
52+
"""Takes a :class:`.BridgeCommand` and adds both a slash and traditional (prefix-based) version of the command
53+
to the bot.
54+
"""
55+
# Ignore the type hinting error here. All subclasses of BotBase pass the type checks.
56+
command.add_to(self) # type: ignore
57+
58+
def bridge_command(self, **kwargs):
59+
"""A shortcut decorator that invokes :func:`.bridge_command` and adds it to
60+
the internal command list via :meth:`~.Bot.add_bridge_command`.
61+
62+
Returns
63+
--------
64+
Callable[..., :class:`BridgeCommand`]
65+
A decorator that converts the provided method into an :class:`.BridgeCommand`, adds both a slash and
66+
traditional (prefix-based) version of the command to the bot, and returns the :class:`.BridgeCommand`.
67+
"""
68+
69+
def decorator(func) -> BridgeCommand:
70+
result = bridge_command(**kwargs)(func)
71+
self.add_bridge_command(result)
72+
return result
73+
74+
return decorator
75+
76+
77+
class Bot(BotBase, ExtBot):
78+
"""Represents a discord bot, with support for cross-compatibility between command types.
79+
80+
This class is a subclass of :class:`.ext.commands.Bot` and as a result
81+
anything that you can do with a :class:`.ext.commands.Bot` you can do with
82+
this bot.
83+
84+
.. versionadded:: 2.0
85+
"""
86+
87+
pass
88+
89+
90+
class AutoShardedBot(BotBase, ExtAutoShardedBot):
91+
"""This is similar to :class:`.Bot` except that it is inherited from
92+
:class:`.ext.commands.AutoShardedBot` instead.
93+
94+
.. versionadded:: 2.0
95+
"""
96+
97+
pass

discord/ext/bridge/context.py

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
"""
2+
The MIT License (MIT)
3+
4+
Copyright (c) 2015-2021 Rapptz
5+
Copyright (c) 2021-present Pycord Development
6+
7+
Permission is hereby granted, free of charge, to any person obtaining a
8+
copy of this software and associated documentation files (the "Software"),
9+
to deal in the Software without restriction, including without limitation
10+
the rights to use, copy, modify, merge, publish, distribute, sublicense,
11+
and/or sell copies of the Software, and to permit persons to whom the
12+
Software is furnished to do so, subject to the following conditions:
13+
14+
The above copyright notice and this permission notice shall be included in
15+
all copies or substantial portions of the Software.
16+
17+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
18+
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
22+
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
23+
DEALINGS IN THE SOFTWARE.
24+
"""
25+
from abc import ABC, abstractmethod
26+
from typing import TYPE_CHECKING, Any, Optional, Union
27+
28+
from discord.commands import ApplicationContext
29+
from discord.interactions import Interaction, InteractionMessage
30+
from discord.message import Message
31+
from discord.webhook import WebhookMessage
32+
33+
from ..commands import Context
34+
35+
__all__ = ("BridgeContext", "BridgeExtContext", "BridgeApplicationContext")
36+
37+
38+
class BridgeContext(ABC):
39+
"""
40+
The base context class for compatibility commands. This class is an :class:`ABC` (abstract base class), which is
41+
subclassed by :class:`BridgeExtContext` and :class:`BridgeApplicationContext`. The methods in this class are meant
42+
to give parity between the two contexts, while still allowing for all of their functionality.
43+
44+
When this is passed to a command, it will either be passed as :class:`BridgeExtContext`, or
45+
:class:`BridgeApplicationContext`. Since they are two separate classes, it is quite simple to use :meth:`isinstance`
46+
to make different functionality for each context. For example, if you want to respond to a command with the command
47+
type that it was invoked with, you can do the following:
48+
49+
.. code-block:: python3
50+
51+
@bot.bridge_command()
52+
async def example(ctx: BridgeContext):
53+
if isinstance(ctx, BridgeExtContext):
54+
command_type = "Traditional (prefix-based) command"
55+
elif isinstance(ctx, BridgeApplicationContext):
56+
command_type = "Application command"
57+
await ctx.send(f"This command was invoked with a(n) {command_type}.")
58+
59+
.. versionadded:: 2.0
60+
"""
61+
62+
@abstractmethod
63+
async def _respond(self, *args, **kwargs) -> Union[Union[Interaction, WebhookMessage], Message]:
64+
...
65+
66+
@abstractmethod
67+
async def _defer(self, *args, **kwargs) -> None:
68+
...
69+
70+
@abstractmethod
71+
async def _edit(self, *args, **kwargs) -> Union[InteractionMessage, Message]:
72+
...
73+
74+
async def respond(self, *args, **kwargs) -> Union[Union[Interaction, WebhookMessage], Message]:
75+
"""|coro|
76+
77+
Responds to the command with the respective response type to the current context. In :class:`BridgeExtContext`,
78+
this will be :meth:`~.ExtContext.reply` while in :class:`BridgeApplicationContext`, this will be
79+
:meth:`~.ApplicationContext.respond`.
80+
"""
81+
return await self._respond(*args, **kwargs)
82+
83+
async def reply(self, *args, **kwargs) -> Union[Union[Interaction, WebhookMessage], Message]:
84+
"""|coro|
85+
86+
Alias for :meth:`~.BridgeContext.respond`.
87+
"""
88+
return await self.respond(*args, **kwargs)
89+
90+
async def defer(self, *args, **kwargs) -> None:
91+
"""|coro|
92+
93+
Defers the command with the respective approach to the current context. In :class:`BridgeExtContext`, this will
94+
be :meth:`~.ExtContext.trigger_typing` while in :class:`BridgeApplicationContext`, this will be
95+
:meth:`~.ApplicationContext.defer`.
96+
97+
.. note::
98+
There is no ``trigger_typing`` alias for this method. ``trigger_typing`` will always provide the same
99+
functionality across contexts.
100+
"""
101+
return await self._defer(*args, **kwargs)
102+
103+
async def edit(self, *args, **kwargs) -> Union[InteractionMessage, Message]:
104+
"""|coro|
105+
106+
Edits the original response message with the respective approach to the current context. In
107+
:class:`BridgeExtContext`, this will have a custom approach where :meth:`.respond` caches the message to be
108+
edited here. In :class:`BridgeApplicationContext`, this will be :meth:`~.ApplicationContext.edit`.
109+
"""
110+
return await self._edit(*args, **kwargs)
111+
112+
def _get_super(self, attr: str) -> Optional[Any]:
113+
return getattr(super(), attr)
114+
115+
116+
class BridgeApplicationContext(BridgeContext, ApplicationContext):
117+
"""
118+
The application context class for compatibility commands. This class is a subclass of :class:`BridgeContext` and
119+
:class:`ApplicationContext`. This class is meant to be used with :class:`BridgeCommand`.
120+
121+
.. versionadded:: 2.0
122+
"""
123+
124+
async def _respond(self, *args, **kwargs) -> Union[Interaction, WebhookMessage]:
125+
return await self._get_super("respond")(*args, **kwargs)
126+
127+
async def _defer(self, *args, **kwargs) -> None:
128+
return await self._get_super("defer")(*args, **kwargs)
129+
130+
async def _edit(self, *args, **kwargs) -> InteractionMessage:
131+
return await self._get_super("edit")(*args, **kwargs)
132+
133+
134+
class BridgeExtContext(BridgeContext, Context):
135+
"""
136+
The ext.commands context class for compatibility commands. This class is a subclass of :class:`BridgeContext` and
137+
:class:`Context`. This class is meant to be used with :class:`BridgeCommand`.
138+
139+
.. versionadded:: 2.0
140+
"""
141+
142+
def __init__(self, *args, **kwargs):
143+
super().__init__(*args, **kwargs)
144+
self._original_response_message: Optional[Message] = None
145+
146+
async def _respond(self, *args, **kwargs) -> Message:
147+
message = await self._get_super("reply")(*args, **kwargs)
148+
if self._original_response_message == None:
149+
self._original_response_message = message
150+
return message
151+
152+
async def _defer(self, *args, **kwargs) -> None:
153+
return await self._get_super("trigger_typing")(*args, **kwargs)
154+
155+
async def _edit(self, *args, **kwargs) -> Message:
156+
return await self._original_response_message.edit(*args, **kwargs)
157+
158+
159+
if TYPE_CHECKING:
160+
# This is a workaround for mypy not being able to resolve the type of BridgeCommand.
161+
class BridgeContext(ApplicationContext, Context):
162+
...

0 commit comments

Comments
 (0)