From 2a933ac3bd02cc36b39e3e967b850c6e4511016d Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sat, 11 Sep 2021 14:05:24 +0200 Subject: [PATCH] Remove sync hack --- .github/ISSUE_TEMPLATE/bug-report.md | 2 +- README.rst | 22 +++--- readthedocs/basic/quick-start.rst | 12 ++-- readthedocs/basic/signing-in.rst | 20 ++++-- readthedocs/concepts/asyncio.rst | 93 +------------------------ readthedocs/concepts/sessions.rst | 8 +-- readthedocs/index.rst | 20 +++--- readthedocs/misc/v2-migration-guide.rst | 19 +++++ readthedocs/quick-references/faq.rst | 23 +----- telethon/_client/account.py | 3 - telethon/_client/auth.py | 8 +-- telethon/_client/chats.py | 3 - telethon/_client/downloads.py | 3 - telethon/_client/telegrambaseclient.py | 15 +--- telethon/_client/telegramclient.py | 3 - telethon/_client/updates.py | 12 +--- telethon/helpers.py | 30 +------- telethon/requestiter.py | 18 ----- telethon/sync.py | 74 -------------------- telethon/tl/custom/conversation.py | 3 - telethon_generator/generators/docs.py | 2 +- tests/telethon/test_helpers.py | 37 ---------- 22 files changed, 77 insertions(+), 353 deletions(-) delete mode 100644 telethon/sync.py diff --git a/.github/ISSUE_TEMPLATE/bug-report.md b/.github/ISSUE_TEMPLATE/bug-report.md index dc7a26c2..1e7ebec6 100644 --- a/.github/ISSUE_TEMPLATE/bug-report.md +++ b/.github/ISSUE_TEMPLATE/bug-report.md @@ -14,7 +14,7 @@ assignees: '' **Code that causes the issue** ```python -from telethon.sync import TelegramClient +from telethon import TelegramClient ... ``` diff --git a/README.rst b/README.rst index f1eb902c..15985350 100755 --- a/README.rst +++ b/README.rst @@ -35,15 +35,19 @@ Creating a client .. code-block:: python - from telethon import TelegramClient, events, sync + import asyncio + from telethon import TelegramClient, events # These example values won't work. You must get your own api_id and # api_hash from https://my.telegram.org, under API Development. api_id = 12345 api_hash = '0123456789abcdef0123456789abcdef' - client = TelegramClient('session_name', api_id, api_hash) - client.start() + async def main(): + client = TelegramClient('session_name', api_id, api_hash) + await client.start() + + asyncio.run(main()) Doing stuff @@ -51,14 +55,14 @@ Doing stuff .. code-block:: python - print(client.get_me().stringify()) + print((await client.get_me()).stringify()) - client.send_message('username', 'Hello! Talking to you from Telethon') - client.send_file('username', '/home/myself/Pictures/holidays.jpg') + await client.send_message('username', 'Hello! Talking to you from Telethon') + await client.send_file('username', '/home/myself/Pictures/holidays.jpg') - client.download_profile_photo('me') - messages = client.get_messages('username') - messages[0].download_media() + await client.download_profile_photo('me') + messages = await client.get_messages('username') + await messages[0].download_media() @client.on(events.NewMessage(pattern='(?i)hi|hello')) async def handler(event): diff --git a/readthedocs/basic/quick-start.rst b/readthedocs/basic/quick-start.rst index cd187c81..8dbf928d 100644 --- a/readthedocs/basic/quick-start.rst +++ b/readthedocs/basic/quick-start.rst @@ -100,12 +100,8 @@ proceeding. We will see all the available methods later on. # Most of your code should go here. # You can of course make and use your own async def (do_something). # They only need to be async if they need to await things. - me = await client.get_me() - await do_something(me) + async with client: + me = await client.get_me() + await do_something(me) - with client: - client.loop.run_until_complete(main()) - - After you understand this, you may use the ``telethon.sync`` hack if you - want do so (see :ref:`compatibility-and-convenience`), but note you may - run into other issues (iPython, Anaconda, etc. have some issues with it). + client.loop.run_until_complete(main()) diff --git a/readthedocs/basic/signing-in.rst b/readthedocs/basic/signing-in.rst index 9fb14853..7f584a95 100644 --- a/readthedocs/basic/signing-in.rst +++ b/readthedocs/basic/signing-in.rst @@ -55,9 +55,12 @@ We can finally write some code to log into our account! api_id = 12345 api_hash = '0123456789abcdef0123456789abcdef' - # The first parameter is the .session file name (absolute paths allowed) - with TelegramClient('anon', api_id, api_hash) as client: - client.loop.run_until_complete(client.send_message('me', 'Hello, myself!')) + async def main(): + # The first parameter is the .session file name (absolute paths allowed) + async with TelegramClient('anon', api_id, api_hash) as client: + await client.send_message('me', 'Hello, myself!') + + client.loop.run_until_complete(main()) In the first line, we import the class name so we can create an instance @@ -95,7 +98,7 @@ You will still need an API ID and hash, but the process is very similar: .. code-block:: python - from telethon.sync import TelegramClient + from telethon import TelegramClient api_id = 12345 api_hash = '0123456789abcdef0123456789abcdef' @@ -104,9 +107,12 @@ You will still need an API ID and hash, but the process is very similar: # We have to manually call "start" if we want an explicit bot token bot = TelegramClient('bot', api_id, api_hash).start(bot_token=bot_token) - # But then we can use the client instance as usual - with bot: - ... + async def main(): + # But then we can use the client instance as usual + async with bot: + ... + + client.loop.run_until_complete(main()) To get a bot account, you need to talk diff --git a/readthedocs/concepts/asyncio.rst b/readthedocs/concepts/asyncio.rst index ef7c3cd3..dd85f957 100644 --- a/readthedocs/concepts/asyncio.rst +++ b/readthedocs/concepts/asyncio.rst @@ -58,84 +58,6 @@ What are asyncio basics? loop.run_until_complete(main()) -What does telethon.sync do? -=========================== - -The moment you import any of these: - -.. code-block:: python - - from telethon import sync, ... - # or - from telethon.sync import ... - # or - import telethon.sync - -The ``sync`` module rewrites most ``async def`` -methods in Telethon to something similar to this: - -.. code-block:: python - - def new_method(): - result = original_method() - if loop.is_running(): - # the loop is already running, return the await-able to the user - return result - else: - # the loop is not running yet, so we can run it for the user - return loop.run_until_complete(result) - - -That means you can do this: - -.. code-block:: python - - print(client.get_me().username) - -Instead of this: - -.. code-block:: python - - me = client.loop.run_until_complete(client.get_me()) - print(me.username) - - # or, using asyncio's default loop (it's the same) - import asyncio - loop = asyncio.get_event_loop() # == client.loop - me = loop.run_until_complete(client.get_me()) - print(me.username) - - -As you can see, it's a lot of boilerplate and noise having to type -``run_until_complete`` all the time, so you can let the magic module -to rewrite it for you. But notice the comment above: it won't run -the loop if it's already running, because it can't. That means this: - -.. code-block:: python - - async def main(): - # 3. the loop is running here - print( - client.get_me() # 4. this will return a coroutine! - .username # 5. this fails, coroutines don't have usernames - ) - - loop.run_until_complete( # 2. run the loop and the ``main()`` coroutine - main() # 1. calling ``async def`` "returns" a coroutine - ) - - -Will fail. So if you're inside an ``async def``, then the loop is -running, and if the loop is running, you must ``await`` things yourself: - -.. code-block:: python - - async def main(): - print((await client.get_me()).username) - - loop.run_until_complete(main()) - - What are async, await and coroutines? ===================================== @@ -275,7 +197,7 @@ in it. So if you want to run *other* code, create tasks for it: loop.create_task(clock()) ... - client.run_until_disconnected() + await client.run_until_disconnected() This creates a task for a clock that prints the time every second. You don't need to use `client.run_until_disconnected() @@ -344,19 +266,6 @@ When you use a library, you're not limited to use only its methods. You can combine all the libraries you want. People seem to forget this simple fact! -Why does client.start() work outside async? -=========================================== - -Because it's so common that it's really convenient to offer said -functionality by default. This means you can set up all your event -handlers and start the client without worrying about loops at all. - -Using the client in a ``with`` block, `start -`, `run_until_disconnected -`, and -`disconnect ` -all support this. - Where can I read more? ====================== diff --git a/readthedocs/concepts/sessions.rst b/readthedocs/concepts/sessions.rst index a94bc773..8ba75938 100644 --- a/readthedocs/concepts/sessions.rst +++ b/readthedocs/concepts/sessions.rst @@ -73,10 +73,10 @@ You can import these ``from telethon.sessions``. For example, using the .. code-block:: python - from telethon.sync import TelegramClient + from telethon import TelegramClient from telethon.sessions import StringSession - with TelegramClient(StringSession(string), api_id, api_hash) as client: + async with TelegramClient(StringSession(string), api_id, api_hash) as client: ... # use the client # Save the string session as a string; you should decide how @@ -129,10 +129,10 @@ The easiest way to generate a string session is as follows: .. code-block:: python - from telethon.sync import TelegramClient + from telethon import TelegramClient from telethon.sessions import StringSession - with TelegramClient(StringSession(), api_id, api_hash) as client: + async with TelegramClient(StringSession(), api_id, api_hash) as client: print(client.session.save()) diff --git a/readthedocs/index.rst b/readthedocs/index.rst index 827823cd..1794ce72 100644 --- a/readthedocs/index.rst +++ b/readthedocs/index.rst @@ -4,17 +4,21 @@ Telethon's Documentation .. code-block:: python - from telethon.sync import TelegramClient, events + import asyncio + from telethon import TelegramClient, events - with TelegramClient('name', api_id, api_hash) as client: - client.send_message('me', 'Hello, myself!') - print(client.download_profile_photo('me')) + async def main(): + async with TelegramClient('name', api_id, api_hash) as client: + await client.send_message('me', 'Hello, myself!') + print(await client.download_profile_photo('me')) - @client.on(events.NewMessage(pattern='(?i).*Hello')) - async def handler(event): - await event.reply('Hey!') + @client.on(events.NewMessage(pattern='(?i).*Hello')) + async def handler(event): + await event.reply('Hey!') - client.run_until_disconnected() + await client.run_until_disconnected() + + asyncio.run(main()) * Are you new here? Jump straight into :ref:`installation`! diff --git a/readthedocs/misc/v2-migration-guide.rst b/readthedocs/misc/v2-migration-guide.rst index 7655e673..5c748895 100644 --- a/readthedocs/misc/v2-migration-guide.rst +++ b/readthedocs/misc/v2-migration-guide.rst @@ -32,3 +32,22 @@ change even across minor version changes, and thus have your code break. * The ``telethon.client`` module is now ``telethon._client``, meaning you should stop relying on anything inside of it. This includes all of the subclasses that used to exist (like ``UserMethods``). + + +Synchronous compatibility mode has been removed +----------------------------------------------- + +The "sync hack" (which kicked in as soon as anything from ``telethon.sync`` was imported) has been +removed. This implies: + +* The ``telethon.sync`` module is gone. +* Synchronous context-managers (``with`` as opposed to ``async with``) are no longer supported. + Most notably, you can no longer do ``with client``. It must be ``async with client`` now. +* The "smart" behaviour of the following methods has been removed and now they no longer work in + a synchronous context when the ``asyncio`` event loop was not running. This means they now need + to be used with ``await`` (or, alternatively, manually used with ``loop.run_until_complete``): + * ``start`` + * ``disconnect`` + * ``run_until_disconnected`` + +// TODO provide standalone alternative for this? diff --git a/readthedocs/quick-references/faq.rst b/readthedocs/quick-references/faq.rst index df267b86..0b1e28b0 100644 --- a/readthedocs/quick-references/faq.rst +++ b/readthedocs/quick-references/faq.rst @@ -127,14 +127,7 @@ This is basic Python knowledge. You should use the dot operator: AttributeError: 'coroutine' object has no attribute 'id' ======================================================== -You either forgot to: - -.. code-block:: python - - import telethon.sync - # ^^^^^ import sync - -Or: +Telethon is an asynchronous library. This means you need to ``await`` most methods: .. code-block:: python @@ -218,19 +211,7 @@ Check out `quart_login.py`_ for an example web-application based on Quart. Can I use Anaconda/Spyder/IPython with the library? =================================================== -Yes, but these interpreters run the asyncio event loop implicitly, -which interferes with the ``telethon.sync`` magic module. - -If you use them, you should **not** import ``sync``: - -.. code-block:: python - - # Change any of these...: - from telethon import TelegramClient, sync, ... - from telethon.sync import TelegramClient, ... - - # ...with this: - from telethon import TelegramClient, ... +Yes, but these interpreters run the asyncio event loop implicitly, so be wary of that. You are also more likely to get "sqlite3.OperationalError: database is locked" with them. If they cause too much trouble, just write your code in a ``.py`` diff --git a/telethon/_client/account.py b/telethon/_client/account.py index 6300b791..46e0b6dc 100644 --- a/telethon/_client/account.py +++ b/telethon/_client/account.py @@ -56,9 +56,6 @@ class _TakeoutClient: raise ValueError("Failed to finish the takeout.") self.session.takeout_id = None - __enter__ = helpers._sync_enter - __exit__ = helpers._sync_exit - async def __call__(self, request, ordered=False): takeout_id = self.__client.session.takeout_id if takeout_id is None: diff --git a/telethon/_client/auth.py b/telethon/_client/auth.py index c27f6512..3699d795 100644 --- a/telethon/_client/auth.py +++ b/telethon/_client/auth.py @@ -12,7 +12,7 @@ if typing.TYPE_CHECKING: from .telegramclient import TelegramClient -def start( +async def start( self: 'TelegramClient', phone: typing.Callable[[], str] = lambda: input('Please enter your phone (or bot token): '), password: typing.Callable[[], str] = lambda: getpass.getpass('Please enter your password: '), @@ -39,7 +39,7 @@ def start( raise ValueError('Both a phone and a bot token provided, ' 'must only provide one of either') - coro = self._start( + return await self._start( phone=phone, password=password, bot_token=bot_token, @@ -49,10 +49,6 @@ def start( last_name=last_name, max_attempts=max_attempts ) - return ( - coro if self.loop.is_running() - else self.loop.run_until_complete(coro) - ) async def _start( self: 'TelegramClient', phone, password, bot_token, force_sms, diff --git a/telethon/_client/chats.py b/telethon/_client/chats.py index 0429d563..4147b45b 100644 --- a/telethon/_client/chats.py +++ b/telethon/_client/chats.py @@ -76,9 +76,6 @@ class _ChatAction: self._task = None - __enter__ = helpers._sync_enter - __exit__ = helpers._sync_exit - async def _update(self): try: while self._running: diff --git a/telethon/_client/downloads.py b/telethon/_client/downloads.py index 2150dc92..4500df33 100644 --- a/telethon/_client/downloads.py +++ b/telethon/_client/downloads.py @@ -137,9 +137,6 @@ class _DirectDownloadIter(RequestIter): async def __aexit__(self, *args): await self.close() - __enter__ = helpers._sync_enter - __exit__ = helpers._sync_exit - class _GenericDownloadIter(_DirectDownloadIter): async def _load_next_chunk(self): diff --git a/telethon/_client/telegrambaseclient.py b/telethon/_client/telegrambaseclient.py index 79ea85b3..16822d6a 100644 --- a/telethon/_client/telegrambaseclient.py +++ b/telethon/_client/telegrambaseclient.py @@ -337,19 +337,8 @@ def is_connected(self: 'TelegramClient') -> bool: sender = getattr(self, '_sender', None) return sender and sender.is_connected() -def disconnect(self: 'TelegramClient'): - if self.loop.is_running(): - return self._disconnect_coro() - else: - try: - self.loop.run_until_complete(self._disconnect_coro()) - except RuntimeError: - # Python 3.5.x complains when called from - # `__aexit__` and there were pending updates with: - # "Event loop stopped before Future completed." - # - # However, it doesn't really make a lot of sense. - pass +async def disconnect(self: 'TelegramClient'): + return await self._disconnect_coro() def set_proxy(self: 'TelegramClient', proxy: typing.Union[tuple, dict]): init_proxy = None if not issubclass(self._connection, TcpMTProxy) else \ diff --git a/telethon/_client/telegramclient.py b/telethon/_client/telegramclient.py index 58f6b56a..1c50a805 100644 --- a/telethon/_client/telegramclient.py +++ b/telethon/_client/telegramclient.py @@ -619,9 +619,6 @@ class TelegramClient: async def __aexit__(self, *args): await self.disconnect() - __enter__ = helpers._sync_enter - __exit__ = helpers._sync_exit - # endregion Auth # region Bots diff --git a/telethon/_client/updates.py b/telethon/_client/updates.py index 4860a8cd..04d7fbfb 100644 --- a/telethon/_client/updates.py +++ b/telethon/_client/updates.py @@ -40,7 +40,7 @@ async def set_receive_updates(self: 'TelegramClient', receive_updates): if receive_updates: await self(functions.updates.GetStateRequest()) -def run_until_disconnected(self: 'TelegramClient'): +async def run_until_disconnected(self: 'TelegramClient'): """ Runs the event loop until the library is disconnected. @@ -75,15 +75,7 @@ def run_until_disconnected(self: 'TelegramClient'): # script from exiting. await client.run_until_disconnected() """ - if self.loop.is_running(): - return self._run_until_disconnected() - try: - return self.loop.run_until_complete(self._run_until_disconnected()) - except KeyboardInterrupt: - pass - finally: - # No loop.run_until_complete; it's already syncified - self.disconnect() + return await self._run_until_disconnected() def on(self: 'TelegramClient', event: EventBuilder): """ diff --git a/telethon/helpers.py b/telethon/helpers.py index 6c782b0b..f9297816 100644 --- a/telethon/helpers.py +++ b/telethon/helpers.py @@ -118,7 +118,7 @@ def retry_range(retries, force_retry=True): while attempt != retries: attempt += 1 yield attempt - + async def _maybe_await(value): @@ -165,34 +165,6 @@ async def _cancel(log, **tasks): '%s (%s)', name, type(task), task) -def _sync_enter(self): - """ - Helps to cut boilerplate on async context - managers that offer synchronous variants. - """ - if hasattr(self, 'loop'): - loop = self.loop - else: - loop = self._client.loop - - if loop.is_running(): - raise RuntimeError( - 'You must use "async with" if the event loop ' - 'is running (i.e. you are inside an "async def")' - ) - - return loop.run_until_complete(self.__aenter__()) - - -def _sync_exit(self, *args): - if hasattr(self, 'loop'): - loop = self.loop - else: - loop = self._client.loop - - return loop.run_until_complete(self.__aexit__(*args)) - - def _entity_type(entity): # This could be a `utils` method that just ran a few `isinstance` on # `utils.get_peer(...)`'s result. However, there are *a lot* of auto diff --git a/telethon/requestiter.py b/telethon/requestiter.py index fd28419d..6473fe0f 100644 --- a/telethon/requestiter.py +++ b/telethon/requestiter.py @@ -12,9 +12,6 @@ class RequestIter(abc.ABC): It has some facilities, such as automatically sleeping a desired amount of time between requests if needed (but not more). - Can be used synchronously if the event loop is not running and - as an asynchronous iterator otherwise. - `limit` is the total amount of items that the iterator should return. This is handled on this base class, and will be always ``>= 0``. @@ -82,12 +79,6 @@ class RequestIter(abc.ABC): self.index += 1 return result - def __next__(self): - try: - return self.client.loop.run_until_complete(self.__anext__()) - except StopAsyncIteration: - raise StopIteration - def __aiter__(self): self.buffer = None self.index = 0 @@ -95,15 +86,6 @@ class RequestIter(abc.ABC): self.left = self.limit return self - def __iter__(self): - if self.client.loop.is_running(): - raise RuntimeError( - 'You must use "async for" if the event loop ' - 'is running (i.e. you are inside an "async def")' - ) - - return self.__aiter__() - async def collect(self): """ Create a `self` iterator and collect it into a `TotalList` diff --git a/telethon/sync.py b/telethon/sync.py deleted file mode 100644 index 80b80bea..00000000 --- a/telethon/sync.py +++ /dev/null @@ -1,74 +0,0 @@ -""" -This magical module will rewrite all public methods in the public interface -of the library so they can run the loop on their own if it's not already -running. 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 the runtime overhead is relatively low. - -Some really common methods which are hardly used offer this ability by -default, such as ``.start()`` and ``.run_until_disconnected()`` (since -you may want to start, and then run until disconnected while using async -event handlers). -""" -import asyncio -import functools -import inspect - -from . import events, errors, utils, connection -from .client.account import _TakeoutClient -from .client.telegramclient import TelegramClient -from .tl import types, functions, custom -from .tl.custom import ( - Draft, Dialog, MessageButton, Forward, Button, - Message, InlineResult, Conversation -) -from .tl.custom.chatgetter import ChatGetter -from .tl.custom.sendergetter import SenderGetter - - -def _syncify_wrap(t, method_name): - method = getattr(t, method_name) - - @functools.wraps(method) - def syncified(*args, **kwargs): - coro = method(*args, **kwargs) - loop = asyncio.get_event_loop() - if loop.is_running(): - return coro - else: - return loop.run_until_complete(coro) - - # Save an accessible reference to the original method - setattr(syncified, '__tl.sync', method) - setattr(t, method_name, syncified) - - -def syncify(*types): - """ - Converts all the methods in the given types (class definitions) - into synchronous, which return either the coroutine or the result - based on whether ``asyncio's`` event loop is running. - """ - # Our asynchronous generators all are `RequestIter`, which already - # provide a synchronous iterator variant, so we don't need to worry - # about asyncgenfunction's here. - for t in types: - for name in dir(t): - if not name.startswith('_') or name == '__call__': - if inspect.iscoroutinefunction(getattr(t, name)): - _syncify_wrap(t, name) - - -syncify(TelegramClient, _TakeoutClient, Draft, Dialog, MessageButton, - ChatGetter, SenderGetter, Forward, Message, InlineResult, Conversation) - - -# Private special case, since a conversation's methods return -# futures (but the public function themselves are synchronous). -_syncify_wrap(Conversation, '_get_result') - -__all__ = [ - 'TelegramClient', 'Button', - 'types', 'functions', 'custom', 'errors', - 'events', 'utils', 'connection' -] diff --git a/telethon/tl/custom/conversation.py b/telethon/tl/custom/conversation.py index 6cb973d4..b99831f3 100644 --- a/telethon/tl/custom/conversation.py +++ b/telethon/tl/custom/conversation.py @@ -524,6 +524,3 @@ class Conversation(ChatGetter): del self._client._conversations[chat_id] self._cancel_all() - - __enter__ = helpers._sync_enter - __exit__ = helpers._sync_exit diff --git a/telethon_generator/generators/docs.py b/telethon_generator/generators/docs.py index 34b599ff..8b46e4d1 100755 --- a/telethon_generator/generators/docs.py +++ b/telethon_generator/generators/docs.py @@ -396,7 +396,7 @@ def _write_html_pages(tlobjects, methods, layer, input_res): docs.write('
') docs.write('''
\
-from telethon.sync import TelegramClient
+from telethon import TelegramClient
 from telethon import functions, types
 
 with TelegramClient(name, api_id, api_hash) as client:
