Remove sync hack

This commit is contained in:
Lonami Exo 2021-09-11 14:05:24 +02:00
parent 34e7b7cc9f
commit 2a933ac3bd
22 changed files with 77 additions and 353 deletions

View File

@ -14,7 +14,7 @@ assignees: ''
**Code that causes the issue**
```python
from telethon.sync import TelegramClient
from telethon import TelegramClient
...
```

View File

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

View File

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

View File

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

View File

@ -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
<telethon.client.auth.AuthMethods.start>`, `run_until_disconnected
<telethon.client.updates.UpdateMethods.run_until_disconnected>`, and
`disconnect <telethon.client.telegrambaseclient.TelegramBaseClient.disconnect>`
all support this.
Where can I read more?
======================

View File

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

View File

@ -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`!

View File

@ -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?

View File

@ -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``

View File

@ -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:

View File

@ -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,

View File

@ -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:

View File

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

View File

@ -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 \

View File

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

View File

@ -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):
"""

View File

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

View File

@ -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`

View File

@ -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'
]

View File

@ -524,6 +524,3 @@ class Conversation(ChatGetter):
del self._client._conversations[chat_id]
self._cancel_all()
__enter__ = helpers._sync_enter
__exit__ = helpers._sync_exit

View File

@ -396,7 +396,7 @@ def _write_html_pages(tlobjects, methods, layer, input_res):
docs.write('<details>')
docs.write('''<pre>\
<strong>from</strong> telethon.sync <strong>import</strong> TelegramClient
<strong>from</strong> telethon <strong>import</strong> TelegramClient
<strong>from</strong> telethon <strong>import</strong> functions, types
<strong>with</strong> TelegramClient(name, api_id, api_hash) <strong>as</strong> client:

View File

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