From d5d3733fd4d9ade0ce73f006bf49e9712f16029a Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sat, 22 Sep 2018 12:51:58 +0200 Subject: [PATCH] Create events.register and siblings for "handler templates" This can be thought of as a different approach to Flask's blueprints. --- .../advanced-usage/mastering-telethon.rst | 2 +- .../extra/basic/working-with-updates.rst | 83 ++++++++++++-- readthedocs/extra/wall-of-shame.rst | 2 + telethon/client/updates.py | 10 ++ telethon/events/__init__.py | 104 ++++++++++++++++++ 5 files changed, 191 insertions(+), 10 deletions(-) diff --git a/readthedocs/extra/advanced-usage/mastering-telethon.rst b/readthedocs/extra/advanced-usage/mastering-telethon.rst index a0270250..8d17ee87 100644 --- a/readthedocs/extra/advanced-usage/mastering-telethon.rst +++ b/readthedocs/extra/advanced-usage/mastering-telethon.rst @@ -216,7 +216,7 @@ Will show a much better: Now it's easy to see how we could get, for example, the ``was_online`` time. It's inside ``status``: -.. code-block:: +.. code-block:: python online_at = user.status.was_online diff --git a/readthedocs/extra/basic/working-with-updates.rst b/readthedocs/extra/basic/working-with-updates.rst index 6aa9341e..aaca6b59 100644 --- a/readthedocs/extra/basic/working-with-updates.rst +++ b/readthedocs/extra/basic/working-with-updates.rst @@ -181,7 +181,7 @@ random number, while if you say ``'eval 4+4'``, you will reply with the solution. Try it! -Properties vs. methods +Properties vs. Methods ********************** The event shown above acts just like a `custom.Message @@ -220,26 +220,91 @@ methods (`message.get_sender and you should use methods in events for these properties that may need network. -Events without decorators +Events Without the client ************************* -If for any reason you can't use the `@client.on -` syntax, don't worry. -You can call `client.add_event_handler(callback, event) -` to achieve -the same effect. +The code of your application starts getting big, so you decide to +separate the handlers into different files. But how can you access +the client from these files? You don't need to! Just `events.register +` them: + +.. code-block:: python + + # handlers/welcome.py + from telethon import events + + @events.register(events.NewMessage('(?i)hello')) + async def handler(event): + client = event.client + await event.respond('Hey!') + await client.send_message('me', 'I said hello to someone') + + +Registering events is a way of saying "this method is an event handler". +You can use `telethon.events.is_handler` to check if any method is a handler. +You can think of them as a different approach to Flask's blueprints. + +It's important to note that this does **not** add the handler to any client! +You never specified the client on which the handler should be used. You only +declared that it is a handler, and its type. + +To actually use the handler, you need to `client.add_event_handler +` to the +client (or clients) where they should be added to: + +.. code-block:: python + + # main.py + from telethon import TelegramClient + import handlers.welcome + + with TelegramClient(...) as client: + client.add_event_handler(handlers.welcome.handler) + client.run_until_disconnected() + + +This also means that you can register an event handler once and +then add it to many clients without re-declaring the event. + + +Events Without Decorators +************************* + +If for any reason you don't want to use `telethon.events.register`, +you can explicitly pass the event handler to use to the mentioned +`client.add_event_handler +`: + +.. code-block:: python + + from telethon import TelegramClient, events + + async def handler(event): + ... + + with TelegramClient(...) as client: + client.add_event_handler(handler, events.NewMessage) + client.run_until_disconnected() + Similarly, you also have `client.remove_event_handler ` and `client.list_event_handlers `. -The ``event`` type is optional in all methods and defaults to +The ``event`` argument is optional in all three methods and defaults to `events.Raw ` for adding, and ``None`` when removing (so all callbacks would be removed). +.. note:: -Stopping propagation of Updates + The ``event`` type is ignored in `client.add_event_handler + ` + if you have used `telethon.events.register` on the ``callback`` + before, since that's the point of using such method at all. + + +Stopping Propagation of Updates ******************************* There might be cases when an event handler is supposed to be used solitary and diff --git a/readthedocs/extra/wall-of-shame.rst b/readthedocs/extra/wall-of-shame.rst index 83e96956..a27a9f53 100644 --- a/readthedocs/extra/wall-of-shame.rst +++ b/readthedocs/extra/wall-of-shame.rst @@ -51,6 +51,7 @@ The current winner is `issue **Issue:** .. figure:: https://user-images.githubusercontent.com/6297805/29822978-9a9a6ef0-8ccd-11e7-9ec5-934ea0f57681.jpg + :alt: Winner issue Winner issue @@ -58,6 +59,7 @@ Winner issue **Answer:** .. figure:: https://user-images.githubusercontent.com/6297805/29822983-9d523402-8ccd-11e7-9fb1-5783740ee366.jpg + :alt: Winner issue answer Winner issue answer diff --git a/telethon/client/updates.py b/telethon/client/updates.py index c808439d..1e40a715 100644 --- a/telethon/client/updates.py +++ b/telethon/client/updates.py @@ -76,6 +76,10 @@ class UpdateMethods(UserMethods): 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. + event (`_EventBuilder` | `type`, optional): The event builder class or instance to be used, for instance ``events.NewMessage``. @@ -84,6 +88,12 @@ class UpdateMethods(UserMethods): :tl:`Update` objects with no further processing) will be passed instead. """ + builders = events._get_handlers(callback) + if builders is not None: + for event in builders: + self._event_builders.append((event, callback)) + return + if isinstance(event, type): event = event() elif not event: diff --git a/telethon/events/__init__.py b/telethon/events/__init__.py index 0ff7c606..25b2d4e9 100644 --- a/telethon/events/__init__.py +++ b/telethon/events/__init__.py @@ -9,6 +9,9 @@ from .callbackquery import CallbackQuery from .inlinequery import InlineQuery +_HANDLERS_ATTRIBUTE = '__tl.handlers' + + class StopPropagation(Exception): """ If this exception is raised in any of the handlers for a given event, @@ -16,6 +19,7 @@ class StopPropagation(Exception): It can be seen as the ``StopIteration`` in a for loop but for events. Example usage: + >>> from telethon import TelegramClient, events >>> client = TelegramClient(...) >>> @@ -33,3 +37,103 @@ class StopPropagation(Exception): # For some reason Sphinx wants the silly >>> or # it will show warnings and look bad when generated. pass + + +def register(event=None): + """ + Decorator method to *register* event handlers. This is the client-less + `add_event_handler + ` variant. + + Note that this method only registers callbacks as handlers, + and does not attach them to any client. This is useful for + external modules that don't have access to the client, but + still want to define themselves as a handler. Example: + + >>> from telethon import events + >>> @events.register(events.NewMessage) + ... async def handler(event): + ... ... + ... + >>> # (somewhere else) + ... + >>> from telethon import TelegramClient + >>> client = TelegramClient(...) + >>> client.add_event_handler(handler) + + Remember that you can use this as a non-decorator + through ``register(event)(callback)``. + + Args: + event (`_EventBuilder` | `type`): + The event builder class or instance to be used, + for instance ``events.NewMessage``. + """ + if isinstance(event, type): + event = event() + elif not event: + event = Raw() + + def decorator(callback): + handlers = getattr(callback, _HANDLERS_ATTRIBUTE, []) + handlers.append(event) + setattr(callback, _HANDLERS_ATTRIBUTE, handlers) + return callback + + return decorator + + +def unregister(callback, event=None): + """ + Inverse operation of `register` (though not a decorator). Client-less + `remove_event_handler + ` + variant. **Note that this won't remove handlers from the client**, + because it simply can't, so you would generally use this before + adding the handlers to the client. + + This method is here for symmetry. You will rarely need to + unregister events, since you can simply just not add them + to any client. + + If no event is given, all events for this callback are removed. + Returns how many callbacks were removed. + """ + found = 0 + if event and not isinstance(event, type): + event = type(event) + + handlers = getattr(callback, _HANDLERS_ATTRIBUTE, []) + handlers.append((event, callback)) + i = len(handlers) + while i: + i -= 1 + ev = handlers[i] + if not event or isinstance(ev, event): + del handlers[i] + found += 1 + + return found + + +def is_handler(callback): + """ + Returns ``True`` if the given callback is an + event handler (i.e. you used `register` on it). + """ + return hasattr(callback, _HANDLERS_ATTRIBUTE) + + +def list(callback): + """ + Returns a list containing the registered event + builders inside the specified callback handler. + """ + return getattr(callback, _HANDLERS_ATTRIBUTE, [])[:] + + +def _get_handlers(callback): + """ + Like ``list`` but returns ``None`` if the callback was never registered. + """ + return getattr(callback, _HANDLERS_ATTRIBUTE, None)