diff --git a/tests/telethon/test_helpers.py b/tests/telethon/test_helpers.py
index 689db8af..5ac4a78e 100644
--- a/tests/telethon/test_helpers.py
+++ b/tests/telethon/test_helpers.py
@@ -14,43 +14,6 @@ def test_strip_text():
     # I can't interpret the rest of the code well enough yet
 
 
-class TestSyncifyAsyncContext:
-    class NoopContextManager:
-        def __init__(self, loop):
-            self.count = 0
-            self.loop = loop
-
-        async def __aenter__(self):
-            self.count += 1
-            return self
-
-        async def __aexit__(self, exc_type, *args):
-            assert exc_type is None
-            self.count -= 1
-
-        __enter__ = helpers._sync_enter
-        __exit__ = helpers._sync_exit
-
-    def test_sync_acontext(self, event_loop):
-        contm = self.NoopContextManager(event_loop)
-        assert contm.count == 0
-
-        with contm:
-            assert contm.count == 1
-
-        assert contm.count == 0
-
-    @pytest.mark.asyncio
-    async def test_async_acontext(self, event_loop):
-        contm = self.NoopContextManager(event_loop)
-        assert contm.count == 0
-
-        async with contm:
-            assert contm.count == 1
-
-        assert contm.count == 0
-
-
 def test_generate_key_data_from_nonce():
     gkdfn = helpers.generate_key_data_from_nonce