Get rid of full_sync

This commit is contained in:
Lonami Exo 2018-11-03 12:44:49 +01:00
parent c70943bb0e
commit 8e6b98669a
3 changed files with 43 additions and 211 deletions

View File

@ -14,31 +14,48 @@ is there to tell you when these important changes happen.
Compatibility 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.** If you have old code, **just use old versions** of the library! There is
It comes with a cost, and new projects will greatly benefit from using nothing wrong with that other than not getting new updates or fixes, but
``asyncio`` by default such as increased speed and easier reasoning about using a fixed version with ``pip install telethon==0.19.1.6`` is easy
the code flow. You should only enable it for old projects you don't have enough to do.
the time to upgrade to ``asyncio``.
There exists a fair amount of code online using Telethon before it reached You might want to consider using `Virtual Environments
its 1.0 version, where it became fully asynchronous by default. Since it was <https://docs.python.org/3/tutorial/venv.html>`_ in your projects.
necessary to clean some things, compatibility was not kept 100% but the
changes are simple: 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 .. code-block:: python
# 1. The library no longer uses threads. # 1. Import the client from telethon.sync
# Add this at the **beginning** of your script to work around that. from telethon.sync import TelegramClient
from telethon import full_sync
full_sync.enable() # 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: # ...for this:
client.connect() with client:
... # REST OF YOUR CODE
# 3. client.idle() no longer exists. # 3. client.idle() no longer exists.
# Change this... # Change this...
@ -52,11 +69,10 @@ changes are simple:
# ...to this: # ...to this:
client.add_event_handler(handler) client.add_event_handler(handler)
# 5. It's good practice to stop the full_sync mode once you're done
try: In addition, all the update handlers must be ``async def``, and you need
... # all your code in here to ``await`` method calls that rely on network requests, such as getting
finally: the chat or sender. If you don't use updates, you're done!
full_sync.stop()
Convenience Convenience
@ -75,8 +91,8 @@ Convenience
This makes the examples shorter and easier to think about. 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 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. forget about ``asyncio`` and just work with sequential code. This can prove
This can prove to be a powerful hybrid for running under the Python REPL too. to be a powerful hybrid for running under the Python REPL too.
.. code-block:: python .. code-block:: python
@ -122,7 +138,7 @@ Speed
When you're ready to micro-optimize your application, or if you simply 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, 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 .. code-block:: python

View File

@ -2,7 +2,7 @@ import logging
from .client.telegramclient import TelegramClient from .client.telegramclient import TelegramClient
from .network import connection from .network import connection
from .tl import types, functions, custom from .tl import types, functions, custom
from . import version, events, utils, errors, full_sync from . import version, events, utils, errors
__version__ = version.__version__ __version__ = version.__version__

View File

@ -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()