From 06faf554d20eb88396a72742c29c414db2124e1e Mon Sep 17 00:00:00 2001 From: Catalyst4 <84055084+Catalyst4222@users.noreply.github.com> Date: Wed, 12 Jan 2022 23:04:55 -0500 Subject: [PATCH 01/21] feat: create basic functionality for Extensions Expands Extension.__new__ and adds new functions to mimic client decorators --- interactions/client.py | 81 +++++++++++++++++++++++++++++++++-------- interactions/client.pyi | 15 +++++++- 2 files changed, 80 insertions(+), 16 deletions(-) diff --git a/interactions/client.py b/interactions/client.py index e65996f6c..ab71b4cd4 100644 --- a/interactions/client.py +++ b/interactions/client.py @@ -1,5 +1,7 @@ +import functools +import inspect import sys -from asyncio import get_event_loop +from asyncio import get_event_loop, iscoroutinefunction from importlib import import_module from importlib.util import resolve_name from logging import Logger, getLogger @@ -722,25 +724,74 @@ def __init__(self, client): async def cog_user_cmd(self, ctx): ... - def setup(bot): - CoolCode(bot) + def setup(client): + CoolCode(client) """ client: Client commands: Optional[List[ApplicationCommand]] listeners: Optional[List[Listener]] - def __new__(cls, bot: Client) -> None: - cls.client = bot - cls.commands = [] - cls.listeners = [] + def __new__(cls, client: Client, *args, **kwargs) -> "Extension": - for _, content in cls.__dict__.items(): - if not content.startswith("__") or content.startswith("_"): - if "on_" in content: - cls.listeners.append(content) - else: - cls.commands.append(content) + self = super().__new__(cls) + + self.client = client + self._commands = [] + self._listeners = [] + self._components = [] + + # This gets every coroutine in a way that we can easily change them + # cls + for name, func in inspect.getmembers(cls, predicate=iscoroutinefunction): + if not name.startswith("__") and not name.startswith("_"): # Keep "hidden" methods + partial = functools.partial( + func, self + ) # If you have a better solution, feel free to do it + partial.__name__ = func.__name__ + partial.__code__ = func.__code__ + + if hasattr(func, "__listener_name__"): # set by extension_listener + partial = client.event( + partial, name=func.__listener_name__ + ) # capture the return value for friendlier ext-ing + self._listeners.append(partial) + + if hasattr(func, "__command_data__"): # Set by extension_command + args, kwargs = func.__command_data__ + partial = client.command(*args, **kwargs)(partial) + self._commands.append(partial) + + if hasattr(func, "__component_data__"): + args, kwargs = func.__component_data__ + partial = client.component(*args, **kwargs) + self._components.append(partial) + + return self + + +@functools.wraps(command) +def extension_command(*args, **kwargs): + def decorator(coro): + coro.__command_data__ = (args, kwargs) + return coro + + return decorator + + +def extension_listener(name=None): + def decorator(func): + func.__listener_name__ = name or func.__name__ + + return func + + return decorator + + +@functools.wraps(Client.component) +def extension_component(*args, **kwargs): + def decorator(func): + func.__component_data__ = (args, kwargs) + return func - for _command in cls.commands: - cls.client.command(**_command) + return decorator diff --git a/interactions/client.pyi b/interactions/client.pyi index 61ef9afaf..9091563b3 100644 --- a/interactions/client.pyi +++ b/interactions/client.pyi @@ -61,7 +61,20 @@ class Client: async def raw_channel_create(self, message) -> dict: ... async def raw_message_create(self, message) -> dict: ... async def raw_guild_create(self, guild) -> dict: ... + def add_extension(self, extension: "Extension"): ... class Extension: client: Client - def __new__(cls, bot: Client) -> None: ... + def __new__(cls, client: Client, *args, **kwargs) -> Extension: ... + +def extension_command( + *, + type: Optional[Union[int, ApplicationCommandType]] = ApplicationCommandType.CHAT_INPUT, + name: Optional[str] = None, + description: Optional[str] = None, + scope: Optional[Union[int, Guild, List[int], List[Guild]]] = None, + options: Optional[Union[Dict[str, Any], List[Dict[str, Any]], Option, List[Option]]] = None, + default_permission: Optional[bool] = None, +): ... +def extension_listener(name=None) -> Callable[..., Any]: ... +def extension_component(component: Union[Button, SelectMenu]) -> Callable[..., Any]: ... \ No newline at end of file From 12d5c4cb4ff1cf0b987e7628b81c1e707a21d2d8 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 13 Jan 2022 04:12:16 +0000 Subject: [PATCH 02/21] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- interactions/client.pyi | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interactions/client.pyi b/interactions/client.pyi index 9091563b3..417c649e7 100644 --- a/interactions/client.pyi +++ b/interactions/client.pyi @@ -77,4 +77,4 @@ def extension_command( default_permission: Optional[bool] = None, ): ... def extension_listener(name=None) -> Callable[..., Any]: ... -def extension_component(component: Union[Button, SelectMenu]) -> Callable[..., Any]: ... \ No newline at end of file +def extension_component(component: Union[Button, SelectMenu]) -> Callable[..., Any]: ... From 94ef8b0368ca5e8448fbe1681660efcd83c45d81 Mon Sep 17 00:00:00 2001 From: Catalyst4 <84055084+Catalyst4222@users.noreply.github.com> Date: Thu, 13 Jan 2022 23:15:23 -0500 Subject: [PATCH 03/21] feat: allow the creation of commands after starting the event loop --- interactions/client.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/interactions/client.py b/interactions/client.py index ab71b4cd4..e24c8012e 100644 --- a/interactions/client.py +++ b/interactions/client.py @@ -351,7 +351,10 @@ def decorator(coro: Coroutine) -> Callable[..., Any]: ) if self.automate_sync: - [self.loop.run_until_complete(self.synchronize(command)) for command in commands] + if self.loop.is_running(): + [self.loop.create_task(self.synchronize(command)) for command in commands] + else: + [self.loop.run_until_complete(self.synchronize(command)) for command in commands] return self.event(coro, name=f"command_{name}") @@ -408,7 +411,10 @@ def decorator(coro: Coroutine) -> Callable[..., Any]: ) if self.automate_sync: - [self.loop.run_until_complete(self.synchronize(command)) for command in commands] + if self.loop.is_running(): + [self.loop.create_task(self.synchronize(command)) for command in commands] + else: + [self.loop.run_until_complete(self.synchronize(command)) for command in commands] return self.event(coro, name=f"command_{name}") @@ -465,7 +471,10 @@ def decorator(coro: Coroutine) -> Callable[..., Any]: ) if self.automate_sync: - [self.loop.run_until_complete(self.synchronize(command)) for command in commands] + if self.loop.is_running(): + [self.loop.create_task(self.synchronize(command)) for command in commands] + else: + [self.loop.run_until_complete(self.synchronize(command)) for command in commands] return self.event(coro, name=f"command_{name}") From caab8dee2fd5e0f40d2a037e5a1c9302d267e06b Mon Sep 17 00:00:00 2001 From: Catalyst4 <84055084+Catalyst4222@users.noreply.github.com> Date: Thu, 13 Jan 2022 23:35:08 -0500 Subject: [PATCH 04/21] feat: add the ability to unload a cog, and remove the commands when doing so --- interactions/client.py | 157 +++++++++++++++++++++++++++++----------- interactions/client.pyi | 6 +- 2 files changed, 121 insertions(+), 42 deletions(-) diff --git a/interactions/client.py b/interactions/client.py index e24c8012e..c9596b6b2 100644 --- a/interactions/client.py +++ b/interactions/client.py @@ -1,6 +1,8 @@ +import asyncio import functools import inspect import sys +import types from asyncio import get_event_loop, iscoroutinefunction from importlib import import_module from importlib.util import resolve_name @@ -354,7 +356,10 @@ def decorator(coro: Coroutine) -> Callable[..., Any]: if self.loop.is_running(): [self.loop.create_task(self.synchronize(command)) for command in commands] else: - [self.loop.run_until_complete(self.synchronize(command)) for command in commands] + [ + self.loop.run_until_complete(self.synchronize(command)) + for command in commands + ] return self.event(coro, name=f"command_{name}") @@ -414,7 +419,10 @@ def decorator(coro: Coroutine) -> Callable[..., Any]: if self.loop.is_running(): [self.loop.create_task(self.synchronize(command)) for command in commands] else: - [self.loop.run_until_complete(self.synchronize(command)) for command in commands] + [ + self.loop.run_until_complete(self.synchronize(command)) + for command in commands + ] return self.event(coro, name=f"command_{name}") @@ -474,7 +482,10 @@ def decorator(coro: Coroutine) -> Callable[..., Any]: if self.loop.is_running(): [self.loop.create_task(self.synchronize(command)) for command in commands] else: - [self.loop.run_until_complete(self.synchronize(command)) for command in commands] + [ + self.loop.run_until_complete(self.synchronize(command)) + for command in commands + ] return self.event(coro, name=f"command_{name}") @@ -607,7 +618,9 @@ def load(self, name: str, package: Optional[str] = None) -> None: if _name in self.extensions: log.error(f"Extension {name} has already been loaded. Skipping.") - module = import_module(name, package) + module = import_module( + name, package + ) # should be a module, because Extensions just need to be __init__-ed try: setup = getattr(module, "setup") @@ -628,16 +641,34 @@ def remove(self, name: str, package: Optional[str] = None) -> None: :param package?: The package of the extension. :type package: Optional[str] """ - _name: str = resolve_name(name, package) - module = self.extensions.get(_name) + try: + _name: str = resolve_name(name, package) + except AttributeError: + _name = name - if module not in self.extensions: + extension = self.extensions.get(_name) + + if _name not in self.extensions: log.error(f"Extension {name} has not been loaded before. Skipping.") + return + + try: + extension.teardown() # made for Extension, usable by others + except AttributeError: + pass + + if isinstance(extension, types.ModuleType): # loaded as a module + for ext_name, ext in inspect.getmembers( + extension, lambda x: isinstance(x, type) and issubclass(x, Extension) + ): + self.remove(ext_name) + + del sys.modules[_name] - log.debug(f"Removed extension {name}.") - del sys.modules[_name] del self.extensions[_name] + log.debug(f"Removed extension {name}.") + def reload(self, name: str, package: Optional[str] = None) -> None: """ "Reloads" an extension off of current client from an import resolve. @@ -648,11 +679,12 @@ def reload(self, name: str, package: Optional[str] = None) -> None: :type package: Optional[str] """ _name: str = resolve_name(name, package) - module = self.extensions.get(_name) + extension = self.extensions.get(_name) - if module is None: + if extension is None: log.warning(f"Extension {name} could not be reloaded because it was never loaded.") - self.extend(name, package) + self.load(name, package) + return self.remove(name, package) self.load(name, package) @@ -738,46 +770,89 @@ def setup(client): """ client: Client - commands: Optional[List[ApplicationCommand]] - listeners: Optional[List[Listener]] def __new__(cls, client: Client, *args, **kwargs) -> "Extension": self = super().__new__(cls) self.client = client - self._commands = [] - self._listeners = [] - self._components = [] + self._commands = {} + self._listeners = {} + self._components = {} # This gets every coroutine in a way that we can easily change them # cls - for name, func in inspect.getmembers(cls, predicate=iscoroutinefunction): - if not name.startswith("__") and not name.startswith("_"): # Keep "hidden" methods - partial = functools.partial( - func, self - ) # If you have a better solution, feel free to do it - partial.__name__ = func.__name__ - partial.__code__ = func.__code__ - - if hasattr(func, "__listener_name__"): # set by extension_listener - partial = client.event( - partial, name=func.__listener_name__ - ) # capture the return value for friendlier ext-ing - self._listeners.append(partial) - - if hasattr(func, "__command_data__"): # Set by extension_command - args, kwargs = func.__command_data__ - partial = client.command(*args, **kwargs)(partial) - self._commands.append(partial) - - if hasattr(func, "__component_data__"): - args, kwargs = func.__component_data__ - partial = client.component(*args, **kwargs) - self._components.append(partial) + for name, func in inspect.getmembers(self, predicate=iscoroutinefunction): + + # TODO we can make these all share the same list, might make it easier to load/unload + if hasattr(func, "__listener_name__"): # set by extension_listener + func = client.event( + func, name=func.__listener_name__ + ) # capture the return value for friendlier ext-ing + + listeners = self._listeners.get(func.__listener_name__, []) + listeners.append(func) + self._listeners[func.__listener_name__] = listeners + + if hasattr(func, "__command_data__"): # Set by extension_command + args, kwargs = func.__command_data__ + func = client.command(*args, **kwargs)(func) + + cmd_name = f"command_{kwargs.get('name') or func.__name__}" + + commands = self._commands.get(cmd_name, []) + commands.append(func) + self._commands[cmd_name] = commands + + if hasattr(func, "__component_data__"): + args, kwargs = func.__component_data__ + func = client.component(*args, **kwargs)(func) + + component = kwargs.get("component") or args[0] + comp_name = ( + _component(component).custom_id + if isinstance(component, (Button, SelectMenu)) + else component + ) + comp_name = f"component_{comp_name}" + + components = self._components.get(comp_name, []) + components.append(func) + self._components[comp_name] = components + + client.extensions[cls.__name__] = self return self + def teardown(self): + for event, funcs in self._listeners.items(): + for func in funcs: + self.client.websocket.dispatch.events[event].remove(func) + + for component, funcs in self._components.items(): + for func in funcs: + self.client.websocket.dispatch.events[component].remove(func) + + for cmd, funcs in self._commands.items(): + for func in funcs: + self.client.websocket.dispatch.events[cmd].remove(func) + + clean_cmd_names = ["_".join(cmd.split("_")[1:]) for cmd in self._commands.keys()] + cmds = filter( + lambda x: x["name"] in clean_cmd_names, + self.client.http.cache.interactions.view, + ) + + if self.client.automate_sync: + [ + self.client.loop.create_task( + self.client.http.delete_application_command( + cmd["application_id"], cmd["id"], cmd["guild_id"] + ) + ) + for cmd in cmds + ] + @functools.wraps(command) def extension_command(*args, **kwargs): @@ -797,7 +872,7 @@ def decorator(func): return decorator -@functools.wraps(Client.component) +# @functools.wraps(Client.component) def extension_component(*args, **kwargs): def decorator(func): func.__component_data__ = (args, kwargs) diff --git a/interactions/client.pyi b/interactions/client.pyi index 9091563b3..ebcbbd1c9 100644 --- a/interactions/client.pyi +++ b/interactions/client.pyi @@ -65,7 +65,11 @@ class Client: class Extension: client: Client + _commands: dict + _listeners: dict + _components: dict def __new__(cls, client: Client, *args, **kwargs) -> Extension: ... + def teardown(self) -> None: ... def extension_command( *, @@ -77,4 +81,4 @@ def extension_command( default_permission: Optional[bool] = None, ): ... def extension_listener(name=None) -> Callable[..., Any]: ... -def extension_component(component: Union[Button, SelectMenu]) -> Callable[..., Any]: ... \ No newline at end of file +def extension_component(component: Union[Button, SelectMenu]) -> Callable[..., Any]: ... From 17076cfd1a051570d2121f040175d3527f247686 Mon Sep 17 00:00:00 2001 From: Catalyst4 <84055084+Catalyst4222@users.noreply.github.com> Date: Fri, 14 Jan 2022 16:50:33 -0500 Subject: [PATCH 05/21] chore: remove unused imports for flake8 --- interactions/client.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/interactions/client.py b/interactions/client.py index c9596b6b2..439160e71 100644 --- a/interactions/client.py +++ b/interactions/client.py @@ -1,4 +1,3 @@ -import asyncio import functools import inspect import sys @@ -9,7 +8,6 @@ from logging import Logger, getLogger from typing import Any, Callable, Coroutine, Dict, List, Optional, Union -from interactions.api.dispatch import Listener from interactions.api.models.misc import Snowflake from .api.cache import Cache From 691739f6a2c675f07344b96dca37ff28d8c21e3d Mon Sep 17 00:00:00 2001 From: Catalyst4 <84055084+Catalyst4222@users.noreply.github.com> Date: Fri, 14 Jan 2022 21:52:29 -0500 Subject: [PATCH 06/21] style: add in suggestions from #428 --- interactions/client.py | 21 ++++++++++----------- interactions/client.pyi | 1 - 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/interactions/client.py b/interactions/client.py index 439160e71..3a49182ea 100644 --- a/interactions/client.py +++ b/interactions/client.py @@ -1,15 +1,14 @@ -import functools -import inspect import sys -import types from asyncio import get_event_loop, iscoroutinefunction +from functools import wraps +from types import ModuleType from importlib import import_module from importlib.util import resolve_name +from inspect import getmembers from logging import Logger, getLogger from typing import Any, Callable, Coroutine, Dict, List, Optional, Union from interactions.api.models.misc import Snowflake - from .api.cache import Cache from .api.cache import Item as Build from .api.error import InteractionException @@ -655,8 +654,8 @@ def remove(self, name: str, package: Optional[str] = None) -> None: except AttributeError: pass - if isinstance(extension, types.ModuleType): # loaded as a module - for ext_name, ext in inspect.getmembers( + if isinstance(extension, ModuleType): # loaded as a module + for ext_name, ext in getmembers( extension, lambda x: isinstance(x, type) and issubclass(x, Extension) ): self.remove(ext_name) @@ -780,7 +779,7 @@ def __new__(cls, client: Client, *args, **kwargs) -> "Extension": # This gets every coroutine in a way that we can easily change them # cls - for name, func in inspect.getmembers(self, predicate=iscoroutinefunction): + for name, func in getmembers(self, predicate=iscoroutinefunction): # TODO we can make these all share the same list, might make it easier to load/unload if hasattr(func, "__listener_name__"): # set by extension_listener @@ -835,9 +834,9 @@ def teardown(self): for func in funcs: self.client.websocket.dispatch.events[cmd].remove(func) - clean_cmd_names = ["_".join(cmd.split("_")[1:]) for cmd in self._commands.keys()] + clean_cmd_names = [cmd[7:] for cmd in self._commands.keys()] cmds = filter( - lambda x: x["name"] in clean_cmd_names, + lambda cmd_data: cmd_data["name"] in clean_cmd_names, self.client.http.cache.interactions.view, ) @@ -852,7 +851,7 @@ def teardown(self): ] -@functools.wraps(command) +@wraps(command) def extension_command(*args, **kwargs): def decorator(coro): coro.__command_data__ = (args, kwargs) @@ -870,7 +869,7 @@ def decorator(func): return decorator -# @functools.wraps(Client.component) +@wraps(Client.component) def extension_component(*args, **kwargs): def decorator(func): func.__component_data__ = (args, kwargs) diff --git a/interactions/client.pyi b/interactions/client.pyi index ebcbbd1c9..b0885305f 100644 --- a/interactions/client.pyi +++ b/interactions/client.pyi @@ -61,7 +61,6 @@ class Client: async def raw_channel_create(self, message) -> dict: ... async def raw_message_create(self, message) -> dict: ... async def raw_guild_create(self, guild) -> dict: ... - def add_extension(self, extension: "Extension"): ... class Extension: client: Client From dc068a0ea03cd4466d2eb4c83707cf4a559709d1 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 15 Jan 2022 02:52:45 +0000 Subject: [PATCH 07/21] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- interactions/client.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/interactions/client.py b/interactions/client.py index 3a49182ea..2892d1e6b 100644 --- a/interactions/client.py +++ b/interactions/client.py @@ -1,14 +1,15 @@ import sys from asyncio import get_event_loop, iscoroutinefunction from functools import wraps -from types import ModuleType from importlib import import_module from importlib.util import resolve_name from inspect import getmembers from logging import Logger, getLogger +from types import ModuleType from typing import Any, Callable, Coroutine, Dict, List, Optional, Union from interactions.api.models.misc import Snowflake + from .api.cache import Cache from .api.cache import Item as Build from .api.error import InteractionException From 637bc13ab4fb6d9d18bc4a7611c6f49e6ca8b326 Mon Sep 17 00:00:00 2001 From: Catalyst4 <84055084+Catalyst4222@users.noreply.github.com> Date: Fri, 14 Jan 2022 22:42:45 -0500 Subject: [PATCH 08/21] refactor: move Extension components into the _listeners dict --- interactions/client.py | 10 +++------- interactions/client.pyi | 1 - 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/interactions/client.py b/interactions/client.py index 2892d1e6b..aaf60ffa7 100644 --- a/interactions/client.py +++ b/interactions/client.py @@ -776,7 +776,6 @@ def __new__(cls, client: Client, *args, **kwargs) -> "Extension": self.client = client self._commands = {} self._listeners = {} - self._components = {} # This gets every coroutine in a way that we can easily change them # cls @@ -814,9 +813,9 @@ def __new__(cls, client: Client, *args, **kwargs) -> "Extension": ) comp_name = f"component_{comp_name}" - components = self._components.get(comp_name, []) - components.append(func) - self._components[comp_name] = components + listeners = self._listeners.get(comp_name, []) + listeners.append(func) + self._listeners[comp_name] = listeners client.extensions[cls.__name__] = self @@ -827,9 +826,6 @@ def teardown(self): for func in funcs: self.client.websocket.dispatch.events[event].remove(func) - for component, funcs in self._components.items(): - for func in funcs: - self.client.websocket.dispatch.events[component].remove(func) for cmd, funcs in self._commands.items(): for func in funcs: diff --git a/interactions/client.pyi b/interactions/client.pyi index b0885305f..86ed913ef 100644 --- a/interactions/client.pyi +++ b/interactions/client.pyi @@ -66,7 +66,6 @@ class Extension: client: Client _commands: dict _listeners: dict - _components: dict def __new__(cls, client: Client, *args, **kwargs) -> Extension: ... def teardown(self) -> None: ... From 7ecdac4ed8073fed14cce4847321d7eb70a20d36 Mon Sep 17 00:00:00 2001 From: Catalyst4 <84055084+Catalyst4222@users.noreply.github.com> Date: Fri, 14 Jan 2022 23:20:44 -0500 Subject: [PATCH 09/21] feat: add an autocomplete Extension decorator --- interactions/client.py | 28 +++++++++++++++++++++++++++- interactions/client.pyi | 3 +++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/interactions/client.py b/interactions/client.py index aaf60ffa7..7d33e1f14 100644 --- a/interactions/client.py +++ b/interactions/client.py @@ -626,6 +626,7 @@ def load(self, name: str, package: Optional[str] = None) -> None: except Exception as error: del sys.modules[name] log.error(f"Could not load {name}: {error}. Skipping.") + raise error else: log.debug(f"Loaded extension {name}.") self.extensions[_name] = module @@ -817,6 +818,23 @@ def __new__(cls, client: Client, *args, **kwargs) -> "Extension": listeners.append(func) self._listeners[comp_name] = listeners + if hasattr(func, "__autocomplete_data__"): + args, kwargs = func.__autocomplete_data__ + func = client.autocomplete(*args, **kwargs)(func) + + name = kwargs.get("name") or args[0] + _command = kwargs.get("command") or args[1] + + _command: Union[Snowflake, int] = ( + _command.id if isinstance(_command, ApplicationCommand) else _command + ) + + auto_name = f"autocomplete_{_command}_{name}" + + listeners = self._listeners.get(auto_name, []) + listeners.append(func) + self._listeners[auto_name] = listeners + client.extensions[cls.__name__] = self return self @@ -826,7 +844,6 @@ def teardown(self): for func in funcs: self.client.websocket.dispatch.events[event].remove(func) - for cmd, funcs in self._commands.items(): for func in funcs: self.client.websocket.dispatch.events[cmd].remove(func) @@ -873,3 +890,12 @@ def decorator(func): return func return decorator + + +@wraps(Client.autocomplete) +def extension_autocomplete(*args, **kwargs): + def decorator(func): + func.__autocomplete_data__ = (args, kwargs) + return func + + return decorator diff --git a/interactions/client.pyi b/interactions/client.pyi index 86ed913ef..f69a32a78 100644 --- a/interactions/client.pyi +++ b/interactions/client.pyi @@ -80,3 +80,6 @@ def extension_command( ): ... def extension_listener(name=None) -> Callable[..., Any]: ... def extension_component(component: Union[Button, SelectMenu]) -> Callable[..., Any]: ... +def extension_autocomplete( + name: str, command: Union[ApplicationCommand, int] +) -> Callable[..., Any]: ... From 7b44849e27ca59afd3849c8346346857aadda144 Mon Sep 17 00:00:00 2001 From: Catalyst4 <84055084+Catalyst4222@users.noreply.github.com> Date: Fri, 14 Jan 2022 23:35:30 -0500 Subject: [PATCH 10/21] feat: add a modal Extension decorator --- interactions/client.py | 20 ++++++++++++++++++++ interactions/client.pyi | 1 + 2 files changed, 21 insertions(+) diff --git a/interactions/client.py b/interactions/client.py index 7d33e1f14..187c7d5d3 100644 --- a/interactions/client.py +++ b/interactions/client.py @@ -835,6 +835,17 @@ def __new__(cls, client: Client, *args, **kwargs) -> "Extension": listeners.append(func) self._listeners[auto_name] = listeners + if hasattr(func, "__modal_data__"): + args, kwargs = func.__modal_data__ + func = client.modal(*args, **kwargs)(func) + + modal = kwargs.get("modal") or args[0] + modal_name = f"modal_{modal.custom_id}" + + listeners = self._listeners.get(modal_name, []) + listeners.append(func) + self._listeners[modal_name] = listeners + client.extensions[cls.__name__] = self return self @@ -899,3 +910,12 @@ def decorator(func): return func return decorator + + +@wraps(Client.modal) +def extension_modal(*args, **kwargs): + def decorator(func): + func.__modal_data__ = (args, kwargs) + return func + + return decorator diff --git a/interactions/client.pyi b/interactions/client.pyi index f69a32a78..f5098c416 100644 --- a/interactions/client.pyi +++ b/interactions/client.pyi @@ -83,3 +83,4 @@ def extension_component(component: Union[Button, SelectMenu]) -> Callable[..., A def extension_autocomplete( name: str, command: Union[ApplicationCommand, int] ) -> Callable[..., Any]: ... +def extension_modal(modal: Modal) -> Callable[..., Any]: ... From fc8aa4777c0dcea86d582ad3a690635cf000df47 Mon Sep 17 00:00:00 2001 From: Catalyst4 <84055084+Catalyst4222@users.noreply.github.com> Date: Mon, 17 Jan 2022 14:05:10 -0500 Subject: [PATCH 11/21] feat: add message and user context menu decorators for Extensions --- interactions/client.py | 20 ++++++++++++++++++++ interactions/client.pyi | 26 ++++++++++++++++++++++++++ 2 files changed, 46 insertions(+) diff --git a/interactions/client.py b/interactions/client.py index 187c7d5d3..0654de27e 100644 --- a/interactions/client.py +++ b/interactions/client.py @@ -919,3 +919,23 @@ def decorator(func): return func return decorator + + +@wraps(Client.message_command) +def extension_message_command(*args, **kwargs): + def decorator(func): + kwargs["type"] = ApplicationCommandType.MESSAGE + func.__command_data__ = (args, kwargs) + return func + + return decorator + + +@wraps(Client.user_command) +def extension_user_command(*args, **kwargs): + def decorator(func): + kwargs["type"] = ApplicationCommandType.USER + func.__command_data__ = (args, kwargs) + return func + + return decorator diff --git a/interactions/client.pyi b/interactions/client.pyi index f5098c416..d6596f794 100644 --- a/interactions/client.pyi +++ b/interactions/client.pyi @@ -51,6 +51,20 @@ class Client: options: Optional[List[Option]] = None, default_permission: Optional[bool] = None, ) -> Callable[..., Any]: ... + def message_command( + self, + *, + name: Optional[str] = None, + scope: Optional[Union[int, Guild, List[int], List[Guild]]] = None, + default_permission: Optional[bool] = None, + ) -> Callable[..., Any]: ... + def user_command( + self, + *, + name: Optional[str] = None, + scope: Optional[Union[int, Guild, List[int], List[Guild]]] = None, + default_permission: Optional[bool] = None, + ) -> Callable[..., Any]: ... def component(self, component: Union[Button, SelectMenu]) -> Callable[..., Any]: ... def autocomplete(self, name: str) -> Callable[..., Any]: ... def modal(self, modal: Modal) -> Callable[..., Any]: ... @@ -84,3 +98,15 @@ def extension_autocomplete( name: str, command: Union[ApplicationCommand, int] ) -> Callable[..., Any]: ... def extension_modal(modal: Modal) -> Callable[..., Any]: ... +def extension_message_command( + *, + name: Optional[str] = None, + scope: Optional[Union[int, Guild, List[int], List[Guild]]] = None, + default_permission: Optional[bool] = None, +) -> Callable[..., Any]: ... +def extension_user_command( + *, + name: Optional[str] = None, + scope: Optional[Union[int, Guild, List[int], List[Guild]]] = None, + default_permission: Optional[bool] = None, +) -> Callable[..., Any]: ... From d7e7bbd01fb96e846b34d6ff54aec12df3ca3746 Mon Sep 17 00:00:00 2001 From: Toricane <73972068+Toricane@users.noreply.github.com> Date: Sun, 23 Jan 2022 18:55:56 -0800 Subject: [PATCH 12/21] feat: advanced extension loading --- interactions/client.py | 29 +++++++++++++++++++++++------ 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/interactions/client.py b/interactions/client.py index 0654de27e..a215917d3 100644 --- a/interactions/client.py +++ b/interactions/client.py @@ -601,8 +601,10 @@ def decorator(coro: Coroutine) -> Any: return decorator - def load(self, name: str, package: Optional[str] = None) -> None: - """ + def load( + self, name: str, package: Optional[str] = None, *args, **kwargs + ) -> Union["Extension", None]: + r""" "Loads" an extension off of the current client by adding a new class which is imported from the library. @@ -610,6 +612,12 @@ def load(self, name: str, package: Optional[str] = None) -> None: :type name: str :param package?: The package of the extension. :type package: Optional[str] + :param \*args?: Optional arguments to pass to the extension + :type \**args: tuple + :param \**kwargs?: Optional keyword-only arguments to pass to the extension. + :type \**kwargs: dict + :return: The loaded extension. + :rtype: interactions.client.Extension """ _name: str = resolve_name(name, package) @@ -622,7 +630,7 @@ def load(self, name: str, package: Optional[str] = None) -> None: try: setup = getattr(module, "setup") - setup(self) + extension = setup(self, *args, **kwargs) except Exception as error: del sys.modules[name] log.error(f"Could not load {name}: {error}. Skipping.") @@ -630,6 +638,7 @@ def load(self, name: str, package: Optional[str] = None) -> None: else: log.debug(f"Loaded extension {name}.") self.extensions[_name] = module + return extension def remove(self, name: str, package: Optional[str] = None) -> None: """ @@ -668,14 +677,22 @@ def remove(self, name: str, package: Optional[str] = None) -> None: log.debug(f"Removed extension {name}.") - def reload(self, name: str, package: Optional[str] = None) -> None: - """ + def reload( + self, name: str, package: Optional[str] = None, *args, **kwargs + ) -> Union["Extension", None]: + r""" "Reloads" an extension off of current client from an import resolve. :param name: The name of the extension. :type name: str :param package?: The package of the extension. :type package: Optional[str] + :param \*args?: Optional arguments to pass to the extension + :type \**args: tuple + :param \**kwargs?: Optional keyword-only arguments to pass to the extension. + :type \**kwargs: dict + :return: The reloaded extension. + :rtype: interactions.client.Extension """ _name: str = resolve_name(name, package) extension = self.extensions.get(_name) @@ -686,7 +703,7 @@ def reload(self, name: str, package: Optional[str] = None) -> None: return self.remove(name, package) - self.load(name, package) + return self.load(name, package) async def raw_socket_create(self, data: Dict[Any, Any]) -> Dict[Any, Any]: """ From b0718b99946cbb3a923aac9f4570ed4ce5dd6b30 Mon Sep 17 00:00:00 2001 From: Toricane <73972068+Toricane@users.noreply.github.com> Date: Sun, 23 Jan 2022 21:38:19 -0800 Subject: [PATCH 13/21] fix: typehints --- interactions/client.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/interactions/client.py b/interactions/client.py index a215917d3..a30efbc1b 100644 --- a/interactions/client.py +++ b/interactions/client.py @@ -603,7 +603,7 @@ def decorator(coro: Coroutine) -> Any: def load( self, name: str, package: Optional[str] = None, *args, **kwargs - ) -> Union["Extension", None]: + ) -> Optional["Extension"]: r""" "Loads" an extension off of the current client by adding a new class which is imported from the library. @@ -617,7 +617,7 @@ def load( :param \**kwargs?: Optional keyword-only arguments to pass to the extension. :type \**kwargs: dict :return: The loaded extension. - :rtype: interactions.client.Extension + :rtype: Optional[interactions.client.Extension] """ _name: str = resolve_name(name, package) @@ -679,7 +679,7 @@ def remove(self, name: str, package: Optional[str] = None) -> None: def reload( self, name: str, package: Optional[str] = None, *args, **kwargs - ) -> Union["Extension", None]: + ) -> Optional["Extension"]: r""" "Reloads" an extension off of current client from an import resolve. @@ -692,7 +692,7 @@ def reload( :param \**kwargs?: Optional keyword-only arguments to pass to the extension. :type \**kwargs: dict :return: The reloaded extension. - :rtype: interactions.client.Extension + :rtype: Optional[interactions.client.Extension] """ _name: str = resolve_name(name, package) extension = self.extensions.get(_name) From 237cac1ab2edf0e126d6d5b0283bfbd0e9d204dc Mon Sep 17 00:00:00 2001 From: Toricane <73972068+Toricane@users.noreply.github.com> Date: Sun, 23 Jan 2022 21:39:17 -0800 Subject: [PATCH 14/21] docs: update client.pyi --- interactions/client.pyi | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/interactions/client.pyi b/interactions/client.pyi index d6596f794..aa42b1dc7 100644 --- a/interactions/client.pyi +++ b/interactions/client.pyi @@ -68,9 +68,9 @@ class Client: def component(self, component: Union[Button, SelectMenu]) -> Callable[..., Any]: ... def autocomplete(self, name: str) -> Callable[..., Any]: ... def modal(self, modal: Modal) -> Callable[..., Any]: ... - def load(self, name: str, package: Optional[str] = None) -> None: ... + def load(self, name: str, package: Optional[str] = None, *args, **kwargs) -> Optional["Extension"]: ... def remove(self, name: str, package: Optional[str] = None) -> None: ... - def reload(self, name: str, package: Optional[str] = None) -> None: ... + def reload(self, name: str, package: Optional[str] = None, *args, **kwargs) -> Optional["Extension"]: ... async def raw_socket_create(self, data: Dict[Any, Any]) -> dict: ... async def raw_channel_create(self, message) -> dict: ... async def raw_message_create(self, message) -> dict: ... From 6fa85137f00a8a1ccfdc5a4ce8d1b554a6829d47 Mon Sep 17 00:00:00 2001 From: Catalyst4 <84055084+Catalyst4222@users.noreply.github.com> Date: Fri, 28 Jan 2022 21:21:37 -0500 Subject: [PATCH 15/21] fix: Apply suggestions from code review (#428) Co-authored-by: fl0w <41456914+goverfl0w@users.noreply.github.com> Co-authored-by: Max Hirtz --- interactions/client.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/interactions/client.py b/interactions/client.py index a30efbc1b..4204b7fee 100644 --- a/interactions/client.py +++ b/interactions/client.py @@ -617,7 +617,7 @@ def load( :param \**kwargs?: Optional keyword-only arguments to pass to the extension. :type \**kwargs: dict :return: The loaded extension. - :rtype: Optional[interactions.client.Extension] + :rtype: Optional[Extension] """ _name: str = resolve_name(name, package) @@ -692,7 +692,7 @@ def reload( :param \**kwargs?: Optional keyword-only arguments to pass to the extension. :type \**kwargs: dict :return: The reloaded extension. - :rtype: Optional[interactions.client.Extension] + :rtype: Optional[Extension] """ _name: str = resolve_name(name, package) extension = self.extensions.get(_name) @@ -703,7 +703,7 @@ def reload( return self.remove(name, package) - return self.load(name, package) + return self.load(name, package, *args, **kwargs) async def raw_socket_create(self, data: Dict[Any, Any]) -> Dict[Any, Any]: """ From 339fdfa68ae1e4580d082fcaa333dc6923b0243a Mon Sep 17 00:00:00 2001 From: Catalyst4 <84055084+Catalyst4222@users.noreply.github.com> Date: Fri, 28 Jan 2022 22:17:35 -0500 Subject: [PATCH 16/21] chore: pre-commit --- interactions/client.pyi | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/interactions/client.pyi b/interactions/client.pyi index 404afc3be..075feca31 100644 --- a/interactions/client.pyi +++ b/interactions/client.pyi @@ -86,9 +86,13 @@ class Client: def component(self, component: Union[Button, SelectMenu]) -> Callable[..., Any]: ... def autocomplete(self, name: str) -> Callable[..., Any]: ... def modal(self, modal: Modal) -> Callable[..., Any]: ... - def load(self, name: str, package: Optional[str] = None, *args, **kwargs) -> Optional["Extension"]: ... + def load( + self, name: str, package: Optional[str] = None, *args, **kwargs + ) -> Optional["Extension"]: ... def remove(self, name: str, package: Optional[str] = None) -> None: ... - def reload(self, name: str, package: Optional[str] = None, *args, **kwargs) -> Optional["Extension"]: ... + def reload( + self, name: str, package: Optional[str] = None, *args, **kwargs + ) -> Optional["Extension"]: ... async def raw_socket_create(self, data: Dict[Any, Any]) -> dict: ... async def raw_channel_create(self, message) -> dict: ... async def raw_message_create(self, message) -> dict: ... From 5fddc6810712c09362f196762c88f08dbee575d2 Mon Sep 17 00:00:00 2001 From: fl0w <41456914+goverfl0w@users.noreply.github.com> Date: Sat, 29 Jan 2022 14:55:59 -0500 Subject: [PATCH 17/21] fix: move to dunder method on extensions Co-authored-by: Max Hirtz --- interactions/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interactions/client.py b/interactions/client.py index d7368e069..1c590083c 100644 --- a/interactions/client.py +++ b/interactions/client.py @@ -880,7 +880,7 @@ def __new__(cls, client: Client, *args, **kwargs) -> "Extension": listeners.append(func) self._listeners[modal_name] = listeners - client.extensions[cls.__name__] = self + client._extensions[cls.__name__] = self return self From 356c83f050ef903ad6b284905b7b34107d33b170 Mon Sep 17 00:00:00 2001 From: fl0w <41456914+goverfl0w@users.noreply.github.com> Date: Sat, 29 Jan 2022 18:18:34 -0500 Subject: [PATCH 18/21] fix: allow extensions to be properly removed. --- interactions/client.py | 1 + 1 file changed, 1 insertion(+) diff --git a/interactions/client.py b/interactions/client.py index 1c590083c..21fd3e47a 100644 --- a/interactions/client.py +++ b/interactions/client.py @@ -690,6 +690,7 @@ def remove(self, name: str, package: Optional[str] = None) -> None: del sys.modules[_name] + del sys.modules[_name] del self._extensions[_name] log.debug(f"Removed extension {name}.") From 268bd3090b996daf7bc5361be9f930c9e3ddec2e Mon Sep 17 00:00:00 2001 From: fl0w <41456914+goverfl0w@users.noreply.github.com> Date: Sat, 29 Jan 2022 18:19:38 -0500 Subject: [PATCH 19/21] perf: end extension loading based on failure. Co-authored-by: Max --- interactions/client.py | 1 + 1 file changed, 1 insertion(+) diff --git a/interactions/client.py b/interactions/client.py index 21fd3e47a..c71905206 100644 --- a/interactions/client.py +++ b/interactions/client.py @@ -640,6 +640,7 @@ def load( if _name in self._extensions: log.error(f"Extension {name} has already been loaded. Skipping.") + return module = import_module( name, package From 46f4fec70eb4d9af1bd6cc48e851b2afae3c975f Mon Sep 17 00:00:00 2001 From: Catalyst4 <84055084+Catalyst4222@users.noreply.github.com> Date: Sat, 29 Jan 2022 23:38:25 -0500 Subject: [PATCH 20/21] fix: restore Extension functionality after breaking changes from 4.0.2 --- interactions/client.py | 18 +++++++----------- interactions/client.pyi | 2 +- 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/interactions/client.py b/interactions/client.py index c71905206..c7d768108 100644 --- a/interactions/client.py +++ b/interactions/client.py @@ -4,15 +4,12 @@ from importlib import import_module from importlib.util import resolve_name from inspect import getmembers -from logging import Logger, getLogger +from logging import Logger from types import ModuleType from typing import Any, Callable, Coroutine, Dict, List, Optional, Union -from interactions.api.models.misc import Snowflake - from .api.cache import Cache from .api.cache import Item as Build -from .api.dispatch import Listener from .api.error import InteractionException from .api.gateway import WebSocket from .api.http import HTTPClient @@ -691,7 +688,6 @@ def remove(self, name: str, package: Optional[str] = None) -> None: del sys.modules[_name] - del sys.modules[_name] del self._extensions[_name] log.debug(f"Removed extension {name}.") @@ -889,22 +885,22 @@ def __new__(cls, client: Client, *args, **kwargs) -> "Extension": def teardown(self): for event, funcs in self._listeners.items(): for func in funcs: - self.client.websocket.dispatch.events[event].remove(func) + self.client._websocket.dispatch.events[event].remove(func) for cmd, funcs in self._commands.items(): for func in funcs: - self.client.websocket.dispatch.events[cmd].remove(func) + self.client._websocket.dispatch.events[cmd].remove(func) clean_cmd_names = [cmd[7:] for cmd in self._commands.keys()] cmds = filter( lambda cmd_data: cmd_data["name"] in clean_cmd_names, - self.client.http.cache.interactions.view, + self.client._http.cache.interactions.view, ) - if self.client.automate_sync: + if self.client._automate_sync: [ - self.client.loop.create_task( - self.client.http.delete_application_command( + self.client._loop.create_task( + self.client._http.delete_application_command( cmd["application_id"], cmd["id"], cmd["guild_id"] ) ) diff --git a/interactions/client.pyi b/interactions/client.pyi index 075feca31..f53697d11 100644 --- a/interactions/client.pyi +++ b/interactions/client.pyi @@ -27,7 +27,7 @@ class Client: _presence: Optional[Presence] _token: str _automate_sync: bool - _extensions: Optional[Dict[str, ModuleType]] + _extensions: Optional[Dict[str, Union[ModuleType, Extension]]] me: Optional[Application] def __init__( self, From a2a000cace654df6bb5e5cfd06e1313931e8f464 Mon Sep 17 00:00:00 2001 From: Catalyst4 <84055084+Catalyst4222@users.noreply.github.com> Date: Sun, 30 Jan 2022 00:36:15 -0500 Subject: [PATCH 21/21] feat: Allow the hot-loading of Extensions --- interactions/api/http.py | 8 +++++++- interactions/client.pyi | 7 +++---- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/interactions/api/http.py b/interactions/api/http.py index 03399a824..5a1f9e931 100644 --- a/interactions/api/http.py +++ b/interactions/api/http.py @@ -218,6 +218,10 @@ async def request(self, route: Route, **kwargs) -> Optional[Any]: # The idea is that its regulated by the priority of Discord's bucket header and not just self-computation. + def release_lock(lock): + if lock.locked(): + lock.release() + if self.ratelimits.get(bucket): _limiter: Limiter = self.ratelimits.get(bucket) if _limiter.lock.locked(): @@ -227,7 +231,7 @@ async def request(self, route: Route, **kwargs) -> Optional[Any]: log.warning( f"The current bucket is still under a rate limit. Calling later in {_limiter.reset_after} seconds." ) - self._loop.call_later(_limiter.reset_after, _limiter.lock.release) + self._loop.call_later(_limiter.reset_after, release_lock, _limiter.lock) _limiter.reset_after = 0 else: self.ratelimits[bucket] = ( @@ -289,6 +293,8 @@ async def request(self, route: Route, **kwargs) -> Optional[Any]: ) log.debug(f"RETURN {response.status}: {dumps(data, indent=4, sort_keys=True)}") + if _limiter.lock.locked(): + _limiter.lock.release() return data # These account for general/specific exceptions. (Windows...) diff --git a/interactions/client.pyi b/interactions/client.pyi index f53697d11..04272481d 100644 --- a/interactions/client.pyi +++ b/interactions/client.pyi @@ -2,18 +2,17 @@ from asyncio import AbstractEventLoop from types import ModuleType from typing import Any, Callable, Coroutine, Dict, List, NoReturn, Optional, Tuple, Union -from .api.models.gw import Presence -from .models.misc import MISSING - from .api.cache import Cache from .api.gateway import WebSocket from .api.http import HTTPClient -from .api.models.guild import Guild from .api.models.flags import Intents +from .api.models.guild import Guild +from .api.models.gw import Presence from .api.models.team import Application from .enums import ApplicationCommandType from .models.command import ApplicationCommand, Option from .models.component import Button, Modal, SelectMenu +from .models.misc import MISSING _token: str = "" # noqa _cache: Optional[Cache] = None