diff --git a/readthedocs/concepts/asyncio.rst b/readthedocs/concepts/asyncio.rst index ef7c3cd3..cf775fcf 100644 --- a/readthedocs/concepts/asyncio.rst +++ b/readthedocs/concepts/asyncio.rst @@ -40,22 +40,22 @@ because tasks are smaller than threads, which are smaller than processes. What are asyncio basics? ======================== +The code samples below assume that you have Python 3.7 or greater installed. + .. code-block:: python # First we need the asyncio library import asyncio - # Then we need a loop to work with - loop = asyncio.get_event_loop() - # We also need something to run async def main(): for char in 'Hello, world!\n': print(char, end='', flush=True) await asyncio.sleep(0.2) - # Then, we need to run the loop with a task - loop.run_until_complete(main()) + # Then, we can create a new asyncio loop and use it to run our coroutine. + # The creation and tear-down of the loop is hidden away from us. + asyncio.run(main()) What does telethon.sync do? @@ -101,7 +101,7 @@ Instead of this: # or, using asyncio's default loop (it's the same) import asyncio - loop = asyncio.get_event_loop() # == client.loop + loop = asyncio.get_running_loop() # == client.loop me = loop.run_until_complete(client.get_me()) print(me.username) @@ -158,13 +158,10 @@ loops or use ``async with``: print(message.sender.username) - loop = asyncio.get_event_loop() - # ^ this assigns the default event loop from the main thread to a variable - - loop.run_until_complete(main()) - # ^ this runs the *entire* loop until the main() function finishes. - # While the main() function does not finish, the loop will be running. - # While the loop is running, you can't run it again. + asyncio.run(main()) + # ^ this will create a new asyncio loop behind the scenes and tear it down + # once the function returns. It will run the loop untiil main finishes. + # You should only use this function if there is no other loop running. The ``await`` keyword blocks the *current* task, and the loop can run @@ -184,14 +181,14 @@ concurrently: await asyncio.sleep(delay) # await tells the loop this task is "busy" print('world') # eventually the loop finishes all tasks - loop = asyncio.get_event_loop() # get the default loop for the main thread - loop.create_task(world(2)) # create the world task, passing 2 as delay - loop.create_task(hello(delay=1)) # another task, but with delay 1 + async def main(): + asyncio.create_task(world(2)) # create the world task, passing 2 as delay + asyncio.create_task(hello(delay=1)) # another task, but with delay 1 + await asyncio.sleep(3) # wait for three seconds before exiting + try: - # run the event loop forever; ctrl+c to stop it - # we could also run the loop for three seconds: - # loop.run_until_complete(asyncio.sleep(3)) - loop.run_forever() + # create a new temporary asyncio loop and use it to run main + asyncio.run(main()) except KeyboardInterrupt: pass @@ -209,10 +206,15 @@ The same example, but without the comment noise: await asyncio.sleep(delay) print('world') - loop = asyncio.get_event_loop() - loop.create_task(world(2)) - loop.create_task(hello(1)) - loop.run_until_complete(asyncio.sleep(3)) + async def main(): + asyncio.create_task(world(2)) + asyncio.create_task(hello(delay=1)) + await asyncio.sleep(3) + + try: + asyncio.run(main()) + except KeyboardInterrupt: + pass Can I use threads? @@ -250,9 +252,9 @@ You may have seen this error: RuntimeError: There is no current event loop in thread 'Thread-1'. -It just means you didn't create a loop for that thread, and if you don't -pass a loop when creating the client, it uses ``asyncio.get_event_loop()``, -which only works in the main thread. +It just means you didn't create a loop for that thread. Please refer to +the ``asyncio`` documentation to correctly learn how to set the event loop +for non-main threads. client.run_until_disconnected() blocks! diff --git a/readthedocs/concepts/updates.rst b/readthedocs/concepts/updates.rst index 624c843b..249d143e 100644 --- a/readthedocs/concepts/updates.rst +++ b/readthedocs/concepts/updates.rst @@ -191,8 +191,7 @@ so the code above and the following are equivalent: async def main(): await client.disconnected - loop = asyncio.get_event_loop() - loop.run_until_complete(main()) + asyncio.run(main()) You could also run `client.disconnected diff --git a/readthedocs/misc/changelog.rst b/readthedocs/misc/changelog.rst index af77d83b..7704530e 100644 --- a/readthedocs/misc/changelog.rst +++ b/readthedocs/misc/changelog.rst @@ -2088,7 +2088,7 @@ the scenes! This means you're now able to do both of the following: async def main(): await client.send_message('me', 'Hello!') - asyncio.get_event_loop().run_until_complete(main()) + asyncio.run(main()) # ...can be rewritten as: diff --git a/readthedocs/misc/compatibility-and-convenience.rst b/readthedocs/misc/compatibility-and-convenience.rst index 2d0769d4..ab9a103a 100644 --- a/readthedocs/misc/compatibility-and-convenience.rst +++ b/readthedocs/misc/compatibility-and-convenience.rst @@ -161,19 +161,17 @@ just get rid of ``telethon.sync`` and work inside an ``async def``: await client.run_until_disconnected() - loop = asyncio.get_event_loop() - loop.run_until_complete(main()) + asyncio.run(main()) -The ``telethon.sync`` magic module simply wraps every method behind: +The ``telethon.sync`` magic module essentially wraps every method behind: .. code-block:: python - loop = asyncio.get_event_loop() - loop.run_until_complete(main()) + asyncio.run(main()) -So that you don't have to write it yourself every time. That's the -overhead you pay if you import it, and what you save if you don't. +With some other tricks, so that you don't have to write it yourself every time. +That's the overhead you pay if you import it, and what you save if you don't. Learning ======== diff --git a/telethon/client/telegrambaseclient.py b/telethon/client/telegrambaseclient.py index e57c44e4..e2f5ee80 100644 --- a/telethon/client/telegrambaseclient.py +++ b/telethon/client/telegrambaseclient.py @@ -192,7 +192,7 @@ class TelegramBaseClient(abc.ABC): Defaults to `lang_code`. loop (`asyncio.AbstractEventLoop`, optional): - Asyncio event loop to use. Defaults to `asyncio.get_event_loop()`. + Asyncio event loop to use. Defaults to `asyncio.get_running_loop()`. This argument is ignored. base_logger (`str` | `logging.Logger`, optional): @@ -470,7 +470,7 @@ class TelegramBaseClient(abc.ABC): # Join the task (wait for it to complete) await task """ - return asyncio.get_event_loop() + return helpers.get_running_loop() @property def disconnected(self: 'TelegramClient') -> asyncio.Future: diff --git a/telethon/events/inlinequery.py b/telethon/events/inlinequery.py index 75cfcb01..98d8e124 100644 --- a/telethon/events/inlinequery.py +++ b/telethon/events/inlinequery.py @@ -4,7 +4,7 @@ import re import asyncio from .common import EventBuilder, EventCommon, name_inner_event -from .. import utils +from .. import utils, helpers from ..tl import types, functions, custom from ..tl.custom.sendergetter import SenderGetter @@ -242,6 +242,6 @@ class InlineQuery(EventBuilder): if inspect.isawaitable(obj): return asyncio.ensure_future(obj) - f = asyncio.get_event_loop().create_future() + f = helpers.get_running_loop().create_future() f.set_result(obj) return f diff --git a/telethon/helpers.py b/telethon/helpers.py index 9ddfe470..81f9a607 100644 --- a/telethon/helpers.py +++ b/telethon/helpers.py @@ -153,7 +153,7 @@ def retry_range(retries, force_retry=True): while attempt != retries: attempt += 1 yield attempt - + async def _maybe_await(value): @@ -426,7 +426,10 @@ class _FileStream(io.IOBase): # endregion def get_running_loop(): - if sys.version_info[:2] <= (3, 6): - return asyncio._get_running_loop() - - return asyncio.get_running_loop() + if sys.version_info >= (3, 7): + try: + return asyncio.get_running_loop() + except RuntimeError: + return asyncio.get_event_loop_policy().get_event_loop() + else: + return asyncio.get_event_loop() diff --git a/telethon/network/connection/connection.py b/telethon/network/connection/connection.py index c9934667..2b2dc55a 100644 --- a/telethon/network/connection/connection.py +++ b/telethon/network/connection/connection.py @@ -155,7 +155,7 @@ class Connection(abc.ABC): # Actual TCP connection is performed here. await asyncio.wait_for( - asyncio.get_event_loop().sock_connect(sock=sock, address=address), + helpers.get_running_loop().sock_connect(sock=sock, address=address), timeout=timeout ) @@ -190,7 +190,7 @@ class Connection(abc.ABC): # Actual TCP connection and negotiation performed here. await asyncio.wait_for( - asyncio.get_event_loop().sock_connect(sock=sock, address=address), + helpers.get_running_loop().sock_connect(sock=sock, address=address), timeout=timeout ) @@ -244,7 +244,7 @@ class Connection(abc.ABC): await self._connect(timeout=timeout, ssl=ssl) self._connected = True - loop = asyncio.get_event_loop() + loop = helpers.get_running_loop() self._send_task = loop.create_task(self._send_loop()) self._recv_task = loop.create_task(self._recv_loop()) diff --git a/telethon/network/mtprotosender.py b/telethon/network/mtprotosender.py index b7eee784..1c666185 100644 --- a/telethon/network/mtprotosender.py +++ b/telethon/network/mtprotosender.py @@ -68,7 +68,7 @@ class MTProtoSender: # pending futures should be cancelled. self._user_connected = False self._reconnecting = False - self._disconnected = asyncio.get_event_loop().create_future() + self._disconnected = helpers.get_running_loop().create_future() self._disconnected.set_result(None) # We need to join the loops upon disconnection @@ -261,7 +261,7 @@ class MTProtoSender: await self._disconnect(error=e) raise e - loop = asyncio.get_event_loop() + loop = helpers.get_running_loop() self._log.debug('Starting send loop') self._send_loop_handle = loop.create_task(self._send_loop()) @@ -400,7 +400,7 @@ class MTProtoSender: self._pending_state.clear() if self._auto_reconnect_callback: - asyncio.get_event_loop().create_task(self._auto_reconnect_callback()) + helpers.get_running_loop().create_task(self._auto_reconnect_callback()) break else: @@ -425,7 +425,7 @@ class MTProtoSender: # gets stuck. # TODO It still gets stuck? Investigate where and why. self._reconnecting = True - asyncio.get_event_loop().create_task(self._reconnect(error)) + helpers.get_running_loop().create_task(self._reconnect(error)) def _keepalive_ping(self, rnd_id): """ diff --git a/telethon/sync.py b/telethon/sync.py index 80b80bea..f647670a 100644 --- a/telethon/sync.py +++ b/telethon/sync.py @@ -14,7 +14,7 @@ import asyncio import functools import inspect -from . import events, errors, utils, connection +from . import events, errors, utils, connection, helpers from .client.account import _TakeoutClient from .client.telegramclient import TelegramClient from .tl import types, functions, custom @@ -32,7 +32,7 @@ def _syncify_wrap(t, method_name): @functools.wraps(method) def syncified(*args, **kwargs): coro = method(*args, **kwargs) - loop = asyncio.get_event_loop() + loop = helpers.get_running_loop() if loop.is_running(): return coro else: diff --git a/telethon_examples/gui.py b/telethon_examples/gui.py index bd241f60..275ffcc5 100644 --- a/telethon_examples/gui.py +++ b/telethon_examples/gui.py @@ -53,7 +53,7 @@ def callback(func): def wrapped(*args, **kwargs): result = func(*args, **kwargs) if inspect.iscoroutine(result): - aio_loop.create_task(result) + asyncio.create_task(result) return wrapped @@ -369,10 +369,4 @@ async def main(interval=0.05): if __name__ == "__main__": - # Some boilerplate code to set up the main method - aio_loop = asyncio.get_event_loop() - try: - aio_loop.run_until_complete(main()) - finally: - if not aio_loop.is_closed(): - aio_loop.close() + asyncio.run(main()) diff --git a/telethon_examples/interactive_telegram_client.py b/telethon_examples/interactive_telegram_client.py index 88f491de..d327b72f 100644 --- a/telethon_examples/interactive_telegram_client.py +++ b/telethon_examples/interactive_telegram_client.py @@ -9,9 +9,6 @@ from telethon.errors import SessionPasswordNeededError from telethon.network import ConnectionTcpAbridged from telethon.utils import get_display_name -# Create a global variable to hold the loop we will be using -loop = asyncio.get_event_loop() - def sprint(string, *args, **kwargs): """Safe Print (handle UnicodeEncodeErrors on some terminals)""" @@ -50,7 +47,7 @@ async def async_input(prompt): let the loop run while we wait for input. """ print(prompt, end='', flush=True) - return (await loop.run_in_executor(None, sys.stdin.readline)).rstrip() + return (await asyncio.get_running_loop().run_in_executor(None, sys.stdin.readline)).rstrip() def get_env(name, message, cast=str): @@ -109,34 +106,34 @@ class InteractiveTelegramClient(TelegramClient): # media known the message ID, for every message having media. self.found_media = {} + async def init(self): # Calling .connect() may raise a connection error False, so you need # to except those before continuing. Otherwise you may want to retry # as done here. print('Connecting to Telegram servers...') try: - loop.run_until_complete(self.connect()) + await self.connect() except IOError: # We handle IOError and not ConnectionError because # PySocks' errors do not subclass ConnectionError # (so this will work with and without proxies). print('Initial connection failed. Retrying...') - loop.run_until_complete(self.connect()) + await self.connect() # If the user hasn't called .sign_in() or .sign_up() yet, they won't # be authorized. The first thing you must do is authorize. Calling # .sign_in() should only be done once as the information is saved on # the *.session file so you don't need to enter the code every time. - if not loop.run_until_complete(self.is_user_authorized()): + if not await self.is_user_authorized(): print('First run. Sending code request...') user_phone = input('Enter your phone: ') - loop.run_until_complete(self.sign_in(user_phone)) + await self.sign_in(user_phone) self_user = None while self_user is None: code = input('Enter the code you just received: ') try: - self_user =\ - loop.run_until_complete(self.sign_in(code=code)) + self_user = await self.sign_in(code=code) # Two-step verification may be enabled, and .sign_in will # raise this error. If that's the case ask for the password. @@ -146,8 +143,7 @@ class InteractiveTelegramClient(TelegramClient): pw = getpass('Two step verification is enabled. ' 'Please enter your password: ') - self_user =\ - loop.run_until_complete(self.sign_in(password=pw)) + self_user = await self.sign_in(password=pw) async def run(self): """Main loop of the TelegramClient, will wait for user action""" @@ -397,9 +393,14 @@ class InteractiveTelegramClient(TelegramClient): )) -if __name__ == '__main__': +async def main(): SESSION = os.environ.get('TG_SESSION', 'interactive') API_ID = get_env('TG_API_ID', 'Enter your API ID: ', int) API_HASH = get_env('TG_API_HASH', 'Enter your API hash: ') client = InteractiveTelegramClient(SESSION, API_ID, API_HASH) - loop.run_until_complete(client.run()) + await client.init() + await client.run() + + +if __name__ == '__main__': + asyncio.run() diff --git a/telethon_examples/payment.py b/telethon_examples/payment.py index c73afbca..03735c44 100644 --- a/telethon_examples/payment.py +++ b/telethon_examples/payment.py @@ -7,8 +7,6 @@ import os import time import sys -loop = asyncio.get_event_loop() - """ Provider token can be obtained via @BotFather. more info at https://core.telegram.org/bots/payments#getting-a-token @@ -180,4 +178,4 @@ if __name__ == '__main__': if not provider_token: logger.error("No provider token supplied.") exit(1) - loop.run_until_complete(main()) + asyncio.run(main()) diff --git a/telethon_examples/quart_login.py b/telethon_examples/quart_login.py index 98fb35de..50498dc8 100644 --- a/telethon_examples/quart_login.py +++ b/telethon_examples/quart_login.py @@ -1,7 +1,6 @@ import base64 import os -import hypercorn.asyncio from quart import Quart, render_template_string, request from telethon import TelegramClient, utils @@ -82,6 +81,8 @@ async def format_message(message): # Connect the client before we start serving with Quart @app.before_serving async def startup(): + # After connecting, the client will create additional asyncio tasks that run until it's disconnected again. + # Be careful to not mix different asyncio loops during a client's lifetime, or things won't work properly! await client.connect() @@ -129,24 +130,11 @@ async def root(): return await render_template_string(BASE_TEMPLATE, content=CODE_FORM) -async def main(): - await hypercorn.asyncio.serve(app, hypercorn.Config()) - - # By default, `Quart.run` uses `asyncio.run()`, which creates a new asyncio -# event loop. If we create the `TelegramClient` before, `telethon` will -# use `asyncio.get_event_loop()`, which is the implicit loop in the main -# thread. These two loops are different, and it won't work. +# event loop. If we had connected the `TelegramClient` before, `telethon` will +# use `asyncio.get_running_loop()` to create some additional tasks. If these +# loops are different, it won't work. # -# So, we have to manually pass the same `loop` to both applications to -# make 100% sure it works and to avoid headaches. -# -# To run Quart inside `async def`, we must use `hypercorn.asyncio.serve()` -# directly. -# -# This example creates a global client outside of Quart handlers. -# If you create the client inside the handlers (common case), you -# won't have to worry about any of this, but it's still good to be -# explicit about the event loop. +# To keep things simple, be sure to not create multiple asyncio loops! if __name__ == '__main__': - client.loop.run_until_complete(main()) + app.run()