Document the magic sync module

This commit is contained in:
Lonami Exo
2018-06-25 21:14:58 +02:00
parent 551b0044ce
commit d65f8ecc0d
15 changed files with 389 additions and 278 deletions

View File

@@ -1,8 +1,117 @@
.. _asyncio-crash-course:
.. _asyncio-magic:
===========================
A Crash Course into asyncio
===========================
==================
Magic with asyncio
==================
The sync module
***************
It's time to tell you the truth. The library has been doing magic behind
the scenes. We're sorry to tell you this, but at least it wasn't dark magic!
You may have noticed one of these lines across the documentation:
.. code-block:: python
from telethon import sync
# or
import telethon.sync
Either of these lines will import the *magic* ``sync`` module. When you
import this module, you can suddenly use all the methods defined in the
:ref:`TelegramClient <telethon-client>` like so:
.. code-block:: python
client.send_message('me', 'Hello!')
for dialog in client.iter_dialogs():
print(dialog.title)
What happened behind the scenes is that all those methods, called *coroutines*,
were rewritten to be normal methods that will block (with some exceptions).
This means you can use the library without worrying about ``asyncio`` and
event loops.
However, this only works until you run the event loop yourself explicitly:
.. code-block:: python
import asyncio
async def coro():
client.send_message('me', 'Hello!') # <- no longer works!
loop = asyncio.get_event_loop()
loop.run_until_complete(coro())
What things will work and when?
*******************************
You can use all the methods in the :ref:`TelegramClient <telethon-client>`
in a synchronous, blocking way without trouble, as long as you're not running
the loop as we saw above (the ``loop.run_until_complete(...)`` line runs "the
loop"). If you're running the loop, then *you* are the one responsible to
``await`` everything. So to fix the code above:
.. code-block:: python
import asyncio
async def coro():
await client.send_message('me', 'Hello!')
# ^ notice this new await
loop = asyncio.get_event_loop()
loop.run_until_complete(coro())
The library can only run the loop until the method completes if the loop
isn't already running, which is why the magic can't work if you run the
loop yourself.
**When you work with updates or events**, the loop needs to be
running one way or another (using `client.run_until_disconnected()
<telethon.client.updates.UpdateMethods.run_until_disconnected>` runs the loop),
so your event handlers must be ``async def``.
.. important::
Turning your event handlers into ``async def`` is the biggest change
between Telethon pre-1.0 and 1.0, but updating will likely cause a
noticeable speed-up in your programs. Keep reading!
So in short, you can use **all** methods in the client with ``await`` or
without it if the loop isn't running:
.. code-block:: python
client.send_message('me', 'Hello!') # works
async def main():
await client.send_message('me', 'Hello!') # also works
loop.run_until_complete(main())
When you work with updates, you should stick using the ``async def main``
way, since your event handlers will be ``async def`` too.
.. note::
There are two exceptions. Both `client.run_until_disconnected()
<telethon.client.updates.UpdateMethods.run_until_disconnected>` and
`client.start() <telethon.client.updates.UpdateMethods.start>` work in
and outside of ``async def`` for convenience without importing the
magic module. The rest of methods remain ``async`` unless you import it.
You can skip the rest if you already know how ``asyncio`` works and you
already understand what the magic does and how it works. Just remember
to ``await`` all your methods if you're inside an ``async def`` or are
using updates and you will be good.
Why asyncio?
@@ -51,8 +160,11 @@ To get started with ``asyncio``, all you need is to setup your main
loop = asyncio.get_event_loop()
loop.run_until_complete(main())
You don't need to ``import telethon.sync`` if you're going to work this
way. This is the best way to work in real programs since the loop won't
be starting and ending all the time, but is a bit more annoying to setup.
Inside ``async def main():``, you can use the ``await`` keyword. Most
Inside ``async def main()``, you can use the ``await`` keyword. Most
methods in the :ref:`TelegramClient <telethon-client>` are ``async def``.
You must ``await`` all ``async def``, also known as a *coroutines*:
@@ -78,9 +190,11 @@ Another way to use ``async def`` is to use ``loop.run_until_complete(f())``,
but the loop must not be running before.
If you want to handle updates (and don't let the script die), you must
`await client.disconnected <telethon.client.telegrambaseclient.TelegramBaseClient.disconnected>`
`await client.run_until_disconnected()
<telethon.client.updates.UpdateMethods.run_until_disconnected>`
which is a property that you can wait on until you call
`await client.disconnect() <telethon.client.telegrambaseclient.TelegramBaseClient.disconnect>`:
`await client.disconnect()
<telethon.client.telegrambaseclient.TelegramBaseClient.disconnect>`:
.. code-block:: python
@@ -93,13 +207,18 @@ which is a property that you can wait on until you call
async def main():
await client.start()
await client.disconnected
await client.run_until_disconnected()
if __name__ == '__main__':
loop = asyncio.get_event_loop()
loop.run_until_complete(main())
This is the same as using the ``run_until_disconnected()`` method:
`client.run_until_disconnected()
<telethon.client.updates.UpdateMethods.run_until_disconnected>` and
`client.start()
<telethon.client.auth.AuthMethods.start>` are special-cased and work
inside or outside ``async def`` for convenience, even without importing
the ``sync`` module, so you can also do this:
.. code-block:: python
@@ -110,8 +229,7 @@ This is the same as using the ``run_until_disconnected()`` method:
print(event)
if __name__ == '__main__':
loop = asyncio.get_event_loop()
loop.run_until_complete(client.start())
client.start()
client.run_until_disconnected()
@@ -172,9 +290,11 @@ a lot less. You can also rename the run method to something shorter:
rc(asyncio.sleep(1))
rc(message.delete())
The documentation will use all these three styles so you can get used
to them. Which one to use is up to you, but generally you should work
inside an ``async def main()`` and just run the loop there.
The documentation generally runs the loop until complete behind the
scenes if you've imported the magic ``sync`` module, but if you haven't,
you need to run the loop yourself. We recommend that you use the
``async def main()`` method to do all your work with ``await``.
It's the easiest and most performant thing to do.
More resources to learn asyncio

View File

@@ -26,15 +26,7 @@ one is very simple:
.. code-block:: python
import asyncio
loop = asyncio.get_event_loop()
# Rename loop.run_until_complete(...) as rc(...), we will use it a lot.
# This basically lets us run the event loop (necessary in asyncio) to
# execute all the requests we need.
rc = loop.run_until_complete
from telethon import TelegramClient
from telethon import TelegramClient, sync
# Use your own values here
api_id = 12345
@@ -62,7 +54,7 @@ your disk. This is by default a database file using Python's ``sqlite3``.
.. code-block:: python
rc(client.start())
client.start()
This is explained after going through the manual process.
@@ -72,14 +64,14 @@ Doing so is very easy:
.. code-block:: python
rc(client.connect()) # Must return True, otherwise, try again
client.connect()
You may or may not be authorized yet. You must be authorized
before you're able to send any request:
.. code-block:: python
rc(client.is_user_authorized()) # Returns True if you can send requests
client.is_user_authorized() # Returns True if you can send requests
If you're not authorized, you need to `.sign_in
<telethon.client.auth.AuthMethods.sign_in>`:
@@ -87,8 +79,8 @@ If you're not authorized, you need to `.sign_in
.. code-block:: python
phone_number = '+34600000000'
rc(client.send_code_request(phone_number))
myself = rc(client.sign_in(phone_number, input('Enter code: ')))
client.send_code_request(phone_number)
myself = client.sign_in(phone_number, input('Enter code: '))
# If .sign_in raises PhoneNumberUnoccupiedError, use .sign_up instead
# If .sign_in raises SessionPasswordNeeded error, call .sign_in(password=...)
# You can import both exceptions from telethon.errors.
@@ -112,13 +104,10 @@ As a full example:
client = TelegramClient('anon', api_id, api_hash)
async def main():
assert await client.connect()
if not await client.is_user_authorized():
await client.send_code_request(phone_number)
me = await client.sign_in(phone_number, input('Enter code: '))
loop.run_until_complete(main())
client.connect()
if not client.is_user_authorized():
client.send_code_request(phone_number)
me = client.sign_in(phone_number, input('Enter code: '))
All of this, however, can be done through a call to `.start()
@@ -127,7 +116,7 @@ All of this, however, can be done through a call to `.start()
.. code-block:: python
client = TelegramClient('anon', api_id, api_hash)
loop.run_until_complete(client.start())
client.start()
The code shown is just what `.start()
@@ -181,12 +170,11 @@ again with a ``password=``:
import getpass
from telethon.errors import SessionPasswordNeededError
async def main():
await client.sign_in(phone)
try:
await client.sign_in(code=input('Enter code: '))
except SessionPasswordNeededError:
await client.sign_in(password=getpass.getpass())
client.sign_in(phone)
try:
client.sign_in(code=input('Enter code: '))
except SessionPasswordNeededError:
client.sign_in(password=getpass.getpass())
The mentioned `.start()
@@ -209,33 +197,32 @@ See the examples below:
from telethon.errors import EmailUnconfirmedError
async def main():
# Sets 2FA password for first time:
await client.edit_2fa(new_password='supersecurepassword')
# Sets 2FA password for first time:
client.edit_2fa(new_password='supersecurepassword')
# Changes password:
await client.edit_2fa(current_password='supersecurepassword',
new_password='changedmymind')
# Changes password:
client.edit_2fa(current_password='supersecurepassword',
new_password='changedmymind')
# Clears current password (i.e. removes 2FA):
await client.edit_2fa(current_password='changedmymind', new_password=None)
# Clears current password (i.e. removes 2FA):
client.edit_2fa(current_password='changedmymind', new_password=None)
# Sets new password with recovery email:
try:
await client.edit_2fa(new_password='memes and dreams',
email='JohnSmith@example.com')
# Raises error (you need to check your email to complete 2FA setup.)
except EmailUnconfirmedError:
# You can put email checking code here if desired.
pass
# Sets new password with recovery email:
try:
client.edit_2fa(new_password='memes and dreams',
email='JohnSmith@example.com')
# Raises error (you need to check your email to complete 2FA setup.)
except EmailUnconfirmedError:
# You can put email checking code here if desired.
pass
# Also take note that unless you remove 2FA or explicitly
# give email parameter again it will keep the last used setting
# Also take note that unless you remove 2FA or explicitly
# give email parameter again it will keep the last used setting
# Set hint after already setting password:
await client.edit_2fa(current_password='memes and dreams',
new_password='memes and dreams',
hint='It keeps you alive')
# Set hint after already setting password:
client.edit_2fa(current_password='memes and dreams',
new_password='memes and dreams',
hint='It keeps you alive')
__ https://github.com/Anorov/PySocks#installation
__ https://github.com/Anorov/PySocks#usage-1

View File

@@ -42,32 +42,31 @@ you're able to just do this:
.. code-block:: python
async def main():
# Dialogs are the "conversations you have open".
# This method returns a list of Dialog, which
# has the .entity attribute and other information.
dialogs = await client.get_dialogs()
# Dialogs are the "conversations you have open".
# This method returns a list of Dialog, which
# has the .entity attribute and other information.
dialogs = client.get_dialogs()
# All of these work and do the same.
lonami = await client.get_entity('lonami')
lonami = await client.get_entity('t.me/lonami')
lonami = await client.get_entity('https://telegram.dog/lonami')
# All of these work and do the same.
lonami = client.get_entity('lonami')
lonami = client.get_entity('t.me/lonami')
lonami = client.get_entity('https://telegram.dog/lonami')
# Other kind of entities.
channel = await client.get_entity('telegram.me/joinchat/AAAAAEkk2WdoDrB4-Q8-gg')
contact = await client.get_entity('+34xxxxxxxxx')
friend = await client.get_entity(friend_id)
# Other kind of entities.
channel = client.get_entity('telegram.me/joinchat/AAAAAEkk2WdoDrB4-Q8-gg')
contact = client.get_entity('+34xxxxxxxxx')
friend = client.get_entity(friend_id)
# Getting entities through their ID (User, Chat or Channel)
entity = await client.get_entity(some_id)
# Getting entities through their ID (User, Chat or Channel)
entity = client.get_entity(some_id)
# You can be more explicit about the type for said ID by wrapping
# it inside a Peer instance. This is recommended but not necessary.
from telethon.tl.types import PeerUser, PeerChat, PeerChannel
# You can be more explicit about the type for said ID by wrapping
# it inside a Peer instance. This is recommended but not necessary.
from telethon.tl.types import PeerUser, PeerChat, PeerChannel
my_user = await client.get_entity(PeerUser(some_id))
my_chat = await client.get_entity(PeerChat(some_id))
my_channel = await client.get_entity(PeerChannel(some_id))
my_user = client.get_entity(PeerUser(some_id))
my_chat = client.get_entity(PeerChat(some_id))
my_channel = client.get_entity(PeerChannel(some_id))
All methods in the :ref:`telegram-client` call `.get_input_entity()
@@ -137,8 +136,7 @@ wherever needed, so you can even do things like:
.. code-block:: python
async def main():
await client(SendMessageRequest('username', 'hello'))
client(SendMessageRequest('username', 'hello'))
The library will call the ``.resolve()`` method of the request, which will
resolve ``'username'`` with the appropriated :tl:`InputPeer`. Don't worry if

View File

@@ -21,18 +21,14 @@ Creating a client
.. code-block:: python
import asyncio
loop = asyncio.get_event_loop()
from telethon import TelegramClient
from telethon import TelegramClient, sync
# 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)
loop.run_until_complete(client.start())
client = TelegramClient('session_name', api_id, api_hash).start()
**More details**: :ref:`creating-a-client`
@@ -42,36 +38,33 @@ Basic Usage
.. code-block:: python
async def main():
# Getting information about yourself
me = await client.get_me()
print(me.stringify())
# Getting information about yourself
me = client.get_me()
print(me.stringify())
# Sending a message (you can use 'me' or 'self' to message yourself)
await client.send_message('username', 'Hello World from Telethon!')
# Sending a message (you can use 'me' or 'self' to message yourself)
client.send_message('username', 'Hello World from Telethon!')
# Sending a file
await client.send_file('username', '/home/myself/Pictures/holidays.jpg')
# Sending a file
client.send_file('username', '/home/myself/Pictures/holidays.jpg')
# Retrieving messages from a chat
from telethon import utils
async for message in client.iter_messages('username', limit=10):
print(utils.get_display_name(message.sender), message.message)
# Retrieving messages from a chat
from telethon import utils
for message in client.iter_messages('username', limit=10):
print(utils.get_display_name(message.sender), message.message)
# Listing all the dialogs (conversations you have open)
async for dialog in client.get_dialogs(limit=10):
print(dialog.name, dialog.draft.text)
# Listing all the dialogs (conversations you have open)
for dialog in client.get_dialogs(limit=10):
print(dialog.name, dialog.draft.text)
# Downloading profile photos (default path is the working directory)
await client.download_profile_photo('username')
# Downloading profile photos (default path is the working directory)
client.download_profile_photo('username')
# Once you have a message with .media (if message.media)
# you can download it using client.download_media(),
# or even using message.download_media():
messages = await client.get_messages('username')
await messages[0].download_media()
loop.run_until_complete(main())
# Once you have a message with .media (if message.media)
# you can download it using client.download_media(),
# or even using message.download_media():
messages = client.get_messages('username')
messages[0].download_media()
**More details**: :ref:`telegram-client`
@@ -86,8 +79,8 @@ Handling Updates
from telethon import events
@client.on(events.NewMessage(incoming=True, pattern='(?i)hi'))
async def handler(event):
await event.reply('Hello!')
def handler(event):
event.reply('Hello!')
client.run_until_disconnected()

View File

@@ -31,19 +31,11 @@ growing!) on the :ref:`TelegramClient <telethon-client>` class that abstract
you from the need of manually importing the requests you need.
For instance, retrieving your own user can be done in a single line
(if we ignore the boilerplate needed to setup ``asyncio``, which only
needs to be done once for your entire program):
(assuming you have ``from telethon import sync`` or ``import telethon.sync``):
.. code-block:: python
import asyncio
async def main():
myself = await client.get_me() # <- a single line!
if __name__ == '__main__':
loop = asyncio.get_event_loop()
loop.run_until_complete(main())
myself = client.get_me()
Internally, this method has sent a request to Telegram, who replied with
the information about your own user, and then the desired information
@@ -55,11 +47,10 @@ how the library refers to either of these:
.. code-block:: python
async def main():
# The method will infer that you've passed an username
# It also accepts phone numbers, and will get the user
# from your contact list.
lonami = await client.get_entity('lonami')
# The method will infer that you've passed an username
# It also accepts phone numbers, and will get the user
# from your contact list.
lonami = client.get_entity('lonami')
The so called "entities" are another important whole concept on its own,
but for now you don't need to worry about it. Simply know that they are
@@ -69,31 +60,30 @@ Many other common methods for quick scripts are also available:
.. code-block:: python
async def main():
# Note that you can use 'me' or 'self' to message yourself
await client.send_message('username', 'Hello World from Telethon!')
# Note that you can use 'me' or 'self' to message yourself
client.send_message('username', 'Hello World from Telethon!')
# .send_message's parse mode defaults to markdown, so you
# can use **bold**, __italics__, [links](https://example.com), `code`,
# and even [mentions](@username)/[mentions](tg://user?id=123456789)
await client.send_message('username', '**Using** __markdown__ `too`!')
# .send_message's parse mode defaults to markdown, so you
# can use **bold**, __italics__, [links](https://example.com), `code`,
# and even [mentions](@username)/[mentions](tg://user?id=123456789)
client.send_message('username', '**Using** __markdown__ `too`!')
await client.send_file('username', '/home/myself/Pictures/holidays.jpg')
client.send_file('username', '/home/myself/Pictures/holidays.jpg')
# The utils package has some goodies, like .get_display_name()
from telethon import utils
async for message in client.iter_messages('username', limit=10):
print(utils.get_display_name(message.sender), message.message)
# The utils package has some goodies, like .get_display_name()
from telethon import utils
for message in client.iter_messages('username', limit=10):
print(utils.get_display_name(message.sender), message.message)
# Dialogs are the conversations you have open
async for dialog in client.get_dialogs(limit=10):
print(dialog.name, dialog.draft.text)
# Dialogs are the conversations you have open
for dialog in client.get_dialogs(limit=10):
print(dialog.name, dialog.draft.text)
# Default path is the working directory
await client.download_profile_photo('username')
# Default path is the working directory
client.download_profile_photo('username')
# Call .disconnect() when you're done
await client.disconnect()
# Call .disconnect() when you're done
client.disconnect()
Remember that you can call ``.stringify()`` to any object Telegram returns
to pretty print it. Calling ``str(result)`` does the same operation, but on

View File

@@ -4,6 +4,16 @@
Working with Updates
====================
.. important::
Make sure you have read at least the first part of :ref:`asyncio-magic`
before working with updates. **This is a big change from Telethon pre-1.0
and 1.0, and your old handlers won't work with this version**.
To port your code to the new version, you should just prefix all your
event handlers with ``async`` and ``await`` everything that makes an
API call, such as replying, deleting messages, etc.
The library comes with the `telethon.events` module. *Events* are an abstraction
over what Telegram calls `updates`__, and are meant to ease simple and common
@@ -36,7 +46,6 @@ Getting Started
.. code-block:: python
import asyncio
from telethon import TelegramClient, events
client = TelegramClient('name', api_id, api_hash)
@@ -46,7 +55,7 @@ Getting Started
if 'hello' in event.raw_text:
await event.reply('hi!')
asyncio.get_event_loop().run_until_complete(client.start())
client.start()
client.run_until_disconnected()
@@ -54,7 +63,6 @@ Not much, but there might be some things unclear. What does this code do?
.. code-block:: python
import asyncio
from telethon import TelegramClient, events
client = TelegramClient('name', api_id, api_hash)
@@ -86,15 +94,21 @@ and ``'hello'`` is in the text of the message, we `.reply()
<telethon.tl.custom.message.Message.reply>` to the event
with a ``'hi!'`` message.
Do you notice anything different? Yes! Event handlers **must** be ``async``
for them to work, and **every method using the network** needs to have an
``await``, otherwise, Python's ``asyncio`` will tell you that you forgot
to do so, so you can easily add it.
.. code-block:: python
asyncio.get_event_loop().run_until_complete(client.start())
client.start()
client.run_until_disconnected()
Finally, this tells the client that we're done with our code. We run the
``asyncio`` loop until the client starts, and then we run it again until
we are disconnected. Of course, you can do other things instead of running
``asyncio`` loop until the client starts (this is done behind the scenes,
since the method is so common), and then we run it again until we are
disconnected. Of course, you can do other things instead of running
until disconnected. For this refer to :ref:`update-modes`.