Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
06faf55
feat: create basic functionality for Extensions
Catalyst4222 Jan 13, 2022
12d5c4c
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jan 13, 2022
94ef8b0
feat: allow the creation of commands after starting the event loop
Catalyst4222 Jan 14, 2022
caab8de
feat: add the ability to unload a cog, and remove the commands when d…
Catalyst4222 Jan 14, 2022
38c91dd
Merge remote-tracking branch 'origin/unstable' into unstable
Catalyst4222 Jan 14, 2022
17076cf
chore: remove unused imports for flake8
Catalyst4222 Jan 14, 2022
691739f
style: add in suggestions from #428
Catalyst4222 Jan 15, 2022
dc068a0
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jan 15, 2022
637bc13
refactor: move Extension components into the _listeners dict
Catalyst4222 Jan 15, 2022
7ecdac4
feat: add an autocomplete Extension decorator
Catalyst4222 Jan 15, 2022
7b44849
feat: add a modal Extension decorator
Catalyst4222 Jan 15, 2022
fc8aa47
feat: add message and user context menu decorators for Extensions
Catalyst4222 Jan 17, 2022
d7e7bbd
feat: advanced extension loading
Toricane Jan 24, 2022
b0718b9
fix: typehints
Toricane Jan 24, 2022
237cac1
docs: update client.pyi
Toricane Jan 24, 2022
2c9fe2f
Merge pull request #1 from Toricane/unstable
Catalyst4222 Jan 24, 2022
6fa8513
fix: Apply suggestions from code review (#428)
Catalyst4222 Jan 29, 2022
8d4481c
Merge branch 'unstable' into unstable
Catalyst4222 Jan 29, 2022
339fdfa
chore: pre-commit
Catalyst4222 Jan 29, 2022
5fddc68
fix: move to dunder method on extensions
i0bs Jan 29, 2022
356c83f
fix: allow extensions to be properly removed.
i0bs Jan 29, 2022
268bd30
perf: end extension loading based on failure.
i0bs Jan 29, 2022
46f4fec
fix: restore Extension functionality after breaking changes from 4.0.2
Catalyst4222 Jan 30, 2022
a2a000c
feat: Allow the hot-loading of Extensions
Catalyst4222 Jan 30, 2022
bbfc10b
Merge branch 'unstable' into unstable
Catalyst4222 Jan 30, 2022
f75fd0e
Merge branch 'unstable' into unstable
Catalyst4222 Jan 31, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
197 changes: 165 additions & 32 deletions interactions/client.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import functools
import inspect
import sys
from asyncio import get_event_loop
import types
from asyncio import get_event_loop, iscoroutinefunction
from importlib import import_module
from importlib.util import resolve_name
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
Expand Down Expand Up @@ -349,7 +351,13 @@ 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}")

Expand Down Expand Up @@ -406,7 +414,13 @@ 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}")

Expand Down Expand Up @@ -463,7 +477,13 @@ 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}")

Expand Down Expand Up @@ -596,7 +616,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")
Expand All @@ -617,16 +639,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

extension = self.extensions.get(_name)

if module not in self.extensions:
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.
Expand All @@ -637,11 +677,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)
Expand Down Expand Up @@ -722,25 +763,117 @@ 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 = []

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)

for _command in cls.commands:
cls.client.command(**_command)
def __new__(cls, client: Client, *args, **kwargs) -> "Extension":

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(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):
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

return decorator
19 changes: 18 additions & 1 deletion interactions/client.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,24 @@ 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: ...
_commands: dict
_listeners: dict
_components: dict
def __new__(cls, client: Client, *args, **kwargs) -> Extension: ...
def teardown(self) -> None: ...

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]: ...