From f64c785cb79bd37a7f0900b5793417ecc76e5a2e Mon Sep 17 00:00:00 2001 From: davfsa Date: Sat, 24 Jun 2023 15:40:28 +0200 Subject: [PATCH 1/3] Move event listeners and client callbacks API to use AbstractComponentLoader --- docs/reference/listeners.md | 3 + tanjun/__init__.py | 4 + tanjun/abc.py | 22 +- tanjun/components.py | 62 ++++- tanjun/listeners.py | 238 ++++++++++++++++++++ tests/test_components.py | 232 +------------------ tests/test_components_future_annotations.py | 214 ------------------ tests/test_listeners.py | 143 ++++++++++++ tests/test_listeners_future_annotations.py | 174 ++++++++++++++ 9 files changed, 632 insertions(+), 460 deletions(-) create mode 100644 docs/reference/listeners.md create mode 100644 tanjun/listeners.py delete mode 100644 tests/test_components_future_annotations.py create mode 100644 tests/test_listeners.py create mode 100644 tests/test_listeners_future_annotations.py diff --git a/docs/reference/listeners.md b/docs/reference/listeners.md new file mode 100644 index 000000000..13ff9eb59 --- /dev/null +++ b/docs/reference/listeners.md @@ -0,0 +1,3 @@ +# tanjun.listeners + +::: tanjun.listeners diff --git a/tanjun/__init__.py b/tanjun/__init__.py index fa570ead5..f3d022980 100644 --- a/tanjun/__init__.py +++ b/tanjun/__init__.py @@ -142,6 +142,8 @@ async def hello(ctx: tanjun.abc.Context, user: hikari.User | None) -> None: "as_self_injecting", "as_slash_command", "as_time_schedule", + "as_client_callback", + "as_event_listener", "as_unloader", "as_user_menu", "cached_inject", @@ -301,6 +303,8 @@ async def hello(ctx: tanjun.abc.Context, user: hikari.User | None) -> None: from .hooks import MessageHooks from .hooks import SlashHooks from .injecting import as_self_injecting +from .listeners import as_client_callback +from .listeners import as_event_listener from .parsing import ShlexParser from .parsing import with_argument from .parsing import with_greedy_argument diff --git a/tanjun/abc.py b/tanjun/abc.py index 968f0d447..ed9b193e5 100644 --- a/tanjun/abc.py +++ b/tanjun/abc.py @@ -86,6 +86,7 @@ from typing_extensions import Self from . import errors + from . import listeners as listeners_ _BaseSlashCommandT = typing.TypeVar("_BaseSlashCommandT", bound="BaseSlashCommand") @@ -3589,31 +3590,20 @@ def remove_listener(self, event: type[_EventT], listener: ListenerCallbackSig[_E The component to enable chained calls. """ - @abc.abstractmethod def with_listener( - self, *event_types: type[hikari.Event] - ) -> collections.Callable[[_ListenerCallbackSigT], _ListenerCallbackSigT]: + self, event_listener: listeners_.EventListener[_ListenerCallbackSigT] + ) -> listeners_.EventListener[_ListenerCallbackSigT]: """Add a listener to this component through a decorator call. Parameters ---------- - *event_types - One or more event types to listen for. - - If none are provided then the event type(s) will be inferred from - the callback's type-hints. + event_listener + The event listener to add to the component. Returns ------- - collections.abc.Callable[[ListenerCallbackSig], ListenerCallbackSig] + tanjun.listeners.EventListener[ListenerCallbackSigT] Decorator callback which takes listener to add. - - Raises - ------ - ValueError - If nothing was passed for `event_types` and no subclasses of - [hikari.Event][hikari.events.base_events.Event] are found in the - type-hint for the callback's first argument. """ @abc.abstractmethod diff --git a/tanjun/components.py b/tanjun/components.py index 82a72d344..c3f105abc 100644 --- a/tanjun/components.py +++ b/tanjun/components.py @@ -44,6 +44,7 @@ from collections import abc as collections import hikari +import typing_extensions from . import _internal from . import abc as tanjun @@ -51,6 +52,7 @@ if typing.TYPE_CHECKING: from typing_extensions import Self + from . import listeners as listeners_ from . import schedules as schedules_ _AppCommandContextT = typing.TypeVar("_AppCommandContextT", bound=tanjun.AppCommandContext) @@ -715,9 +717,25 @@ def remove_client_callback(self, name: str, callback: tanjun.MetaEventSig, /) -> if self._client: self._client.remove_client_callback(name, callback) + @typing.overload + def with_client_callback( + self, client_callback: listeners_.ClientCallback[_MetaEventSigT], / + ) -> listeners_.ClientCallback[_MetaEventSigT]: + ... + + @typing.overload + @typing_extensions.deprecated("Use tanjun.as_client_callback decorator instead of passing arguments here") def with_client_callback( self, name: typing.Union[str, tanjun.ClientCallbackNames], / ) -> collections.Callable[[_MetaEventSigT], _MetaEventSigT]: + ... + + def with_client_callback( + self, + client_callback_or_name: typing.Union[str, tanjun.ClientCallbackNames] + | listeners_.ClientCallback[_MetaEventSigT], + /, + ) -> collections.Callable[[_MetaEventSigT], _MetaEventSigT] | listeners_.ClientCallback[_MetaEventSigT]: """Add a client callback through a decorator call. Examples @@ -732,10 +750,15 @@ async def on_close() -> None: Parameters ---------- - name - The name this callback is being registered to. + client_callback_or_name + The client callback object to register. - This is case-insensitive. + .. deprecated :: 2.15 + This argument used to be `name`: + + The name this callback is being registered to. + + This is case-insensitive. Returns ------- @@ -746,9 +769,13 @@ async def on_close() -> None: keyword arguments a callback should expect depend on implementation detail around the `name` being subscribed to. """ + if isinstance(client_callback_or_name, listeners_.ClientCallback): + self.add_client_callback(client_callback_or_name.name, client_callback_or_name.callback) + return client_callback_or_name + # Deprecated implementation def decorator(callback: _MetaEventSigT, /) -> _MetaEventSigT: - self.add_client_callback(name, callback) + self.add_client_callback(client_callback_or_name, callback) return callback return decorator @@ -1017,12 +1044,37 @@ def remove_listener(self, event: type[_EventT], listener: tanjun.ListenerCallbac return self + @typing.overload + @typing_extensions.deprecated("Use tanjun.as_event_listener decorator instead of passing arguments here") def with_listener( self, *event_types: type[hikari.Event] ) -> collections.Callable[[_ListenerCallbackSigT], _ListenerCallbackSigT]: + ... + + @typing.overload + def with_listener( + self, *event_listener: listeners_.EventListener[_ListenerCallbackSigT] + ) -> listeners_.EventListener[_ListenerCallbackSigT]: + ... + + def with_listener( + self, *event_types_or_listener: type[hikari.Event] | listeners_.EventListener[_ListenerCallbackSigT] + ) -> collections.Callable[[_ListenerCallbackSigT], _ListenerCallbackSigT] | listeners_.EventListener[ + _ListenerCallbackSigT + ]: # <>. + # Unfortunately, during the deprecation period we need to do some hacky stuff + possible_listener = event_types_or_listener[0] + if isinstance(possible_listener, listeners_.EventListener): + for event_type in possible_listener.event_types: + self.add_listener(event_type, possible_listener.callback) + + return possible_listener + + # Deprecated impl def decorator(callback: _ListenerCallbackSigT, /) -> _ListenerCallbackSigT: - for event_type in event_types or _internal.infer_listener_types(callback): + for event_type in event_types_or_listener or _internal.infer_listener_types(callback): + event_type = typing.cast("type[hikari.Event]", event_type) self.add_listener(event_type, callback) return callback diff --git a/tanjun/listeners.py b/tanjun/listeners.py new file mode 100644 index 000000000..52d4a3963 --- /dev/null +++ b/tanjun/listeners.py @@ -0,0 +1,238 @@ +# -*- coding: utf-8 -*- +# BSD 3-Clause License +# +# Copyright (c) 2020-2023, Faster Speeding +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, this +# list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# * Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +"""Interface and interval implementation for a Tanjun based event listener.""" +from __future__ import annotations + +__all__: list[str] = ["as_event_listener", "EventListener", "as_client_callback", "ClientCallback"] + +import copy +import typing +from collections import abc as collections + +from . import _internal +from . import abc as tanjun +from . import components + +if typing.TYPE_CHECKING: + import hikari + from typing_extensions import Self + + +_ListenerCallbackSigT = typing.TypeVar("_ListenerCallbackSigT", bound=tanjun.ListenerCallbackSig[typing.Any]) +_ClientCallbackSigT = typing.TypeVar("_ClientCallbackSigT", bound=tanjun.MetaEventSig) + + +def as_event_listener( + *event_types: type[hikari.Event], +) -> collections.Callable[[_ListenerCallbackSigT], EventListener[_ListenerCallbackSigT]]: + """Create an event listener through a decorator call. + + Parameters + ---------- + *event_types + One or more event types to listen for. + + If none are provided then the event type(s) will be inferred from + the callback's type-hints. + + Returns + ------- + collections.abc.Callable[[ListenerCallbackSig], EventListener[ListenerCallbackSigT]] + Decorator callback which takes listener to add. + + Raises + ------ + ValueError + If nothing was passed for `event_types` and no subclasses of + [hikari.Event][hikari.events.base_events.Event] are found in the + type-hint for the callback's first argument. + """ + return lambda callback: EventListener(callback, *event_types) + + +class EventListener(typing.Generic[_ListenerCallbackSigT], components.AbstractComponentLoader): + """An event listener. + + This should be loaded into a component using either + [Component.load_from_scope][tanjun.components.Component.load_from_scope], + [Component.add_event_listener][tanjun.components.Component.add_event_listener] or + [Component.with_event_listener][tanjun.components.Component.with_event_listener] and + will be registered and unregistered with the linked tanjun client. + """ + + __slots__ = ("_callback", "_event_types") + + def __init__(self, callback: _ListenerCallbackSigT, *event_types: type[hikari.Event]) -> None: + """Initialise the event listener. + + Parameters + ---------- + event_types + One or more event types to listen for. + + If none are provided then the event type(s) will be inferred from + the callback's type-hints. + + Raises + ------ + ValueError + If nothing was passed for `event_types` and no subclasses of + [hikari.Event][hikari.events.base_events.Event] are found in the + type-hint for the callback's first argument. + """ + self._callback = callback + self._event_types = event_types or _internal.infer_listener_types(callback) + + @property + def callback(self) -> _ListenerCallbackSigT: + """The callback that will be registered.""" + return self._callback + + @property + def event_types(self) -> collections.Sequence[type[hikari.Event]]: + """The event types to register the callback to.""" + return self._event_types + + if typing.TYPE_CHECKING: + __call__: _ListenerCallbackSigT + + else: + + async def __call__(self, *args: typing.Any, **kwargs: typing.Any) -> None: + await self._callback(*args, **kwargs) + + def copy(self) -> Self: + """Create a copy the current instance.""" + inst = copy.copy(self) + self._event_types = copy.copy(self._event_types) + return inst + + def load_into_component(self, component: tanjun.Component, /) -> None: + # <>. + for event_type in self.event_types: + component.add_listener(event_type, self._callback) + + +@typing.runtime_checkable +class _ClientCallbackComponentProto(typing.Protocol): + def add_client_callback( + self, name: typing.Union[str, tanjun.ClientCallbackNames], /, *callbacks: tanjun.MetaEventSig + ) -> Self: + raise NotImplementedError + + +def as_client_callback( + name: typing.Union[str, tanjun.ClientCallbackNames] +) -> collections.Callable[[_ClientCallbackSigT], ClientCallback[_ClientCallbackSigT]]: + """Create an event listener through a decorator call. + + Examples + -------- + ```py + client = tanjun.Client.from_rest_bot(bot) + + @client.with_client_callback("closed") + async def on_close() -> None: + raise NotImplementedError + ``` + + Parameters + ---------- + name + The name this callback is being registered to. + + This is case-insensitive. + + Returns + ------- + collections.abc.Callable[[tanjun.abc.MetaEventSig], ClientCallback[tanjun.abc.MetaEventSig]] + Decorator callback used to register the client callback. + + This may be sync or async and must return None. The positional and + keyword arguments a callback should expect depend on implementation + detail around the `name` being subscribed to. + """ + return lambda callback: ClientCallback(callback, name=name) + + +class ClientCallback(typing.Generic[_ClientCallbackSigT], components.AbstractComponentLoader): + """A client callback. + + This should be loaded into a component using either + [Component.load_from_scope][tanjun.components.Component.load_from_scope], + [Component.add_client_callback][tanjun.components.Component.add_client_callback] or + [Component.with_client_callback][tanjun.components.Component.with_client_callback] and + will be registered and unregistered with the linked tanjun client. + """ + + __slots__ = ("_callback", "_name") + + def __init__( + self, callback: _ClientCallbackSigT, /, *, name: typing.Union[str, tanjun.ClientCallbackNames] + ) -> None: + """Initialise the event listener. + + Parameters + ---------- + name + The name this callback is being registered to. + + This is case-insensitive + """ + self._callback = callback + self._name = name + + @property + def callback(self) -> _ClientCallbackSigT: + """The callback that will be registered.""" + return self._callback + + @property + def name(self) -> typing.Union[str, tanjun.ClientCallbackNames]: + """The name the callback will be registered to.""" + return self.name + + if typing.TYPE_CHECKING: + __call__: _ClientCallbackSigT + + else: + + async def __call__(self, *args: typing.Any, **kwargs: typing.Any) -> None: + await self._callback(*args, **kwargs) + + def copy(self) -> Self: + """Create a copy the current instance.""" + return copy.copy(self) + + def load_into_component(self, component: tanjun.Component, /) -> None: + # <>. + if isinstance(component, _ClientCallbackComponentProto): + component.add_client_callback(self._name, self._callback) diff --git a/tests/test_components.py b/tests/test_components.py index 384cb7ab1..cb4f0963a 100644 --- a/tests/test_components.py +++ b/tests/test_components.py @@ -36,9 +36,7 @@ import asyncio import inspect -import sys import types -import typing import hikari import mock @@ -469,10 +467,11 @@ def test_with_client_callback(self): exec_body=lambda ns: ns.update({"add_client_callback": add_client_callback}), )() mock_callback = mock.Mock() + mock_client_callback = mock.Mock(name="aye", callback=mock_callback) - result = component.with_client_callback("aye")(mock_callback) + result = component.with_client_callback(mock_client_callback) - assert result is mock_callback + assert result is mock_client_callback add_client_callback.assert_called_once_with("aye", mock_callback) def test_add_command_for_menu_command(self): @@ -1176,234 +1175,17 @@ def test_remove_listener_when_listener_not_found(self): mock_client.remove_listener.assert_not_called() def test_with_listener(self): - add_listener = mock.Mock() - component: tanjun.Component = types.new_class( - "StubComponent", (tanjun.Component,), exec_body=lambda ns: ns.update({"add_listener": add_listener}) - )() - mock_listener = mock.Mock() - - result = component.with_listener(hikari.Event)(mock_listener) - - assert result is mock_listener - add_listener.assert_called_once_with(hikari.Event, mock_listener) - - def test_with_listener_no_provided_event(self): - async def callback(foo) -> None: # type: ignore - ... - - add_listener = mock.Mock() - component: tanjun.Component = types.new_class( - "StubComponent", (tanjun.Component,), exec_body=lambda ns: ns.update({"add_listener": add_listener}) - )() - - with pytest.raises(ValueError, match="Missing event argument annotation"): - component.with_listener()(callback) # pyright: ignore[reportUnknownArgumentType] - - add_listener.assert_not_called() - - def test_with_listener_no_provided_event_callback_has_no_signature(self): - with pytest.raises(ValueError, match=".+"): - inspect.Signature.from_callable(int) - - add_listener = mock.Mock() - component: tanjun.Component = types.new_class( - "StubComponent", (tanjun.Component,), exec_body=lambda ns: ns.update({"add_listener": add_listener}) - )() - - with pytest.raises(ValueError, match="Missing event type"): - component.with_listener()(int) # type: ignore - - add_listener.assert_not_called() - - def test_with_listener_missing_positional_event_arg(self): - async def callback(*, event: hikari.Event, **kwargs: str) -> None: - ... - - add_listener = mock.Mock() - component: tanjun.Component = types.new_class( - "StubComponent", (tanjun.Component,), exec_body=lambda ns: ns.update({"add_listener": add_listener}) - )() - - with pytest.raises(ValueError, match="Missing positional event argument"): - component.with_listener()(callback) # type: ignore - - add_listener.assert_not_called() - - def test_with_listener_no_args(self): - async def callback() -> None: - ... - - add_listener = mock.Mock() - component: tanjun.Component = types.new_class( - "StubComponent", (tanjun.Component,), exec_body=lambda ns: ns.update({"add_listener": add_listener}) - )() - - with pytest.raises(ValueError, match="Missing positional event argument"): - component.with_listener()(callback) # type: ignore - - add_listener.assert_not_called() - - def test_with_listener_with_multiple_events(self): add_listener = mock.Mock() component: tanjun.Component = types.new_class( "StubComponent", (tanjun.Component,), exec_body=lambda ns: ns.update({"add_listener": add_listener}) )() mock_callback = mock.Mock() + mock_listener = mock.Mock(event_types=[hikari.Event, hikari.GuildMessageCreateEvent], callback=mock_callback) - result = component.with_listener(hikari.GuildAvailableEvent, hikari.GuildLeaveEvent, hikari.GuildChannelEvent)( - mock_callback - ) - - assert result is mock_callback - add_listener.assert_has_calls( - [ - mock.call(hikari.GuildAvailableEvent, mock_callback), - mock.call(hikari.GuildLeaveEvent, mock_callback), - mock.call(hikari.GuildChannelEvent, mock_callback), - ] - ) - - def test_with_listener_with_type_hint(self): - async def callback(event: hikari.BanCreateEvent) -> None: - ... - - add_listener = mock.Mock() - component: tanjun.Component = types.new_class( - "StubComponent", (tanjun.Component,), exec_body=lambda ns: ns.update({"add_listener": add_listener}) - )() - - result = component.with_listener()(callback) - - assert result is callback - add_listener.assert_called_once_with(hikari.BanCreateEvent, callback) + result = component.with_listener(mock_listener) - def test_with_listener_with_type_hint_in_annotated(self): - async def callback(event: typing.Annotated[hikari.BanCreateEvent, 123, 321]) -> None: - ... - - add_listener = mock.Mock() - component: tanjun.Component = types.new_class( - "StubComponent", (tanjun.Component,), exec_body=lambda ns: ns.update({"add_listener": add_listener}) - )() - - result = component.with_listener()(callback) - - assert result is callback - add_listener.assert_called_once_with(hikari.BanCreateEvent, callback) - - def test_with_listener_with_positional_only_type_hint(self): - async def callback(event: hikari.BanDeleteEvent, /) -> None: - ... - - add_listener = mock.Mock() - component: tanjun.Component = types.new_class( - "StubComponent", (tanjun.Component,), exec_body=lambda ns: ns.update({"add_listener": add_listener}) - )() - - result = component.with_listener()(callback) - - assert result is callback - add_listener.assert_called_once_with(hikari.BanDeleteEvent, callback) - - def test_with_listener_with_var_positional_type_hint(self): - async def callback(*event: hikari.BanEvent) -> None: - ... - - add_listener = mock.Mock() - component: tanjun.Component = types.new_class( - "StubComponent", (tanjun.Component,), exec_body=lambda ns: ns.update({"add_listener": add_listener}) - )() - - result = component.with_listener()(callback) - - assert result is callback - add_listener.assert_called_once_with(hikari.BanEvent, callback) - - def test_with_listener_with_type_hint_union(self): - async def callback(event: typing.Union[hikari.RoleEvent, typing.Literal["ok"], hikari.GuildEvent, str]) -> None: - ... - - add_listener = mock.Mock() - component: tanjun.Component = types.new_class( - "StubComponent", (tanjun.Component,), exec_body=lambda ns: ns.update({"add_listener": add_listener}) - )() - - result = component.with_listener()(callback) - - assert result is callback - add_listener.assert_has_calls([mock.call(hikari.RoleEvent, callback), mock.call(hikari.GuildEvent, callback)]) - - def test_with_listener_with_type_hint_union_nested_annotated(self): - async def callback( - event: typing.Annotated[ - typing.Union[ - typing.Annotated[typing.Union[hikari.RoleEvent, hikari.ReactionDeleteEvent], 123, 321], - hikari.GuildEvent, - ], - True, - "meow", - ] - ) -> None: - ... - - add_listener = mock.Mock() - component: tanjun.Component = types.new_class( - "StubComponent", (tanjun.Component,), exec_body=lambda ns: ns.update({"add_listener": add_listener}) - )() - - result = component.with_listener()(callback) - - assert result is callback - add_listener.assert_has_calls( - [ - mock.call(hikari.RoleEvent, callback), - mock.call(hikari.ReactionDeleteEvent, callback), - mock.call(hikari.GuildEvent, callback), - ] - ) - - # These tests covers syntax which was introduced in 3.10 - if sys.version_info >= (3, 10): - - def test_with_listener_with_type_hint_310_union(self): - async def callback(event: hikari.ShardEvent | typing.Literal[""] | hikari.VoiceEvent | str) -> None: - ... - - add_listener = mock.Mock() - component: tanjun.Component = types.new_class( - "StubComponent", (tanjun.Component,), exec_body=lambda ns: ns.update({"add_listener": add_listener}) - )() - - result = component.with_listener()(callback) - - assert result is callback - add_listener.assert_has_calls( - [mock.call(hikari.ShardEvent, callback), mock.call(hikari.VoiceEvent, callback)] - ) - - def test_with_listener_with_type_hint_310_union_nested_annotated(self): - async def callback( - event: typing.Annotated[ - typing.Annotated[hikari.BanEvent | hikari.GuildEvent, 123, 321] | hikari.InviteEvent, True, "meow" - ] - ) -> None: - ... - - add_listener = mock.Mock() - component: tanjun.Component = types.new_class( - "StubComponent", (tanjun.Component,), exec_body=lambda ns: ns.update({"add_listener": add_listener}) - )() - - result = component.with_listener()(callback) - - assert result is callback - add_listener.assert_has_calls( - [ - mock.call(hikari.BanEvent, callback), - mock.call(hikari.GuildEvent, callback), - mock.call(hikari.InviteEvent, callback), - ] - ) + assert result is mock_listener + add_listener.assert_has_calls((mock.call(hikari.Event, mock_callback), mock.call(hikari.Event, mock_callback))) def test_add_on_close(self): mock_callback = mock.Mock() diff --git a/tests/test_components_future_annotations.py b/tests/test_components_future_annotations.py deleted file mode 100644 index 7c31d9be7..000000000 --- a/tests/test_components_future_annotations.py +++ /dev/null @@ -1,214 +0,0 @@ -# -*- coding: utf-8 -*- -# BSD 3-Clause License -# -# Copyright (c) 2020-2023, Faster Speeding -# All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are met: -# -# * Redistributions of source code must retain the above copyright notice, this -# list of conditions and the following disclaimer. -# -# * Redistributions in binary form must reproduce the above copyright notice, -# this list of conditions and the following disclaimer in the documentation -# and/or other materials provided with the distribution. -# -# * Neither the name of the copyright holder nor the names of its -# contributors may be used to endorse or promote products derived from -# this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -from __future__ import annotations - -import inspect -import sys -import types -import typing - -import hikari -import mock -import pytest - -import tanjun - - -class TestComponent: - def test_with_listener_no_provided_event(self): - async def callback(foo) -> None: # type: ignore - ... - - add_listener = mock.Mock() - component: tanjun.Component = types.new_class( - "StubComponent", (tanjun.Component,), exec_body=lambda ns: ns.update({"add_listener": add_listener}) - )() - - with pytest.raises(ValueError, match="Missing event argument annotation"): - component.with_listener()(callback) # pyright: ignore[reportUnknownArgumentType] - - add_listener.assert_not_called() - - def test_with_listener_no_provided_event_callback_has_no_signature(self): - with pytest.raises(ValueError, match=".+"): - inspect.Signature.from_callable(int) - - add_listener = mock.Mock() - component: tanjun.Component = types.new_class( - "StubComponent", (tanjun.Component,), exec_body=lambda ns: ns.update({"add_listener": add_listener}) - )() - - with pytest.raises(ValueError, match="Missing event type"): - component.with_listener()(int) # type: ignore - - add_listener.assert_not_called() - - def test_with_listener_with_type_hint(self): - async def callback(event: hikari.BanCreateEvent) -> None: - ... - - add_listener = mock.Mock() - component: tanjun.Component = types.new_class( - "StubComponent", (tanjun.Component,), exec_body=lambda ns: ns.update({"add_listener": add_listener}) - )() - - result = component.with_listener()(callback) - - assert result is callback - add_listener.assert_called_once_with(hikari.BanCreateEvent, callback) - - def test_with_listener_with_type_hint_in_annotated(self): - async def callback(event: typing.Annotated[hikari.BanCreateEvent, 123, 321]) -> None: - ... - - add_listener = mock.Mock() - component: tanjun.Component = types.new_class( - "StubComponent", (tanjun.Component,), exec_body=lambda ns: ns.update({"add_listener": add_listener}) - )() - - result = component.with_listener()(callback) - - assert result is callback - add_listener.assert_called_once_with(hikari.BanCreateEvent, callback) - - def test_with_listener_with_positional_only_type_hint(self): - async def callback(event: hikari.BanDeleteEvent, /) -> None: - ... - - add_listener = mock.Mock() - component: tanjun.Component = types.new_class( - "StubComponent", (tanjun.Component,), exec_body=lambda ns: ns.update({"add_listener": add_listener}) - )() - - result = component.with_listener()(callback) - - assert result is callback - add_listener.assert_called_once_with(hikari.BanDeleteEvent, callback) - - def test_with_listener_with_var_positional_type_hint(self): - async def callback(*event: hikari.BanEvent) -> None: - ... - - add_listener = mock.Mock() - component: tanjun.Component = types.new_class( - "StubComponent", (tanjun.Component,), exec_body=lambda ns: ns.update({"add_listener": add_listener}) - )() - - result = component.with_listener()(callback) - - assert result is callback - add_listener.assert_called_once_with(hikari.BanEvent, callback) - - def test_with_listener_with_type_hint_union(self): - async def callback(event: typing.Union[hikari.RoleEvent, typing.Literal["ok"], hikari.GuildEvent, str]) -> None: - ... - - add_listener = mock.Mock() - component: tanjun.Component = types.new_class( - "StubComponent", (tanjun.Component,), exec_body=lambda ns: ns.update({"add_listener": add_listener}) - )() - - result = component.with_listener()(callback) - - assert result is callback - add_listener.assert_has_calls([mock.call(hikari.RoleEvent, callback), mock.call(hikari.GuildEvent, callback)]) - - def test_with_listener_with_type_hint_union_nested_annotated(self): - async def callback( - event: typing.Annotated[ - typing.Union[ - typing.Annotated[typing.Union[hikari.RoleEvent, hikari.ReactionDeleteEvent], 123, 321], - hikari.GuildEvent, - ], - True, - "meow", - ] - ) -> None: - ... - - add_listener = mock.Mock() - component: tanjun.Component = types.new_class( - "StubComponent", (tanjun.Component,), exec_body=lambda ns: ns.update({"add_listener": add_listener}) - )() - - result = component.with_listener()(callback) - - assert result is callback - add_listener.assert_has_calls( - [ - mock.call(hikari.RoleEvent, callback), - mock.call(hikari.ReactionDeleteEvent, callback), - mock.call(hikari.GuildEvent, callback), - ] - ) - - # These tests covers syntax which was introduced in 3.10 - if sys.version_info >= (3, 10): - - def test_with_listener_with_type_hint_310_union(self): - async def callback(event: hikari.ShardEvent | typing.Literal[""] | hikari.VoiceEvent | str) -> None: - ... - - add_listener = mock.Mock() - component: tanjun.Component = types.new_class( - "StubComponent", (tanjun.Component,), exec_body=lambda ns: ns.update({"add_listener": add_listener}) - )() - - result = component.with_listener()(callback) - - assert result is callback - add_listener.assert_has_calls( - [mock.call(hikari.ShardEvent, callback), mock.call(hikari.VoiceEvent, callback)] - ) - - def test_with_listener_with_type_hint_310_union_nested_annotated(self): - async def callback( - event: typing.Annotated[ - typing.Annotated[hikari.BanEvent | hikari.GuildEvent, 123, 321] | hikari.InviteEvent, True, "meow" - ] - ) -> None: - ... - - add_listener = mock.Mock() - component: tanjun.Component = types.new_class( - "StubComponent", (tanjun.Component,), exec_body=lambda ns: ns.update({"add_listener": add_listener}) - )() - - result = component.with_listener()(callback) - - assert result is callback - add_listener.assert_has_calls( - [ - mock.call(hikari.BanEvent, callback), - mock.call(hikari.GuildEvent, callback), - mock.call(hikari.InviteEvent, callback), - ] - ) diff --git a/tests/test_listeners.py b/tests/test_listeners.py new file mode 100644 index 000000000..57245a77b --- /dev/null +++ b/tests/test_listeners.py @@ -0,0 +1,143 @@ +import inspect +import sys +import typing + +import hikari +import mock +import pytest + +import tanjun + + +class TestEventListener: + def test_init(self): + async def callback(foo) -> None: # type: ignore + ... + + event_listener = tanjun.listeners.EventListener(callback, hikari.ShardEvent, hikari.GuildEvent) + + assert event_listener.callback == callback + assert event_listener.event_types == (hikari.ShardEvent, hikari.GuildEvent) + + def test_init_with_listener_no_provided_event(self): + async def callback(foo) -> None: # type: ignore + ... + + with pytest.raises(ValueError, match="Missing event argument annotation"): + tanjun.listeners.EventListener(callback) # pyright: ignore[reportUnknownArgumentType] + + def test_init_with_listener_no_provided_event_callback_has_no_signature(self): + with pytest.raises(ValueError, match=".+"): + inspect.Signature.from_callable(int) + + with pytest.raises(ValueError, match="Missing event type"): + tanjun.listeners.EventListener(int) # type: ignore + + def test_init_with_listener_with_type_hint(self): + async def callback(event: hikari.BanCreateEvent) -> None: + ... + + event_listener = tanjun.listeners.EventListener(callback) + + assert event_listener.callback is callback + assert event_listener.event_types == [hikari.BanCreateEvent] + + def test_init_with_listener_with_type_hint_in_annotated(self): + async def callback(event: typing.Annotated[hikari.BanCreateEvent, 123, 321]) -> None: + ... + + event_listener = tanjun.listeners.EventListener(callback) + + assert event_listener.callback is callback + assert event_listener.event_types == [hikari.BanCreateEvent] + + def test_init_with_listener_with_positional_only_type_hint(self): + async def callback(event: hikari.BanDeleteEvent, /) -> None: + ... + + event_listener = tanjun.listeners.EventListener(callback) + + assert event_listener.callback is callback + assert event_listener.event_types == [hikari.BanDeleteEvent] + + def test_init_with_listener_with_var_positional_type_hint(self): + async def callback(*event: hikari.BanEvent) -> None: + ... + + event_listener = tanjun.listeners.EventListener(callback) + + assert event_listener.callback is callback + assert event_listener.event_types == [hikari.BanEvent] + + def test_init_with_listener_with_type_hint_union(self): + async def callback(event: typing.Union[hikari.RoleEvent, typing.Literal["ok"], hikari.GuildEvent, str]) -> None: + ... + + event_listener = tanjun.listeners.EventListener(callback) + + assert event_listener.callback is callback + assert event_listener.event_types == [hikari.RoleEvent, hikari.GuildEvent] + + def test_init_with_listener_with_type_hint_union_nested_annotated(self): + async def callback( + event: typing.Annotated[ + typing.Union[ + typing.Annotated[typing.Union[hikari.RoleEvent, hikari.ReactionDeleteEvent], 123, 321], + hikari.GuildEvent, + ], + True, + "meow", + ] + ) -> None: + ... + + event_listener = tanjun.listeners.EventListener(callback) + + assert event_listener.callback is callback + assert event_listener.event_types == [hikari.RoleEvent, hikari.ReactionDeleteEvent, hikari.GuildEvent] + + # These tests covers syntax which was introduced in 3.10 + if sys.version_info >= (3, 10): + + def test_init_with_listener_with_type_hint_310_union(self): + async def callback(event: hikari.ShardEvent | typing.Literal[""] | hikari.VoiceEvent | str) -> None: + ... + + event_listener = tanjun.listeners.EventListener(callback) + + assert event_listener.callback is callback + assert event_listener.event_types == [hikari.ShardEvent, hikari.VoiceEvent] + + def test_init_with_listener_with_type_hint_310_union_nested_annotated(self): + async def callback( + event: typing.Annotated[ + typing.Annotated[hikari.BanEvent | hikari.GuildEvent, 123, 321] | hikari.InviteEvent, True, "meow" + ] + ) -> None: + ... + + event_listener = tanjun.listeners.EventListener(callback) + + assert event_listener.callback is callback + assert event_listener.event_types == [hikari.BanEvent, hikari.GuildEvent, hikari.InviteEvent] + + def test_callback_property(self): + mock_callback = mock.Mock() + event_listener = tanjun.listeners.EventListener(mock_callback, hikari.GuildEvent) + + assert event_listener.callback is mock_callback + + def test_event_types_property(self): + mock_callback = mock.Mock() + event_listener = tanjun.listeners.EventListener(mock_callback, hikari.GuildEvent, hikari.ShardEvent) + + assert event_listener.event_types == (hikari.GuildEvent, hikari.ShardEvent) + + @pytest.mark.asyncio + async def test__call__(self): + mock_callback = mock.AsyncMock() + + event_listener = tanjun.listeners.EventListener(mock_callback, hikari.ShardEvent) + await event_listener("testing", a="test") + + mock_callback.assert_awaited_once_with("testing", a="test") diff --git a/tests/test_listeners_future_annotations.py b/tests/test_listeners_future_annotations.py new file mode 100644 index 000000000..e547c95de --- /dev/null +++ b/tests/test_listeners_future_annotations.py @@ -0,0 +1,174 @@ +# -*- coding: utf-8 -*- +# BSD 3-Clause License +# +# Copyright (c) 2020-2023, Faster Speeding +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, this +# list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# * Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# -*- coding: utf-8 -*- +# BSD 3-Clause License +# +# Copyright (c) 2020-2023, Faster Speeding +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, this +# list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# * Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +from __future__ import annotations + +import inspect +import sys +import typing + +import hikari +import pytest + +import tanjun + + +class TestEventListener: + def test_init_with_listener_no_provided_event(self): + async def callback(foo) -> None: # type: ignore + ... + + with pytest.raises(ValueError, match="Missing event argument annotation"): + tanjun.listeners.EventListener(callback) # pyright: ignore[reportUnknownArgumentType] + + def test_init_with_listener_no_provided_event_callback_has_no_signature(self): + with pytest.raises(ValueError, match=".+"): + inspect.Signature.from_callable(int) + + with pytest.raises(ValueError, match="Missing event type"): + tanjun.listeners.EventListener(int) # type: ignore + + def test_init_with_listener_with_type_hint(self): + async def callback(event: hikari.BanCreateEvent) -> None: + ... + + event_listener = tanjun.listeners.EventListener(callback) + + assert event_listener.callback is callback + assert event_listener.event_types == [hikari.BanCreateEvent] + + def test_init_with_listener_with_type_hint_in_annotated(self): + async def callback(event: typing.Annotated[hikari.BanCreateEvent, 123, 321]) -> None: + ... + + event_listener = tanjun.listeners.EventListener(callback) + + assert event_listener.callback is callback + assert event_listener.event_types == [hikari.BanCreateEvent] + + def test_init_with_listener_with_positional_only_type_hint(self): + async def callback(event: hikari.BanDeleteEvent, /) -> None: + ... + + event_listener = tanjun.listeners.EventListener(callback) + + assert event_listener.callback is callback + assert event_listener.event_types == [hikari.BanDeleteEvent] + + def test_init_with_listener_with_var_positional_type_hint(self): + async def callback(*event: hikari.BanEvent) -> None: + ... + + event_listener = tanjun.listeners.EventListener(callback) + + assert event_listener.callback is callback + assert event_listener.event_types == [hikari.BanEvent] + + def test_init_with_listener_with_type_hint_union(self): + async def callback(event: typing.Union[hikari.RoleEvent, typing.Literal["ok"], hikari.GuildEvent, str]) -> None: + ... + + event_listener = tanjun.listeners.EventListener(callback) + + assert event_listener.callback is callback + assert event_listener.event_types == [hikari.RoleEvent, hikari.GuildEvent] + + def test_init_with_listener_with_type_hint_union_nested_annotated(self): + async def callback( + event: typing.Annotated[ + typing.Union[ + typing.Annotated[typing.Union[hikari.RoleEvent, hikari.ReactionDeleteEvent], 123, 321], + hikari.GuildEvent, + ], + True, + "meow", + ] + ) -> None: + ... + + event_listener = tanjun.listeners.EventListener(callback) + + assert event_listener.callback is callback + assert event_listener.event_types == [hikari.RoleEvent, hikari.ReactionDeleteEvent, hikari.GuildEvent] + + # These tests covers syntax which was introduced in 3.10 + if sys.version_info >= (3, 10): + + def test_init_with_listener_with_type_hint_310_union(self): + async def callback(event: hikari.ShardEvent | typing.Literal[""] | hikari.VoiceEvent | str) -> None: + ... + + event_listener = tanjun.listeners.EventListener(callback) + + assert event_listener.callback is callback + assert event_listener.event_types == [hikari.ShardEvent, hikari.VoiceEvent] + + def test_init_with_listener_with_type_hint_310_union_nested_annotated(self): + async def callback( + event: typing.Annotated[ + typing.Annotated[hikari.BanEvent | hikari.GuildEvent, 123, 321] | hikari.InviteEvent, True, "meow" + ] + ) -> None: + ... + + event_listener = tanjun.listeners.EventListener(callback) + + assert event_listener.callback is callback + assert event_listener.event_types == [hikari.BanEvent, hikari.GuildEvent, hikari.InviteEvent] From 458b592357fddba286b570a2f7b27cc611f188fe Mon Sep 17 00:00:00 2001 From: davfsa Date: Sun, 25 Jun 2023 20:13:30 +0200 Subject: [PATCH 2/3] Update documentation Switch to using better method to get python source-code --- docs/usage.md | 70 ++++++++++++++++----------------- docs_src/usage.py | 99 ++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 132 insertions(+), 37 deletions(-) diff --git a/docs/usage.md b/docs/usage.md index 61b8cf989..1c6f06e6b 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -12,7 +12,7 @@ gateway-based message and application command execution. To run Tanjun you'll want to link it to a Hikari bot. ```py ---8<-- "./docs_src/usage.py:26:31" +--8<-- "./docs_src/usage.py:gateway_bot_example" ``` Here a Tanjun client is linked to a gateway bot instance to enable both @@ -26,7 +26,7 @@ commands and context menus on startup, and `mention_prefix=True` allows the bot's message commands to be triggered by starting a command call with `@bot`. ```py ---8<-- "./docs_src/usage.py:35:37" +--8<-- "./docs_src/usage.py:rest_bot_example" ``` And here a Tanjun client is linked to a REST server bot instance to enable @@ -44,7 +44,7 @@ provide a cross-compatible alternative for these (which also supports dependency injection). ```py ---8<-- "./docs_src/usage.py:41:50" +--8<-- "./docs_src/usage.py:client_lifetime_example" ``` ## Managing bot functionality @@ -54,7 +54,7 @@ group bot functionality, storing functionality such as event listeners, commands, scheduled callbacks, and client callbacks. ```py ---8<-- "./docs_src/usage.py:54:63" +--8<-- "./docs_src/usage.py:components_example" ``` The `with_` methods on [Component][tanjun.components.Component] allow @@ -63,7 +63,7 @@ through a decorator call; the relevant `add_` functions allow adding functionality through chained calls. ```py ---8<-- "./docs_src/usage.py:67:71" +--8<-- "./docs_src/usage.py:load_from_scope_example" ``` Alternatively, functionality which is represented by a dedicated object can be @@ -83,20 +83,20 @@ add a component to a client, you can also declare "loaders" and "unloaders" for a module to more ergonomically load this functionality into a client. ```py ---8<-- "./docs_src/usage.py:75:83" +--8<-- "./docs_src/usage.py:as_loader_example" ``` You can either declare one or more custom loaders and unloaders as shown above ```py ---8<-- "./docs_src/usage.py:87:89" +--8<-- "./docs_src/usage.py:make_loader_example" ``` or use [make_loader][tanjun.components.Component.make_loader] to generate a loader and unloader for the component. ```py ---8<-- "./docs_src/usage.py:93:97" +--8<-- "./docs_src/usage.py:loading_example" ``` Modules with loaders can then be loaded into a client by calling @@ -120,7 +120,7 @@ All command callbacks must be asynchronous and can use dependency injection. ### Slash commands ```py ---8<-- "./docs_src/usage.py:101:104" +--8<-- "./docs_src/usage.py:slash_command_example" ``` Slash commands represent the commands you see when you start typing with "/" in @@ -150,7 +150,7 @@ converters found in [tanjun.conversion][]) similarly to message command arguments. ```py ---8<-- "./docs_src/usage.py:108:118" +--8<-- "./docs_src/usage.py:slash_command_group_example" ``` Slash commands can be stored in groups where the above example will be shown in @@ -162,7 +162,7 @@ see [slash_command_group][tanjun.commands.slash.slash_command_group]. ### Message commands ```py ---8<-- "./docs_src/usage.py:122:132" +--8<-- "./docs_src/usage.py:message_command_example" ``` Message commands are triggered based on chat messages where the client's @@ -178,7 +178,7 @@ either [Client.from_gateway_bot][tanjun.clients.Client.from_gateway_bot] or Mention prefixes work even if the `MESSAGE_CONTENT` intent is not declared. ```py ---8<-- "./docs_src/usage.py:136:152" +--8<-- "./docs_src/usage.py:message_command_group_example" ``` Message command groups are a collection of message commands under a shared name @@ -213,7 +213,7 @@ more configuration see [tanjun.parsing][] and for the standard converters see ### Context menus ```py ---8<-- "./docs_src/usage.py:156:164" +--8<-- "./docs_src/usage.py:context_menu_example" ``` Context menus represent the application commands shown when you click on a user @@ -227,7 +227,7 @@ Previously you've seen how to manually declare command options per command type, now it's time to go higher. ```py ---8<-- "./docs_src/usage.py:179:194" +--8<-- "./docs_src/usage.py:annotations_example" ``` [tanjun.annotations][] provides a simple way to declare the arguments for both @@ -251,7 +251,7 @@ When using `follow_wrapped` the relevant decorator will be applied to all the compatible `as_{}_command` decorator calls below it in the chain. ```py ---8<-- "./docs_src/usage.py:201:206" +--8<-- "./docs_src/usage.py:wrapped_command_example" ``` While the previous command examples have typed `ctx` as a context type that's @@ -263,7 +263,7 @@ types. ## Responding to commands ```py ---8<-- "./docs_src/usage.py:210:223" +--8<-- "./docs_src/usage.py:responding_to_commands_example" ``` [Context.respond][tanjun.abc.Context.respond] is used to respond to a command @@ -274,7 +274,7 @@ when `ensure_result=True` is passed. ### Ephemeral responses ```py ---8<-- "./docs_src/usage.py:227:238" +--8<-- "./docs_src/usage.py:ephemeral_response_example" ``` Ephemeral responses are a slash command and context menu exclusive feature which @@ -323,7 +323,7 @@ dynamically return choice suggestions to a user as they type a string option. Autocomplete callbacks must be asynchronous and support dependency injection. ```py ---8<-- "./docs_src/usage.py:242:256" +--8<-- "./docs_src/usage.py:autocomplete_example" ``` To set the results for an autocomplete interaction call @@ -342,13 +342,13 @@ callbacks, checks, hook callbacks, event listeners, schedule callbacks) through [Alluka][alluka]. ```py ---8<-- "./docs_src/usage.py:268:270" +--8<-- "./docs_src/usage.py:set_client_deps_example" ``` Here we set the dependencies for the types `Foo` and `Bar`. ```py ---8<-- "./docs_src/usage.py:274:278" +--8<-- "./docs_src/usage.py:require_deps_example" ``` And here we declare a command callback as taking the client set values for @@ -410,7 +410,7 @@ Checks are functions that run before command execution to decide whether a command or group of commands matches a context and should be called with it. ```py ---8<-- "./docs_src/usage.py:282:288" +--8<-- "./docs_src/usage.py:standard_check_example" ``` There's a collection of standard checks in [tanjun.checks][] which work @@ -419,7 +419,7 @@ will care about for the standard checks is the `error_message` argument which lets you adjust the response messages these send when they fail. ```py ---8<-- "./docs_src/usage.py:301:319" +--8<-- "./docs_src/usage.py:using_checks_example" ``` Checks (both custom and standard) can be added to clients, components, and @@ -430,7 +430,7 @@ chain. Checks on a client, component, or command group will be used for every child command. ```py ---8<-- "./docs_src/usage.py:323:327" +--8<-- "./docs_src/usage.py:custom_check_example" ``` Custom checks can be made by making a function with either the signature @@ -452,28 +452,28 @@ There are several different kinds of hooks which all support dependency injection and may be synchronous or asynchronous: ```py ---8<-- "./docs_src/usage.py:331:335" +--8<-- "./docs_src/usage.py:pre_execution_hook_example" ``` Pre-execution hooks are called before the execution of a command but after command matching has finished and all the relevant checks have passed. ```py ---8<-- "./docs_src/usage.py:339:341" +--8<-- "./docs_src/usage.py:post_execution_hook_example" ``` Post-execution hooks are called after a command has finished executing, regardless of whether it passed or failed. ```py ---8<-- "./docs_src/usage.py:345:347" +--8<-- "./docs_src/usage.py:success_hook_example" ``` Success hooks are called after a command has finished executing successfully (without raising any errors). ```py ---8<-- "./docs_src/usage.py:351:353" +--8<-- "./docs_src/usage.py:error_hook_example" ``` Error hooks are called when command's execution is ended early by an error raise @@ -488,7 +488,7 @@ error and [None][] acts as no vote. In the case of a tie the error will be re-raised. ```py ---8<-- "./docs_src/usage.py:357:359" +--8<-- "./docs_src/usage.py:parser_error_hook_example" ``` Parser error hooks are called when the argument parsing of a message command @@ -500,7 +500,7 @@ Concurrency limiters allow you to limit how many calls can be made to a group of commands concurrently. ```py ---8<-- "./docs_src/usage.py:363:369" +--8<-- "./docs_src/usage.py:concurrency_limiter_config_example" ``` Here [InMemoryConcurrencyLimiter][tanjun.dependencies.InMemoryConcurrencyLimiter] @@ -515,7 +515,7 @@ being used to set this limiter for a client (note that clients can only have 1 linked limiter). ```py ---8<-- "./docs_src/usage.py:373:377" +--8<-- "./docs_src/usage.py:assign_concurrency_limit_example" ``` And here we use [with_concurrency_limit][tanjun.dependencies.with_concurrency_limit] @@ -529,7 +529,7 @@ more information on the resources concurrency can be limited by see Cooldowns limit how often a group of commands can be called. ```py ---8<-- "./docs_src/usage.py:381:387" +--8<-- "./docs_src/usage.py:cooldown_config_example" ``` Here [InMemoryCooldownManager][tanjun.dependencies.InMemoryCooldownManager] @@ -544,7 +544,7 @@ being used to set this cooldown manager for a client (note that clients can only have 1 linked cooldown manager). ```py ---8<-- "./docs_src/usage.py:391:395" +--8<-- "./docs_src/usage.py:assign_cooldown_example" ``` And here we use [with_cooldown][tanjun.dependencies.with_cooldown] @@ -564,7 +564,7 @@ of a field. Localisation on Discord is limited to the locales Discord supports ### Localising command declarations ```py ---8<-- "./docs_src/usage.py:399:401" +--8<-- "./docs_src/usage.py:localisation_example" ``` For fields which support localisation you've previously seen a single string @@ -580,7 +580,7 @@ setting/overriding the locale-specific variants used for localised fields such as error message responses and application fields globally. ```py ---8<-- "./docs_src/usage.py:405:417" +--8<-- "./docs_src/usage.py:client_localiser_example" ``` Specific fields may be overridden by their ID as shown above. There is no @@ -604,7 +604,7 @@ It's highly recommended that 3rd party libraries match this format if possible. ### Localising command responses ```py ---8<-- "./docs_src/usage.py:421:440" +--8<-- "./docs_src/usage.py:response_localisation_example" ``` [tanjun.abc.AppCommandContext.interaction][] (base class for both diff --git a/docs_src/usage.py b/docs_src/usage.py index fadcf34cb..c86145e40 100644 --- a/docs_src/usage.py +++ b/docs_src/usage.py @@ -23,21 +23,26 @@ def gateway_bot_example() -> None: + # --8<-- [start:gateway_bot_example] bot = hikari.impl.GatewayBot("TOKEN") client = tanjun.Client.from_gateway_bot(bot, declare_global_commands=True, mention_prefix=True) ... bot.run() + # --8<-- [end:gateway_bot_example] def rest_bot_example() -> None: + # --8<-- [start:rest_bot_example] bot = hikari.impl.RESTBot("TOKEN", hikari.TokenType.BOT) tanjun.Client.from_rest_bot(bot, bot_managed=True, declare_global_commands=True) bot.run() + # --8<-- [end:rest_bot_example] def client_lifetime_example(bot: hikari.GatewayBotAware) -> None: + # --8<-- [start:client_lifetime_example] client = tanjun.Client.from_gateway_bot(bot) @client.with_client_callback(tanjun.ClientCallbackNames.STARTING) @@ -48,9 +53,11 @@ async def on_closed(session: alluka.Injected[aiohttp.ClientSession]) -> None: await session.close() client.add_client_callback(tanjun.ClientCallbackNames.CLOSED, on_closed) + # --8<-- [end:client_lifetime_example] def components_example() -> None: + # --8<-- [start:components_example] component = tanjun.Component() @component.with_command @@ -58,20 +65,26 @@ def components_example() -> None: async def slash_command(ctx: tanjun.abc.SlashContext) -> None: ... - @component.with_listener() + @component.with_listener + @tanjun.as_event_listener() async def event_listener(event: hikari.Event) -> None: ... + # --8<-- [end:components_example] + def load_from_scope_example() -> None: + # --8<-- [start:load_from_scope_example] @tanjun.as_message_command("name") async def command(ctx: tanjun.abc.MessageContext) -> None: ... component = tanjun.Component().load_from_scope() + # --8<-- [end:load_from_scope_example] def as_loader_example() -> None: + # --8<-- [start:as_loader_example] component = tanjun.Component().load_from_scope() @tanjun.as_loader @@ -82,29 +95,39 @@ def load(client: tanjun.Client) -> None: def unload(client: tanjun.Client) -> None: client.remove_component(component) + # --8<-- [end:as_loader_example] + def make_loader_example() -> None: + # --8<-- [start:make_loader_example] component = tanjun.Component().load_from_scope() loader = component.make_loader() + # --8<-- [end:make_loader_example] def loading_example(bot: hikari.GatewayBotAware) -> None: + # --8<-- [start:loading_example] ( tanjun.Client.from_gateway_bot(bot) .load_directory("./bot/components", namespace="bot.components") .load_modules("bot.owner") ) + # --8<-- [end:loading_example] def slash_command_example() -> None: + # --8<-- [start:slash_command_example] @tanjun.with_str_slash_option("option", "description") @tanjun.as_slash_command("name", "description") async def slash_command(ctx: tanjun.abc.SlashContext) -> None: ... + # --8<-- [end:slash_command_example] + def slash_command_group_example() -> None: + # --8<-- [start:slash_command_group_example] ding_group = tanjun.slash_command_group("ding", "ding group") @ding_group.as_sub_command("dong", "dong command") @@ -117,8 +140,11 @@ async def dong_command(ctx: tanjun.abc.SlashContext) -> None: async def ding_command(ctx: tanjun.abc.SlashContext) -> None: ... + # --8<-- [end:slash_command_group_example] + def message_command_example(bot: hikari.GatewayBotAware) -> None: + # --8<-- [start:message_command_example] tanjun.Client.from_gateway_bot(bot).add_prefix("!") ... @@ -131,8 +157,11 @@ def message_command_example(bot: hikari.GatewayBotAware) -> None: async def message_command(ctx: tanjun.abc.MessageContext) -> None: ... + # --8<-- [end:message_command_example] + def message_command_group_example() -> None: + # --8<-- [start:message_command_group_example] # prefixes=["!"] @tanjun.as_message_command_group("groupy") @@ -151,8 +180,11 @@ async def tour_group(ctx: tanjun.abc.MessageContext): async def de_france_command(ctx: tanjun.abc.MessageContext): ... + # --8<-- [end:message_command_group_example] + def context_menu_example(component: tanjun.Component) -> None: + # --8<-- [start:context_menu_example] @component.with_command @tanjun.as_message_menu("name") async def message_menu_command(ctx: tanjun.abc.MenuContext, message: hikari.Message) -> None: @@ -163,6 +195,8 @@ async def message_menu_command(ctx: tanjun.abc.MenuContext, message: hikari.Mess async def user_menu_command(ctx: tanjun.abc.MenuContext, user: hikari.User) -> None: ... + # --8<-- [end:context_menu_example] + class Video: ... @@ -176,6 +210,7 @@ def get_video(value: str) -> Video: def annotations_example() -> None: + # --8<-- [start:annotations_example] from typing import Annotated, Optional from tanjun.annotations import Bool, Converted, Int, Ranged, Str, User @@ -193,11 +228,14 @@ async def command( ) -> None: ... + # --8<-- [end:annotations_example] + # isort: on def wrapped_command_example() -> None: + # --8<-- [start:wrapped_command_example] @tanjun.annotations.with_annotated_args(follow_wrapped=True) @tanjun.with_guild_check(follow_wrapped=True) @tanjun.as_slash_command("name", "description") @@ -205,8 +243,11 @@ def wrapped_command_example() -> None: async def command(ctx: tanjun.abc.Context) -> None: ... + # --8<-- [end:wrapped_command_example] + def responding_to_commands_example() -> None: + # --8<-- [start:responding_to_commands_example] @tanjun.annotations.with_annotated_args(follow_wrapped=True) @tanjun.as_slash_command("name", "description") @tanjun.as_message_command("name") @@ -222,8 +263,11 @@ async def command( ensure_result=True, ) + # --8<-- [end:responding_to_commands_example] + def ephemeral_response_example(component: tanjun.Component) -> None: + # --8<-- [start:ephemeral_response_example] # All this command's responses will be ephemeral. @component.with_command @tanjun.as_slash_command("name", "description", default_to_ephemeral=True) @@ -237,8 +281,11 @@ async def command_2(ctx: tanjun.abc.MenuContext, user: hikari.User) -> None: await ctx.respond("meow") # public response await ctx.create_followup("finished the thing", ephemeral=True) # private response + # --8<-- [end:ephemeral_response_example] + def autocomplete_example(component: tanjun.Component) -> None: + # --8<-- [start:autocomplete_example] @component.with_command @tanjun.with_str_slash_option("opt1", "description") @tanjun.with_str_slash_option("opt2", "description", default=None) @@ -254,6 +301,7 @@ async def opt2_autocomplete(ctx: tanjun.abc.AutocompleteContext, value: str) -> await ctx.set_choices({"name": "value", "other_name": "other_value"}) slash_command.set_str_autocomplete("opt2", opt2_autocomplete) + # --8<-- [end:autocomplete_example] class Foo: @@ -265,20 +313,26 @@ class Bar: def set_client_deps_example(bot: hikari.GatewayBotAware) -> None: + # --8<-- [start:set_client_deps_example] client = tanjun.Client.from_gateway_bot(bot) client.set_type_dependency(Foo, Foo()) client.set_type_dependency(Bar, Bar()) + # --8<-- [end:set_client_deps_example] -def require_deps() -> None: +def require_deps_example() -> None: + # --8<-- [start:require_deps_example] @tanjun.as_slash_command("name", "description") async def command( ctx: tanjun.abc.SlashContext, foo_impl: alluka.Injected[Foo], bar_impl: Bar = alluka.inject(type=Bar) ) -> None: ... + # --8<-- [end:require_deps_example] + def standard_check_example() -> None: + # --8<-- [start:standard_check_example] @tanjun.with_guild_check(follow_wrapped=True) @tanjun.with_author_permission_check(hikari.Permissions.BAN_MEMBERS) @tanjun.with_own_permission_check(hikari.Permissions.BAN_MEMBERS, follow_wrapped=True) @@ -287,6 +341,8 @@ def standard_check_example() -> None: async def command(ctx: tanjun.abc.Context) -> None: ... + # --8<-- [end:standard_check_example] + class DbResult: banned: bool = False @@ -298,6 +354,7 @@ async def get_user(self, user: hikari.Snowflake) -> DbResult: def using_checks_example() -> None: + # --8<-- [start:using_checks_example] component = ( tanjun.Component() .add_check(tanjun.checks.GuildCheck()) @@ -318,48 +375,69 @@ async def db_check(ctx: tanjun.abc.Context, db: alluka.Injected[Db]) -> bool: async def owner_only_command(ctx: tanjun.abc.Context): ... + # --8<-- [end:using_checks_example] + def custom_check_example() -> None: + # --8<-- [start:custom_check_example] def check(ctx: tanjun.abc.Context) -> bool: if ctx.author.discriminator % 2: raise tanjun.CommandError("You are not one of the chosen ones") return True + # --8<-- [end:custom_check_example] + def pre_execution_hook_example() -> None: + # --8<-- [start:pre_execution_hook_example] hooks = tanjun.AnyHooks() @hooks.with_pre_execution # hooks.add_pre_execution async def pre_execution_hook(ctx: tanjun.abc.Context) -> None: ... + # --8<-- [end:pre_execution_hook_example] + def post_execution_hook_example(hooks: tanjun.abc.AnyHooks) -> None: + # --8<-- [start:post_execution_hook_example] @hooks.with_post_execution # hooks.add_post_execution async def post_execution_hook(ctx: tanjun.abc.Context) -> None: ... + # --8<-- [end:post_execution_hook_example] + def success_hook_example(hooks: tanjun.abc.AnyHooks) -> None: + # --8<-- [start:success_hook_example] @hooks.with_on_success # hooks.add_success_hook async def success_hook(ctx: tanjun.abc.Context) -> None: ... + # --8<-- [end:success_hook_example] + def error_hook_example(hooks: tanjun.abc.AnyHooks) -> None: + # --8<-- [start:error_hook_example] @hooks.with_on_error # hooks.add_on_error async def error_hook(ctx: tanjun.abc.Context, error: Exception) -> typing.Optional[bool]: ... + # --8<-- [end:error_hook_example] + def parser_error_hook_example(hooks: tanjun.abc.AnyHooks) -> None: + # --8<-- [start:parser_error_hook_example] @hooks.with_on_parser_error # hooks.add_on_parser_error async def parser_error_hook(ctx: tanjun.abc.Context, error: tanjun.ParserError) -> None: ... + # --8<-- [end:parser_error_hook_example] + def concurrency_limiter_config_example(bot: hikari.GatewayBotAware) -> None: + # --8<-- [start:concurrency_limiter_config_example] client = tanjun.Client.from_gateway_bot(bot) ( tanjun.InMemoryConcurrencyLimiter() @@ -367,17 +445,22 @@ def concurrency_limiter_config_example(bot: hikari.GatewayBotAware) -> None: .disable_bucket("plugin.meta") .add_to_client(client) ) + # --8<-- [end:concurrency_limiter_config_example] def assign_concurrency_limit_example() -> None: + # --8<-- [start:assign_concurrency_limit_example] @tanjun.with_concurrency_limit("main_commands", follow_wrapped=True) @tanjun.as_message_command("name") @tanjun.as_slash_command("name", "description") async def user_command(ctx: tanjun.abc.Context) -> None: ... + # --8<-- [end:assign_concurrency_limit_example] + def cooldown_config_example(bot: hikari.GatewayBotAware) -> None: + # --8<-- [start:cooldown_config_example] client = tanjun.Client.from_gateway_bot(bot) ( tanjun.InMemoryCooldownManager() @@ -385,23 +468,31 @@ def cooldown_config_example(bot: hikari.GatewayBotAware) -> None: .disable_bucket("plugin.meta") .add_to_client(client) ) + # --8<-- [end:cooldown_config_example] def assign_cooldown_example() -> None: + # --8<-- [start:assign_cooldown_example] @tanjun.with_cooldown("main_commands", follow_wrapped=True) @tanjun.as_message_command("name") @tanjun.as_slash_command("name", "description") async def user_command(ctx: tanjun.abc.Context) -> None: ... + # --8<-- [end:assign_cooldown_example] + def localisation_example() -> None: + # --8<-- [start:localisation_example] @tanjun.as_slash_command({hikari.Locale.EN_US: "Hola"}, "description") async def command(ctx: tanjun.abc.Context) -> None: ... + # --8<-- [end:localisation_example] + def client_localiser_example(bot: hikari.GatewayBotAware) -> None: + # --8<-- [start:client_localiser_example] client = tanjun.Client.from_gateway_bot(bot) ( @@ -415,9 +506,11 @@ def client_localiser_example(bot: hikari.GatewayBotAware) -> None: ) .add_to_client(client) ) + # --8<-- [end:client_localiser_example] def response_localisation_example() -> None: + # --8<-- [start:response_localisation_example] LOCALISED_RESPONSES: dict[str, str] = { hikari.Locale.DA: "Hej", hikari.Locale.DE: "Hallo", @@ -438,3 +531,5 @@ def response_localisation_example() -> None: @tanjun.as_slash_command("name", "description") async def as_slash_command(ctx: tanjun.abc.SlashContext) -> None: await ctx.respond(LOCALISED_RESPONSES.get(ctx.interaction.locale, "hello")) + + # --8<-- [end:response_localisation_example] From b183c13f887ad5b737f5247ea0e319eed53ca07f Mon Sep 17 00:00:00 2001 From: davfsa Date: Sun, 25 Jun 2023 20:17:39 +0200 Subject: [PATCH 3/3] Add missing abstractmethod --- tanjun/abc.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tanjun/abc.py b/tanjun/abc.py index ed9b193e5..b60589183 100644 --- a/tanjun/abc.py +++ b/tanjun/abc.py @@ -3590,6 +3590,7 @@ def remove_listener(self, event: type[_EventT], listener: ListenerCallbackSig[_E The component to enable chained calls. """ + @abc.abstractmethod def with_listener( self, event_listener: listeners_.EventListener[_ListenerCallbackSigT] ) -> listeners_.EventListener[_ListenerCallbackSigT]: