From 8e6b98669aedc7ce2ab93e2919b7546a89ec6354 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sat, 3 Nov 2018 12:44:49 +0100 Subject: [PATCH] Get rid of full_sync --- .../basic/compatibility-and-convenience.rst | 68 ++++--- telethon/__init__.py | 2 +- telethon/full_sync.py | 184 ------------------ 3 files changed, 43 insertions(+), 211 deletions(-) delete mode 100644 telethon/full_sync.py diff --git a/readthedocs/extra/basic/compatibility-and-convenience.rst b/readthedocs/extra/basic/compatibility-and-convenience.rst index 2111d39a..86be8c1c 100644 --- a/readthedocs/extra/basic/compatibility-and-convenience.rst +++ b/readthedocs/extra/basic/compatibility-and-convenience.rst @@ -14,31 +14,48 @@ is there to tell you when these important changes happen. Compatibility ************* -.. important:: +Some decisions when developing will inevitable be proven wrong in the future. +One of these decisions was using threads. Now that Python 3.4 is reaching EOL +and using ``asyncio`` is usable as of Python 3.5 it makes sense for a library +like Telethon to make a good use of it. - **You should not enable the thread-compatibility mode for new projects.** - It comes with a cost, and new projects will greatly benefit from using - ``asyncio`` by default such as increased speed and easier reasoning about - the code flow. You should only enable it for old projects you don't have - the time to upgrade to ``asyncio``. +If you have old code, **just use old versions** of the library! There is +nothing wrong with that other than not getting new updates or fixes, but +using a fixed version with ``pip install telethon==0.19.1.6`` is easy +enough to do. -There exists a fair amount of code online using Telethon before it reached -its 1.0 version, where it became fully asynchronous by default. Since it was -necessary to clean some things, compatibility was not kept 100% but the -changes are simple: +You might want to consider using `Virtual Environments +`_ in your projects. + +There's no point in maintaining a synchronous version because the whole point +is that people don't have time to upgrade, and there has been several changes +and clean-ups. Using an older version is the right way to go. + +Sometimes, other small decisions are made. These all will be reflected in the +:ref:`changelog` which you should read when upgrading. + +If you want to jump the ``asyncio`` boat, here are some of the things you will +need to start migrating really old code: .. code-block:: python - # 1. The library no longer uses threads. - # Add this at the **beginning** of your script to work around that. - from telethon import full_sync - full_sync.enable() + # 1. Import the client from telethon.sync + from telethon.sync import TelegramClient + + # 2. Change this monster... + try: + assert client.connect() + if not client.is_user_authorized(): + client.send_code_request(phone_number) + me = client.sign_in(phone_number, input('Enter code: ')) + + ... # REST OF YOUR CODE + finally: + client.disconnect() - # 2. client.connect() no longer returns True. - # Change this... - assert client.connect() # ...for this: - client.connect() + with client: + ... # REST OF YOUR CODE # 3. client.idle() no longer exists. # Change this... @@ -52,11 +69,10 @@ changes are simple: # ...to this: client.add_event_handler(handler) - # 5. It's good practice to stop the full_sync mode once you're done - try: - ... # all your code in here - finally: - full_sync.stop() + +In addition, all the update handlers must be ``async def``, and you need +to ``await`` method calls that rely on network requests, such as getting +the chat or sender. If you don't use updates, you're done! Convenience @@ -75,8 +91,8 @@ Convenience This makes the examples shorter and easier to think about. For quick scripts that don't need updates, it's a lot more convenient to -forget about ``full_sync`` or ``asyncio`` and just work with sequential code. -This can prove to be a powerful hybrid for running under the Python REPL too. +forget about ``asyncio`` and just work with sequential code. This can prove +to be a powerful hybrid for running under the Python REPL too. .. code-block:: python @@ -122,7 +138,7 @@ Speed When you're ready to micro-optimize your application, or if you simply don't need to call any non-basic methods from a synchronous context, -just get rid of both ``telethon.sync`` and ``telethon.full_sync``: +just get rid of ``telethon.sync`` and work inside an ``async def``: .. code-block:: python diff --git a/telethon/__init__.py b/telethon/__init__.py index fb945a13..f0d2b0b8 100644 --- a/telethon/__init__.py +++ b/telethon/__init__.py @@ -2,7 +2,7 @@ import logging from .client.telegramclient import TelegramClient from .network import connection from .tl import types, functions, custom -from . import version, events, utils, errors, full_sync +from . import version, events, utils, errors __version__ = version.__version__ diff --git a/telethon/full_sync.py b/telethon/full_sync.py deleted file mode 100644 index 5f72d1df..00000000 --- a/telethon/full_sync.py +++ /dev/null @@ -1,184 +0,0 @@ -""" -This magical module will rewrite all public methods in the public interface of -the library so they can delegate the call to an asyncio event loop in another -thread and wait for the result. This rewrite may not be desirable if the end -user always uses the methods they way they should be ran, but it's incredibly -useful for quick scripts and legacy code. -""" -import asyncio -import functools -import inspect -import threading -from concurrent.futures import Future, ThreadPoolExecutor - -from async_generator import isasyncgenfunction - -from . import events -from .client.telegramclient import TelegramClient -from .tl.custom import ( - Draft, Dialog, MessageButton, Forward, Message, InlineResult, Conversation -) -from .tl.custom.chatgetter import ChatGetter -from .tl.custom.sendergetter import SenderGetter - - -async def _proxy_future(af, cf): - try: - res = await af - cf.set_result(res) - except Exception as e: - cf.set_exception(e) - - -def _sync_result(loop, x): - f = Future() - loop.call_soon_threadsafe(asyncio.ensure_future, _proxy_future(x, f)) - return f.result() - - -class _SyncGen: - def __init__(self, loop, gen): - self.loop = loop - self.gen = gen - - def __iter__(self): - return self - - def __next__(self): - try: - return _sync_result(self.loop, self.gen.__anext__()) - except StopAsyncIteration: - raise StopIteration from None - - -def _syncify_wrap(t, method_name, loop, thread_ident, - syncifier=_sync_result, rename=None): - method = getattr(t, method_name) - - @functools.wraps(method) - def syncified(*args, **kwargs): - coro = method(*args, **kwargs) - return ( - coro if threading.get_ident() == thread_ident - else syncifier(loop, coro) - ) - - setattr(t, rename or method_name, syncified) - - -def _syncify(*types, loop, thread_ident): - for t in types: - # __enter__ and __exit__ need special care (VERY dirty hack). - # - # Normally we want them to raise if the loop is running because - # the user can't await there, and they need the async with variant. - # - # However they check if the loop is running to raise, which it is - # with full_sync enabled, so we patch them with the async variant. - if hasattr(t, '__aenter__'): - _syncify_wrap( - t, '__aenter__', loop, thread_ident, rename='__enter__') - - _syncify_wrap( - t, '__aexit__', loop, thread_ident, rename='__exit__') - - for name in dir(t): - if not name.startswith('_') or name == '__call__': - meth = getattr(t, name) - meth = getattr(meth, '__tl.sync', meth) - if inspect.iscoroutinefunction(meth): - _syncify_wrap(t, name, loop, thread_ident) - elif isasyncgenfunction(meth): - _syncify_wrap(t, name, loop, thread_ident, _SyncGen) - - -__asyncthread = None - - -def enable(*, loop=None, executor=None, max_workers=1): - """ - Enables the fully synchronous mode. You should enable this at - the beginning of your script, right after importing, only once. - - **Please** make sure to call `stop` at the end of your script. - - You can define the event loop to use and executor, otherwise - the default loop and ``ThreadPoolExecutor`` will be used, in - which case `max_workers` will be passed to it. If you pass a - custom executor, `max_workers` will be ignored. - """ - global __asyncthread - if __asyncthread is not None: - raise RuntimeError("full_sync can only be enabled once") - - if not loop: - loop = asyncio.get_event_loop() - if not executor: - executor = ThreadPoolExecutor(max_workers=max_workers) - - def start(): - asyncio.set_event_loop(loop) - loop.run_forever() - - __asyncthread = threading.Thread( - target=start, name="__telethon_async_thread__", daemon=True - ) - __asyncthread.start() - __asyncthread.loop = loop - __asyncthread.executor = executor - - TelegramClient.__init__ = functools.partialmethod( - TelegramClient.__init__, loop=loop - ) - - event_cls = filter(None, ( - getattr(getattr(events, name), 'Event', None) - for name in dir(events) - )) - _syncify(TelegramClient, Draft, Dialog, MessageButton, ChatGetter, - SenderGetter, Forward, Message, InlineResult, Conversation, - *event_cls, - loop=loop, thread_ident=__asyncthread.ident) - _syncify_wrap(TelegramClient, "start", loop, __asyncthread.ident) - - old_add_event_handler = TelegramClient.add_event_handler - old_remove_event_handler = TelegramClient.remove_event_handler - proxied_event_handlers = {} - - @functools.wraps(old_add_event_handler) - def add_proxied_event_handler(self, callback, *args, **kwargs): - async def _proxy(*pargs, **pkwargs): - await loop.run_in_executor( - executor, functools.partial(callback, *pargs, **pkwargs)) - - proxied_event_handlers[callback] = _proxy - - args = (self, _proxy, *args) - return old_add_event_handler(*args, **kwargs) - - @functools.wraps(old_remove_event_handler) - def remove_proxied_event_handler(self, callback, *args, **kwargs): - args = (self, proxied_event_handlers.get(callback, callback), *args) - return old_remove_event_handler(*args, **kwargs) - - TelegramClient.add_event_handler = add_proxied_event_handler - TelegramClient.remove_event_handler = remove_proxied_event_handler - - def run_until_disconnected(self): - return _sync_result(loop, self._run_until_disconnected()) - - TelegramClient.run_until_disconnected = run_until_disconnected - - return __asyncthread - - -def stop(): - """ - Stops the fully synchronous code. You - should call this before your script exits. - """ - global __asyncthread - if not __asyncthread: - raise RuntimeError("Can't find asyncio thread") - __asyncthread.loop.call_soon_threadsafe(__asyncthread.loop.stop) - __asyncthread.executor.shutdown()