diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 088fea8a1d..52feadd1fb 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -28,6 +28,12 @@ match that of the versioning scheme. There are utilities under `hikari.internal. To aid with the generation of `CHANGELOG.md` as well as the releases changelog we use `towncrier`. +You will need to install `towncrier` and `hikari` from source before making changelog additions. +```bash +pip install towncrier +pip install -e . +``` + For every pull request made to this project, there should be a short explanation of the change under `changes/` with the following format: `{pull_request_number}.{type}.md`, @@ -44,7 +50,7 @@ description so that a maintainer can skip the job that checks for newly added fr Best way to create the fragments is to run `towncrier create {pull_request_number}.{type}.md` after creating the pull request, edit the created file and committing the changes. -Multiple fragments types can be created per pull request if it covers multiple areas. +Multiple fragment types can be created per pull request if it covers multiple areas. # Branches @@ -65,6 +71,11 @@ with a small description of the branch. We have nox to help out with running pipelines locally and provides some helpful functionality. +You will need to install `nox` locally before running any pipelines. +```bash +pip install nox +``` + Nox is similar to tox, but uses a pure Python configuration instead of an INI based configuration. Nox and tox are both tools for generating virtual environments and running commands in those environments. Examples of usage include installing, configuring, and running flake8, running pytest, etc. diff --git a/changes/804.doc.md b/changes/804.doc.md new file mode 100644 index 0000000000..7e1e6309f9 --- /dev/null +++ b/changes/804.doc.md @@ -0,0 +1 @@ +Add docstrings to the remaining undocumented `GatewayBot` methods diff --git a/changes/804.feature.md b/changes/804.feature.md new file mode 100644 index 0000000000..3c7eedd5b9 --- /dev/null +++ b/changes/804.feature.md @@ -0,0 +1 @@ +Add the `add_component` method to `hikari.api.special_endpoints.ActionRowBuilder` diff --git a/hikari/api/event_manager.py b/hikari/api/event_manager.py index f0dc6810da..90ec286c5e 100644 --- a/hikari/api/event_manager.py +++ b/hikari/api/event_manager.py @@ -231,9 +231,11 @@ async def on_everyone_mentioned(event): See Also -------- - Subscribe: `hikari.api.event_manager.EventManager.subscribe` + Listen: `hikari.api.event_manager.EventManager.listen` Stream: `hikari.api.event_manager.EventManager.stream` - Wait for: `hikari.api.event_manager.EventManager.wait_for` + Subscribe: `hikari.api.event_manager.EventManager.subscribe` + Unsubscribe: `hikari.api.event_manager.EventManager.unsubscribe` + Wait_for: `hikari.api.event_manager.EventManager.wait_for` """ # Yes, this is not generic. The reason for this is MyPy complains about @@ -271,9 +273,11 @@ async def on_message(event): See Also -------- + Dispatch: `hikari.api.event_manager.EventManager.dispatch` Listen: `hikari.api.event_manager.EventManager.listen` Stream: `hikari.api.event_manager.EventManager.stream` - Wait for: `hikari.api.event_manager.EventManager.wait_for` + Unsubscribe: `hikari.api.event_manager.EventManager.unsubscribe` + Wait_for: `hikari.api.event_manager.EventManager.wait_for` """ # Yes, this is not generic. The reason for this is MyPy complains about @@ -306,6 +310,14 @@ async def on_message(event): bot.unsubscribe(MessageCreateEvent, on_message) ``` + + See Also + -------- + Dispatch: `hikari.api.event_manager.EventManager.dispatch` + Listen: `hikari.api.event_manager.EventManager.listen` + Stream: `hikari.api.event_manager.EventManager.stream` + Subscribe: `hikari.api.event_manager.EventManager.subscribe` + Wait_for: `hikari.api.event_manager.EventManager.wait_for` """ @abc.abstractmethod @@ -373,7 +385,7 @@ def listen( Stream: `hikari.api.event_manager.EventManager.stream` Subscribe: `hikari.api.event_manager.EventManager.subscribe` Unsubscribe: `hikari.api.event_manager.EventManager.unsubscribe` - Wait for: `hikari.api.event_manager.EventManager.wait_for` + Wait_for: `hikari.api.event_manager.EventManager.wait_for` """ @abc.abstractmethod @@ -439,7 +451,7 @@ def stream( Listen: `hikari.api.event_manager.EventManager.listen` Subscribe: `hikari.api.event_manager.EventManager.subscribe` Unsubscribe: `hikari.api.event_manager.EventManager.unsubscribe` - Wait for: `hikari.api.event_manager.EventManager.wait_for` + Wait_for: `hikari.api.event_manager.EventManager.wait_for` """ @abc.abstractmethod @@ -486,8 +498,9 @@ async def wait_for( See Also -------- + Dispatch: `hikari.api.event_manager.EventManager.dispatch` Listen: `hikari.api.event_manager.EventManager.listen` Stream: `hikari.api.event_manager.EventManager.stream` Subscribe: `hikari.api.event_manager.EventManager.subscribe` - Dispatch: `hikari.api.event_manager.EventManager.dispatch` + Unsubscribe: `hikari.api.event_manager.EventManager.unsubscribe` """ diff --git a/hikari/api/special_endpoints.py b/hikari/api/special_endpoints.py index 2f8a900395..6d73ca5f4d 100644 --- a/hikari/api/special_endpoints.py +++ b/hikari/api/special_endpoints.py @@ -1491,6 +1491,30 @@ def components(self) -> typing.Sequence[ComponentBuilder]: Sequence of the component builders registered within this action row. """ + @abc.abstractmethod + def add_component( + self: _T, + component: ComponentBuilder, + /, + ) -> _T: + """Add a component to this action row builder. + + !!! warning + It is generally better to use `ActionRowBuilder.add_button` + and `ActionRowBuilder.add_select_menu` to add your + component to the builder. Those methods utilize this one. + + Parameters + ---------- + component : ComponentBuilder + The component builder to add to the action row. + + Returns + ------- + ActionRowBuilder + The builder object to enable chained calls. + """ + @typing.overload @abc.abstractmethod def add_button( diff --git a/hikari/guilds.py b/hikari/guilds.py index 3cba854f44..6c2202da6a 100644 --- a/hikari/guilds.py +++ b/hikari/guilds.py @@ -2931,7 +2931,7 @@ def get_voice_state( def get_emoji( self, emoji: snowflakes.SnowflakeishOr[emojis_.CustomEmoji] ) -> typing.Optional[emojis_.KnownCustomEmoji]: - """Get a cached role that belongs to the guild by it's ID or object. + """Get a cached emoji that belongs to the guild by it's ID or object. Parameters ---------- diff --git a/hikari/impl/bot.py b/hikari/impl/bot.py index 9651977224..50892b61f0 100644 --- a/hikari/impl/bot.py +++ b/hikari/impl/bot.py @@ -418,11 +418,112 @@ async def handle(name: str, awaitable: typing.Awaitable[typing.Any]) -> None: self._closed_event = None def dispatch(self, event: event_manager_.EventT_inv) -> asyncio.Future[typing.Any]: + """Dispatch an event. + + Parameters + ---------- + event : hikari.events.base_events.Event + The event to dispatch. + + Example + ------- + We can dispatch custom events by first defining a class that + derives from `hikari.events.base_events.Event`. + + ```py + import attr + + from hikari.traits import RESTAware + from hikari.events.base_events import Event + from hikari.users import User + from hikari.snowflakes import Snowflake + + @attr.define() + class EveryoneMentionedEvent(Event): + app: RESTAware = attr.field() + + author: User = attr.field() + '''The user who mentioned everyone.''' + + content: str = attr.field() + '''The message that was sent.''' + + message_id: Snowflake = attr.field() + '''The message ID.''' + + channel_id: Snowflake = attr.field() + '''The channel ID.''' + ``` + + We can then dispatch our event as we see fit. + + ```py + from hikari.events.messages import MessageCreateEvent + + @bot.listen(MessageCreateEvent) + async def on_message(event): + if "@everyone" in event.content or "@here" in event.content: + event = EveryoneMentionedEvent( + author=event.author, + content=event.content, + message_id=event.id, + channel_id=event.channel_id, + ) + + bot.dispatch(event) + ``` + + This event can be listened to elsewhere by subscribing to it with + `EventManager.subscribe`. + + ```py + @bot.listen(EveryoneMentionedEvent) + async def on_everyone_mentioned(event): + print(event.user, "just pinged everyone in", event.channel_id) + ``` + + Returns + ------- + asyncio.Future[typing.Any] + A future that can be optionally awaited. If awaited, the future + will complete once all corresponding event listeners have been + invoked. If not awaited, this will schedule the dispatch of the + events in the background for later. + + See Also + -------- + Listen: `hikari.impl.bot.GatewayBot.listen` + Stream: `hikari.impl.bot.GatewayBot.stream` + Subscribe: `hikari.impl.bot.GatewayBot.subscribe` + Unubscribe: `hikari.impl.bot.GatewayBot.unsubscribe` + Wait_for: `hikari.impl.bot.GatewayBot.wait_for` + """ return self._event_manager.dispatch(event) def get_listeners( self, event_type: typing.Type[event_manager_.EventT_co], /, *, polymorphic: bool = True ) -> typing.Collection[event_manager_.CallbackT[event_manager_.EventT_co]]: + """Get the listeners for a given event type, if there are any. + + Parameters + ---------- + event_type : typing.Type[T] + The event type to look for. + `T` must be a subclass of `hikari.events.base_events.Event`. + polymorphic : builtins.bool + If `builtins.True`, this will also return the listeners of the + subclasses of the given event type. If `builtins.False`, then + only listeners for this class specifically are returned. The + default is `builtins.True`. + + Returns + ------- + typing.Collection[typing.Callable[[T], typing.Coroutine[typing.Any, typing.Any, builtins.None]] + A copy of the collection of listeners for the event. Will return + an empty collection if nothing is registered. + + `T` must be a subclass of `hikari.events.base_events.Event`. + """ return self._event_manager.get_listeners(event_type, polymorphic=polymorphic) async def join(self, until_close: bool = True) -> None: @@ -440,6 +541,34 @@ def listen( [event_manager_.CallbackT[event_manager_.EventT_co]], event_manager_.CallbackT[event_manager_.EventT_co], ]: + """Generate a decorator to subscribe a callback to an event type. + + This is a second-order decorator. + + Parameters + ---------- + event_type : typing.Optional[typing.Type[T]] + The event type to subscribe to. The implementation may allow this + to be undefined. If this is the case, the event type will be inferred + instead from the type hints on the function signature. + + `T` must be a subclass of `hikari.events.base_events.Event`. + + Returns + ------- + typing.Callable[[T], T] + A decorator for a coroutine function that passes it to + `EventManager.subscribe` before returning the function + reference. + + See Also + -------- + Dispatch: `hikari.impl.bot.GatewayBot.dispatch` + Stream: `hikari.impl.bot.GatewayBot.stream` + Subscribe: `hikari.impl.bot.GatewayBot.subscribe` + Unubscribe: `hikari.impl.bot.GatewayBot.unsubscribe` + Wait_for: `hikari.impl.bot.GatewayBot.wait_for` + """ return self._event_manager.listen(event_type) @staticmethod @@ -872,13 +1001,138 @@ def stream( timeout: typing.Union[float, int, None], limit: typing.Optional[int] = None, ) -> event_manager_.EventStream[event_manager_.EventT_co]: + """Return a stream iterator for the given event and sub-events. + + Parameters + ---------- + event_type : typing.Type[hikari.events.base_events.Event] + The event type to listen for. This will listen for subclasses of + this type additionally. + timeout : typing.Optional[builtins.int, builtins.float] + How long this streamer should wait for the next event before + ending the iteration. If `builtins.None` then this will continue + until explicitly broken from. + limit : typing.Optional[builtins.int] + The limit for how many events this should queue at one time before + dropping extra incoming events, leave this as `builtins.None` for + the cache size to be unlimited. + + Returns + ------- + EventStream[hikari.events.base_events.Event] + The async iterator to handle streamed events. This must be started + with `async with stream:` or `await stream.open()` before + asynchronously iterating over it. + + !!! warning + If you use `await stream.open()` to start the stream then you must + also close it with `await stream.close()` otherwise it may queue + events in memory indefinitely. + + Examples + -------- + + ```py + async with bot.stream(events.ReactionAddEvent, timeout=30).filter(("message_id", message.id)) as stream: + async for user_id in stream.map("user_id").limit(50): + ... + ``` + + or using await `open()` and await `close()` + + ```py + stream = bot.stream(events.ReactionAddEvent, timeout=30).filter(("message_id", message.id)) + await stream.open() + + async for user_id in stream.map("user_id").limit(50) + ... + + await stream.close() + ``` + + See Also + -------- + Dispatch: `hikari.impl.bot.GatewayBot.dispatch` + Listen: `hikari.impl.bot.GatewayBot.listen` + Subscribe: `hikari.impl.bot.GatewayBot.subscribe` + Unubscribe: `hikari.impl.bot.GatewayBot.unsubscribe` + Wait_for: `hikari.impl.bot.GatewayBot.wait_for` + """ self._check_if_alive() return self._event_manager.stream(event_type, timeout=timeout, limit=limit) def subscribe(self, event_type: typing.Type[typing.Any], callback: event_manager_.CallbackT[typing.Any]) -> None: + """Subscribe a given callback to a given event type. + + Parameters + ---------- + event_type : typing.Type[T] + The event type to listen for. This will also listen for any + subclasses of the given type. + `T` must be a subclass of `hikari.events.base_events.Event`. + callback + Must be a coroutine function to invoke. This should + consume an instance of the given event, or an instance of a valid + subclass if one exists. Any result is discarded. + + Example + ------- + The following demonstrates subscribing a callback to message creation + events. + + ```py + from hikari.events.messages import MessageCreateEvent + + async def on_message(event): + ... + + bot.subscribe(MessageCreateEvent, on_message) + ``` + + See Also + -------- + Dispatch: `hikari.impl.bot.GatewayBot.dispatch` + Listen: `hikari.impl.bot.GatewayBot.listen` + Stream: `hikari.impl.bot.GatewayBot.stream` + Unubscribe: `hikari.impl.bot.GatewayBot.unsubscribe` + Wait_for: `hikari.impl.bot.GatewayBot.wait_for` + """ self._event_manager.subscribe(event_type, callback) def unsubscribe(self, event_type: typing.Type[typing.Any], callback: event_manager_.CallbackT[typing.Any]) -> None: + """Unsubscribe a given callback from a given event type, if present. + + Parameters + ---------- + event_type : typing.Type[T] + The event type to unsubscribe from. This must be the same exact + type as was originally subscribed with to be removed correctly. + `T` must derive from `hikari.events.base_events.Event`. + callback + The callback to unsubscribe. + + Example + ------- + The following demonstrates unsubscribing a callback from a message + creation event. + + ```py + from hikari.events.messages import MessageCreateEvent + + async def on_message(event): + ... + + bot.unsubscribe(MessageCreateEvent, on_message) + ``` + + See Also + -------- + Dispatch: `hikari.impl.bot.GatewayBot.dispatch` + Listen: `hikari.impl.bot.GatewayBot.listen` + Stream: `hikari.impl.bot.GatewayBot.stream` + Subscribe: `hikari.impl.bot.GatewayBot.subscribe` + Wait_for: `hikari.impl.bot.GatewayBot.wait_for` + """ self._event_manager.unsubscribe(event_type, callback) async def wait_for( @@ -888,6 +1142,48 @@ async def wait_for( timeout: typing.Union[float, int, None], predicate: typing.Optional[event_manager_.PredicateT[event_manager_.EventT_co]] = None, ) -> event_manager_.EventT_co: + """Wait for a given event to occur once, then return the event. + + Parameters + ---------- + event_type : typing.Type[hikari.events.base_events.Event] + The event type to listen for. This will listen for subclasses of + this type additionally. + predicate + A function taking the event as the single parameter. + This should return `builtins.True` if the event is one you want to + return, or `builtins.False` if the event should not be returned. + If left as `None` (the default), then the first matching event type + that the bot receives (or any subtype) will be the one returned. + + !!! warning + Async predicates are not supported. + timeout : typing.Union[builtins.float, builtins.int, builtins.None] + The amount of time to wait before raising an `asyncio.TimeoutError` + and giving up instead. This is measured in seconds. If + `builtins.None`, then no timeout will be waited for (no timeout can + result in "leaking" of coroutines that never complete if called in + an uncontrolled way, so is not recommended). + + Returns + ------- + hikari.events.base_events.Event + The event that was provided. + + Raises + ------ + asyncio.TimeoutError + If the timeout is not `builtins.None` and is reached before an + event is received that the predicate returns `builtins.True` for. + + See Also + -------- + Dispatch: `hikari.impl.bot.GatewayBot.dispatch` + Listen: `hikari.impl.bot.GatewayBot.listen` + Stream: `hikari.impl.bot.GatewayBot.stream` + Subscribe: `hikari.impl.bot.GatewayBot.subscribe` + Unubscribe: `hikari.impl.bot.GatewayBot.unsubscribe` + """ self._check_if_alive() return await self._event_manager.wait_for(event_type, timeout=timeout, predicate=predicate)