Rework methods to manage event handlers

This commit is contained in:
Lonami Exo 2022-01-28 14:12:32 +01:00
parent 9726169a8c
commit 0802f7e6b2
8 changed files with 184 additions and 54 deletions

View File

@ -788,3 +788,4 @@ it's now
this also means filters are unified, although not all have an effect on all events. from_users renamed to senders. messageread inbox is gone in favor of outgoing/incoming. this also means filters are unified, although not all have an effect on all events. from_users renamed to senders. messageread inbox is gone in favor of outgoing/incoming.
events.register, unregister, is_handler and list are gone. now you can typehint instead. events.register, unregister, is_handler and list are gone. now you can typehint instead.
def handler(event: events.NewMessage) def handler(event: events.NewMessage)
client.on, add, and remove have changed parameters/retval

View File

@ -148,6 +148,7 @@ def init(
self._no_updates = not receive_updates self._no_updates = not receive_updates
self._updates_queue = asyncio.Queue(maxsize=max_queued_updates) self._updates_queue = asyncio.Queue(maxsize=max_queued_updates)
self._updates_handle = None self._updates_handle = None
self._update_handlers = [] # sorted list
self._message_box = MessageBox() self._message_box = MessageBox()
self._entity_cache = EntityCache() # required for proper update handling (to know when to getDifference) self._entity_cache = EntityCache() # required for proper update handling (to know when to getDifference)

View File

@ -2791,15 +2791,24 @@ class TelegramClient:
""" """
@forward_call(updates.on) @forward_call(updates.on)
def on(self: 'TelegramClient', event: EventBuilder): def on(self: 'TelegramClient', *events, priority=0, **filters):
""" """
Decorator used to `add_event_handler` more conveniently. Decorator used to `add_event_handler` more conveniently.
This decorator should be above other decorators which modify the function.
Arguments Arguments
event (`_EventBuilder` | `type`): event (`type` | `tuple`):
The event builder class or instance to be used, The event type(s) you wish to receive, for instance ``events.NewMessage``.
for instance ``events.NewMessage``. This may also be raw update types.
The same handler is registered multiple times, one per type.
priority (`int`):
The event priority. Events with higher priority are dispatched first.
The order between events with the same priority is arbitrary.
filters (any):
Filters passed to `make_filter`.
Example Example
.. code-block:: python .. code-block:: python
@ -2808,7 +2817,12 @@ class TelegramClient:
client = TelegramClient(...) client = TelegramClient(...)
# Here we use client.on # Here we use client.on
@client.on(events.NewMessage) @client.on(events.NewMessage, priority=100)
async def handler(event):
...
# Both new incoming messages and incoming edits
@client.on(events.NewMessage, events.MessageEdited, incoming=True)
async def handler(event): async def handler(event):
... ...
""" """
@ -2816,8 +2830,11 @@ class TelegramClient:
@forward_call(updates.add_event_handler) @forward_call(updates.add_event_handler)
def add_event_handler( def add_event_handler(
self: 'TelegramClient', self: 'TelegramClient',
callback: updates.Callback, callback: updates.Callback = None,
event: EventBuilder = None): event: EventBuilder = None,
priority=0,
**filters
):
""" """
Registers a new event handler callback. Registers a new event handler callback.
@ -2827,17 +2844,29 @@ class TelegramClient:
callback (`callable`): callback (`callable`):
The callable function accepting one parameter to be used. The callable function accepting one parameter to be used.
Note that if you have used `telethon.events.register` in If `None`, the method can be used as a decorator. Note that the handler function
the callback, ``event`` will be ignored, and instead the will be replaced with the `EventHandler` instance in this case, but it will still
events you previously registered will be used. be callable.
event (`_EventBuilder` | `type`, optional): event (`_EventBuilder` | `type`, optional):
The event builder class or instance to be used, The event builder class or instance to be used,
for instance ``events.NewMessage``. for instance ``events.NewMessage``.
If left unspecified, `telethon.events.raw.Raw` (the If left unspecified, it will be inferred from the type hint
:tl:`Update` objects with no further processing) will used in the handler, or be `telethon.events.raw.Raw` (the
be passed instead. :tl:`Update` objects with no further processing) if there is
none. Note that the type hint must be the desired type. It
cannot be a string, an union, or anything more complex.
priority (`int`):
The event priority. Events with higher priority are dispatched first.
The order between events with the same priority is arbitrary.
filters (any):
Filters passed to `make_filter`.
Returns
An `EventHandler` instance, which can be used
Example Example
.. code-block:: python .. code-block:: python
@ -2845,22 +2874,47 @@ class TelegramClient:
from telethon import TelegramClient, events from telethon import TelegramClient, events
client = TelegramClient(...) client = TelegramClient(...)
# Adding a handler, the "boring" way
async def handler(event): async def handler(event):
... ...
client.add_event_handler(handler, events.NewMessage) client.add_event_handler(handler, events.NewMessage, priority=50)
# Automatic type
async def handler(event: events.MessageEdited)
...
client.add_event_handler(handler, outgoing=False)
# Streamlined adding
@client.add_event_handler
async def handler(event: events.MessageDeleted):
...
""" """
@forward_call(updates.remove_event_handler) @forward_call(updates.remove_event_handler)
def remove_event_handler( def remove_event_handler(
self: 'TelegramClient', self: 'TelegramClient',
callback: updates.Callback, callback: updates.Callback = None,
event: EventBuilder = None) -> int: event: EventBuilder = None,
priority=None,
) -> int:
""" """
Inverse operation of `add_event_handler()`. Inverse operation of `add_event_handler()`.
If no event is given, all events for this callback are removed. If no event is given, all events for this callback are removed.
Returns how many callbacks were removed. Returns a list in arbitrary order with all removed `EventHandler` instances.
Arguments
callback (`callable`):
The callable function accepting one parameter to be used.
If passed an `EventHandler` instance, both `event` and `priority` are ignored.
event (`_EventBuilder` | `type`, optional):
The event builder class or instance to be used when searching.
priority (`int`):
The event priority to be used when searching.
Example Example
.. code-block:: python .. code-block:: python
@ -2876,6 +2930,12 @@ class TelegramClient:
# "handler" will stop receiving anything # "handler" will stop receiving anything
client.remove_event_handler(handler) client.remove_event_handler(handler)
# Remove all handlers with priority 50
client.remove_event_handler(priority=50)
# Remove all deleted-message handlers
client.remove_event_handler(event=events.MessageDeleted)
""" """
@forward_call(updates.list_event_handlers) @forward_call(updates.list_event_handlers)
@ -2885,7 +2945,7 @@ class TelegramClient:
Lists all registered event handlers. Lists all registered event handlers.
Returns Returns
A list of pairs consisting of ``(callback, event)``. A list of all registered `EventHandler` in arbitrary order.
Example Example
.. code-block:: python .. code-block:: python
@ -2895,8 +2955,8 @@ class TelegramClient:
'''Greets someone''' '''Greets someone'''
await event.reply('Hi') await event.reply('Hi')
for callback, event in client.list_event_handlers(): for handler in client.list_event_handlers():
print(id(callback), type(event)) print(id(handler.callback), handler.event)
""" """
@forward_call(updates.catch_up) @forward_call(updates.catch_up)

View File

@ -7,12 +7,15 @@ import time
import traceback import traceback
import typing import typing
import logging import logging
import inspect
import bisect
import warnings
from collections import deque from collections import deque
from ..errors._rpcbase import RpcError from ..errors._rpcbase import RpcError
from .._events.base import EventBuilder
from .._events.raw import Raw from .._events.raw import Raw
from .._events.base import StopPropagation, _get_handlers from .._events.base import StopPropagation, EventBuilder, EventHandler
from .._events.filters import make_filter
from .._misc import utils from .._misc import utils
from .. import _tl from .. import _tl
@ -33,51 +36,96 @@ async def run_until_disconnected(self: 'TelegramClient'):
await self(_tl.fn.updates.GetState()) await self(_tl.fn.updates.GetState())
await self._sender.wait_disconnected() await self._sender.wait_disconnected()
def on(self: 'TelegramClient', event: EventBuilder): def on(self: 'TelegramClient', *events, priority=0, **filters):
def decorator(f): def decorator(f):
self.add_event_handler(f, event) for event in events:
self.add_event_handler(f, event, priority=priority, **filters)
return f return f
return decorator return decorator
def add_event_handler( def add_event_handler(
self: 'TelegramClient', self: 'TelegramClient',
callback: Callback, callback=None,
event: EventBuilder = None): event=None,
builders = _get_handlers(callback) priority=0,
if builders is not None: **filters
for event in builders: ):
self._event_builders.append((event, callback)) if callback is None:
return return functools.partial(add_event_handler, self, event=event, priority=priority, **filters)
if isinstance(event, type): if event is None:
event = event() for param in inspect.signature(callback).parameters.values():
elif not event: if not issubclass(param.annotation, EventBuilder):
event = Raw() raise TypeError(f'unrecognized event handler type: {param.annotation!r}')
event = param.annotation
break # only check the first parameter
self._event_builders.append((event, callback)) if event is None:
event = Raw
handler = EventHandler(event, callback, priority, make_filter(**filters))
bisect.insort(self._update_handlers, handler)
return handler
def remove_event_handler( def remove_event_handler(
self: 'TelegramClient', self: 'TelegramClient',
callback: Callback, callback,
event: EventBuilder = None) -> int: event,
found = 0 priority,
if event and not isinstance(event, type): ):
event = type(event) if callback is None and event is None and priority is None:
raise ValueError('must specify at least one of callback, event or priority')
i = len(self._event_builders) if not self._update_handlers:
while i: return [] # won't be removing anything (some code paths rely on non-empty lists)
i -= 1
ev, cb = self._event_builders[i]
if cb == callback and (not event or isinstance(ev, event)):
del self._event_builders[i]
found += 1
return found if isinstance(callback, EventHandler):
if event is not None or priority is not None:
warnings.warn('event and priority are ignored when removing EventHandler instances')
index = bisect.bisect_left(self._update_handlers, callback)
try:
if self._update_handlers[index] == callback:
return [self._update_handlers.pop(index)]
except IndexError:
pass
return []
if priority is not None:
# can binary-search (using a dummy EventHandler)
index = bisect.bisect_right(self._update_handlers, EventHandler(None, None, priority, None))
try:
while self._update_handlers[index].priority == priority:
index += 1
except IndexError:
pass
removed = []
while index > 0 and self._update_handlers[index - 1].priority == priority:
index -= 1
if callback is not None and self._update_handlers[index].callback != callback:
continue
if event is not None and self._update_handlers[index].event != event:
continue
removed.append(self._update_handlers.pop(index))
return removed
# slow-path, remove all matching
removed = []
for index, handler in reversed(enumerate(self._update_handlers)):
if callback is not None and handler.callback != callback:
continue
if event is not None and handler.event != event:
continue
removed.append(self._update_handlers.pop(index))
return removed
def list_event_handlers(self: 'TelegramClient')\ def list_event_handlers(self: 'TelegramClient')\
-> 'typing.Sequence[typing.Tuple[Callback, EventBuilder]]': -> 'typing.Sequence[typing.Tuple[Callback, EventBuilder]]':
return [(callback, event) for event, callback in self._event_builders] return self._update_handlers[:]
async def catch_up(self: 'TelegramClient'): async def catch_up(self: 'TelegramClient'):
# The update loop is probably blocked on either timeout or an update to arrive. # The update loop is probably blocked on either timeout or an update to arrive.

View File

@ -1,4 +1,5 @@
import abc import abc
import functools
class StopPropagation(Exception): class StopPropagation(Exception):
@ -41,3 +42,23 @@ class EventBuilder(abc.ABC):
`self_id` should be the current user's ID, since it is required `self_id` should be the current user's ID, since it is required
for some events which lack this information but still need it. for some events which lack this information but still need it.
""" """
@functools.total_ordering
class EventHandler:
__slots__ = ('_event', '_callback', '_priority', '_filter')
def __init__(self, event, callback, priority, filter):
self._event = event
self._callback = callback
self._priority = priority
self._filter = filter
def __eq__(self, other):
return self is other
def __lt__(self, other):
return self._priority < other._priority
def __call__(self, *args, **kwargs):
return self._callback(*args, **kwargs)

View File

@ -6,7 +6,7 @@ from .. import _tl
from ..types import _custom from ..types import _custom
class NewMessageEvent(EventBuilder, Message): class NewMessage(EventBuilder, _custom.Message):
""" """
Represents the event of a new message. This event can be treated Represents the event of a new message. This event can be treated
to all effects as a `Message <telethon.tl.custom.message.Message>`, to all effects as a `Message <telethon.tl.custom.message.Message>`,

View File

@ -32,7 +32,7 @@ def _requires_status(function):
return wrapped return wrapped
class UserUpdateEvent(EventBuilder, _custom.chatgetter.ChatGetter, _custom.sendergetter.SenderGetter): class UserUpdate(EventBuilder, _custom.chatgetter.ChatGetter, _custom.sendergetter.SenderGetter):
""" """
Occurs whenever a user goes online, starts typing, etc. Occurs whenever a user goes online, starts typing, etc.

View File

@ -1,6 +1,5 @@
from ._events.base import StopPropagation, register, unregister, is_handler, list from ._events.base import StopPropagation
from ._events.raw import Raw from ._events.raw import Raw
from ._events.album import Album from ._events.album import Album
from ._events.chataction import ChatAction from ._events.chataction import ChatAction
from ._events.messagedeleted import MessageDeleted from ._events.messagedeleted import MessageDeleted