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.
events.register, unregister, is_handler and list are gone. now you can typehint instead.
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._updates_queue = asyncio.Queue(maxsize=max_queued_updates)
self._updates_handle = None
self._update_handlers = [] # sorted list
self._message_box = MessageBox()
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)
def on(self: 'TelegramClient', event: EventBuilder):
def on(self: 'TelegramClient', *events, priority=0, **filters):
"""
Decorator used to `add_event_handler` more conveniently.
This decorator should be above other decorators which modify the function.
Arguments
event (`_EventBuilder` | `type`):
The event builder class or instance to be used,
for instance ``events.NewMessage``.
event (`type` | `tuple`):
The event type(s) you wish to receive, 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
.. code-block:: python
@ -2808,7 +2817,12 @@ class TelegramClient:
client = TelegramClient(...)
# 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):
...
"""
@ -2816,8 +2830,11 @@ class TelegramClient:
@forward_call(updates.add_event_handler)
def add_event_handler(
self: 'TelegramClient',
callback: updates.Callback,
event: EventBuilder = None):
callback: updates.Callback = None,
event: EventBuilder = None,
priority=0,
**filters
):
"""
Registers a new event handler callback.
@ -2827,17 +2844,29 @@ class TelegramClient:
callback (`callable`):
The callable function accepting one parameter to be used.
Note that if you have used `telethon.events.register` in
the callback, ``event`` will be ignored, and instead the
events you previously registered will be used.
If `None`, the method can be used as a decorator. Note that the handler function
will be replaced with the `EventHandler` instance in this case, but it will still
be callable.
event (`_EventBuilder` | `type`, optional):
The event builder class or instance to be used,
for instance ``events.NewMessage``.
If left unspecified, `telethon.events.raw.Raw` (the
:tl:`Update` objects with no further processing) will
be passed instead.
If left unspecified, it will be inferred from the type hint
used in the handler, or be `telethon.events.raw.Raw` (the
: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
.. code-block:: python
@ -2845,22 +2874,47 @@ class TelegramClient:
from telethon import TelegramClient, events
client = TelegramClient(...)
# Adding a handler, the "boring" way
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)
def remove_event_handler(
self: 'TelegramClient',
callback: updates.Callback,
event: EventBuilder = None) -> int:
callback: updates.Callback = None,
event: EventBuilder = None,
priority=None,
) -> int:
"""
Inverse operation of `add_event_handler()`.
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
.. code-block:: python
@ -2876,6 +2930,12 @@ class TelegramClient:
# "handler" will stop receiving anything
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)
@ -2885,7 +2945,7 @@ class TelegramClient:
Lists all registered event handlers.
Returns
A list of pairs consisting of ``(callback, event)``.
A list of all registered `EventHandler` in arbitrary order.
Example
.. code-block:: python
@ -2895,8 +2955,8 @@ class TelegramClient:
'''Greets someone'''
await event.reply('Hi')
for callback, event in client.list_event_handlers():
print(id(callback), type(event))
for handler in client.list_event_handlers():
print(id(handler.callback), handler.event)
"""
@forward_call(updates.catch_up)

View File

@ -7,12 +7,15 @@ import time
import traceback
import typing
import logging
import inspect
import bisect
import warnings
from collections import deque
from ..errors._rpcbase import RpcError
from .._events.base import EventBuilder
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 .. import _tl
@ -33,51 +36,96 @@ async def run_until_disconnected(self: 'TelegramClient'):
await self(_tl.fn.updates.GetState())
await self._sender.wait_disconnected()
def on(self: 'TelegramClient', event: EventBuilder):
def on(self: 'TelegramClient', *events, priority=0, **filters):
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 decorator
def add_event_handler(
self: 'TelegramClient',
callback: Callback,
event: EventBuilder = None):
builders = _get_handlers(callback)
if builders is not None:
for event in builders:
self._event_builders.append((event, callback))
return
callback=None,
event=None,
priority=0,
**filters
):
if callback is None:
return functools.partial(add_event_handler, self, event=event, priority=priority, **filters)
if isinstance(event, type):
event = event()
elif not event:
event = Raw()
if event is None:
for param in inspect.signature(callback).parameters.values():
if not issubclass(param.annotation, EventBuilder):
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(
self: 'TelegramClient',
callback: Callback,
event: EventBuilder = None) -> int:
found = 0
if event and not isinstance(event, type):
event = type(event)
callback,
event,
priority,
):
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)
while i:
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
if not self._update_handlers:
return [] # won't be removing anything (some code paths rely on non-empty lists)
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')\
-> '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'):
# The update loop is probably blocked on either timeout or an update to arrive.

View File

@ -1,4 +1,5 @@
import abc
import functools
class StopPropagation(Exception):
@ -41,3 +42,23 @@ class EventBuilder(abc.ABC):
`self_id` should be the current user's ID, since it is required
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
class NewMessageEvent(EventBuilder, Message):
class NewMessage(EventBuilder, _custom.Message):
"""
Represents the event of a new message. This event can be treated
to all effects as a `Message <telethon.tl.custom.message.Message>`,

View File

@ -32,7 +32,7 @@ def _requires_status(function):
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.

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.album import Album
from ._events.chataction import ChatAction
from ._events.messagedeleted import MessageDeleted