Different behaviour of asyncio.Task.all_tasks() - python-asyncio

First example:
import asyncio
async def req():
print('request')
await asyncio.sleep(1)
async def run():
print(len(asyncio.Task.all_tasks()))
asyncio.ensure_future(req())
print(len(asyncio.Task.all_tasks()))
await asyncio.sleep(2)
print(len(asyncio.Task.all_tasks()))
loop = asyncio.get_event_loop()
loop.run_until_complete(run())
The result is:
1
2
request
1
Second example:
import asyncio
async def req():
print('request')
await asyncio.sleep(1)
async def run():
print(len(asyncio.Task.all_tasks()))
t = asyncio.ensure_future(req())
print(len(asyncio.Task.all_tasks()))
await t
print(len(asyncio.Task.all_tasks()))
loop = asyncio.get_event_loop()
loop.run_until_complete(run())
The result is:
1
2
request
2
So, why in first example the last call asyncio.Task.all_tasks() return 1 and in second example it's return 2?
In other words, why in first example the task, that wrap req() was deleted from a set of all tasks for an event loop, and why it is not true for the second example.

The task is removed from all_tasks() when it is destroyed.
Add a del statement:
[...]
await t
del t
print(len(asyncio.Task.all_tasks()))
and it will yield:
1
2
request
1

Related

Decorator to async function python

A simple decorate to calculate time a function takes to run:
import time
def decor(f):
starttime=time.time()
f()
print("child functoin run time is ", (time.time()-starttime)*1000, "ms")
return f
try to use it to decorate async functions:
async def sleep_and_print(seconds):
print(f"starting async {seconds} sleep 😴")
await asyncio.sleep(seconds)
print(f"finished async {seconds} sleep ⏰")
return seconds
#decor
async def main():
# using arguments
results = await asyncio.gather(sleep_and_print(3), sleep_and_print(6))
print(results)
asyncio.run(main())
I got RuntimeWarning: coroutine 'main' was never awaited error
If I change the decorator function to async and await, eg
async def decor(f):
starttime=time.time()
await f()
print("child functoin run time is ", (time.time()-starttime)*1000, "ms")
return f
asyncio.run(main()) failed with coroutine' object is not callable
Why main() becomes uncallable? Any suggestion on the work around?
Try to return async function from the decorator function:
import time
import asyncio
def decor(f):
async def _fn():
starttime = time.time()
await f()
print(
"child function run time is ",
(time.time() - starttime) * 1000,
"ms",
)
return _fn
async def sleep_and_print(seconds):
print(f"starting async {seconds} sleep 😴")
await asyncio.sleep(seconds)
print(f"finished async {seconds} sleep ⏰")
return seconds
#decor
async def main():
# using arguments
results = await asyncio.gather(sleep_and_print(3), sleep_and_print(6))
print(results)
asyncio.run(main())
Prints:
starting async 3 sleep 😴
starting async 6 sleep 😴
finished async 3 sleep ⏰
finished async 6 sleep ⏰
[3, 6]
child function run time is 6002.1820068359375 ms

Create an async generator "from scratch"

Most of the time I am using asyncio API. But how would I create an async generator "from scratch"?
Say I have a classic generator, and I want to make it async. How can I do that?
I naively thought that I could do something like below, but it does not run asynchronously (the 3 "for loops" are running one after the other, instead of concurrently). My hope was to make some_loop() asynchronous by calling it from some_async_loop() and releasing the event loop after each iteration with asyncio.sleep(0):
#!/usr/bin/env python3
import asyncio
async def run():
task = asyncio.ensure_future(run_async_loop())
asyncio.ensure_future(run_async_loop())
asyncio.ensure_future(run_async_loop())
await task
async def run_async_loop():
async for i in some_async_loop():
print(i)
async def some_async_loop():
for i in some_loop():
yield i
asyncio.sleep(0)
def some_loop():
for i in range(10):
yield i
if __name__ == "__main__":
loop = asyncio.get_event_loop()
loop.run_until_complete(run())
I'm glad the fixing the call to asyncio.sleep() got things running.
I'm concerned partly about your run() function, because you're only await-ing on the first task, which means your code could exit before the other tasks are complete. I would suggest this:
async def run():
tasks = [run_async_loop(), run_async_loop(), run_async_loop()]
await asyncio.gather(*tasks)
I think you can also simplify your __main__ block:
if __name__ == '__main__':
asyncio.run(run())

how to accomplish concurrency when using asyncio.create_task

It can accomplish concurrency with asyncio in the form below :
async def say_after(delay, what):
print(f'{what} {delay}')
await asyncio.sleep(delay)
print(f'over {what} {delay}')
async def main():
task1 = asyncio.create_task(say_after(1, "hello"))
task2 = asyncio.create_task(say_after(2, "world"))
print(f"started at {time.strftime('%X')}")
await task1
await task2
print(f"finished at {time.strftime('%X')}")
loop = asyncio.get_event_loop()
loop.run_until_complete(main())
started at 10:18:54
hello 1
world 2
over hello 1
over world 2
finished at 10:18:56
But when I change the form of function 'main' into :
async def main():
print(f"started at {time.strftime('%X')}")
task1 = asyncio.create_task(say_after(1, "hello"))
await task1
task2 = asyncio.create_task(say_after(2, "world"))
await task2
print(f"finished at {time.strftime('%X')}")
It perfoms kind of sequential execution.
started at 10:18:33
hello 1
over hello 1
world 2
over world 2
finished at 10:18:36
So why dose the latter not perform in concurrency?

ValueError when running simple asyncio script

I'm learning about asycio and trying to run this script I get this error:
ValueError: a coroutine was expected, got <async_generator object
mygen at 0x7fa2af959a60>
What am I missing here?
import asyncio
async def mygen(u=10):
"""Yield powers of 2."""
i = 0
while i < int(u):
yield 2 ** i
i += 1
await asyncio.sleep(0.1)
asyncio.run(mygen(5))
The asyncio.run function expects a coroutine but mygen is an asynchronous generator.
You can try something like this:
test.py:
import asyncio
async def mygen(u=10):
"""Yield powers of 2."""
i = 0
while i < int(u):
yield 2**i
i += 1
await asyncio.sleep(0.1)
async def main():
async for i in mygen(5):
print(i)
if __name__ == "__main__":
asyncio.run(main())
Test:
$ python test.py
1
2
4
8
16
References:
What does the "yield" keyword do?
PEP 525 -- Asynchronous Generators

Discord.py - Music bot queue command

I’m trying to code a simple music bot for discord in python. I tried coding a queue command so that I can queue a certain song (searching for songs and playing them etc. all work). This is my code for the queue command:
#commands.command()
async def checkqueue(self, ctx):
if len(self.song_queue[ctx.guild.id]) == 0:
return await ctx.send("Queue is empty")
else:
embed = discord.Embed(title="Song Queue", description="", colour=discord.colour.dark_gold())
i = 1
for url in self.song_queue[ctx.guild.id]:
embed.description += f"{i}) {url}\n"
i += 1
await ctx.send(embed=embed)
When the queue is empty, the bot sends a message in discord saying "Queue is empty”. When I use the play command, it adds a song to the queue. However, when I try to use the checkqueue command when there’s at least one song in the queue, the bot doesn’t send the embed. Not sure if this is a problem with the code in the queue command or outside the queue command, so here’s the cut-down version of the rest of my code in my commands Cog.
import discord
from discord.ext import commands
import youtube_dl
import pafy
import asyncio
class Commands(commands.Cog):
def __init__(self, client):
self.client = client
self.song_queue = {}
#commands.Cog.listener()
async def on_ready(self):
for guild in self.client.guilds:
self.song_queue[guild.id] = []
async def check_queue(self, ctx):
if len(self.song_queue[ctx.guild.id]) > 0:
ctx.voice_client.stop()
await self.play_song(ctx, self.song_queue[ctx.guild.id][0])
self.song_queue[ctx.guild.id].pop(0)
async def search_song(self, amount, song, get_url=False):
info = await self.client.loop.run_in_executor(None, lambda: youtube_dl.YoutubeDL({"format": "bestaudio", "quiet" : True}).extract_info(f"ytsearch{amount}:{song}", download=False, ie_key="YoutubeSearch"))
if len(info["entries"]) == 0:
return None
return [entry["webpage_url"] for entry in info["entries"]] if get_url else info
async def play_song(self, ctx, song):
url = pafy.new(song).getbestaudio().url
ctx.voice_client.play(discord.PCMVolumeTransformer(discord.FFmpegPCMAudio(url)), after=lambda error: self.client.loop.create_task(self.check_queue(ctx)))
ctx.voice_client.source.volume = 0.5
#commands.command()
async def stop(self, ctx):
if ctx.voice_client is not None:
return await ctx.voice_client.disconnect()
return await ctx.send("Disconnected")
else:
return await ctx.send("I am not connected to a voice channel")
#commands.command()
async def join(self, ctx):
if ctx.author.voice is None:
return await ctx.send("You are not connected to a voice channel")
else:
channel = ctx.author.voice.channel
await channel.connect()
await ctx.send(f"Connected to voice channel: '{channel}'")
#commands.command()
async def play(self, ctx, *, song=None):
if song is None:
return await ctx.send("You must include a song to play.")
if ctx.voice_client is None:
return await ctx.send("I must be in a voice channel to play a song.")
if not ("youtube.com/watch?" in song or "https://youtu.be/" in song):
await ctx.send("Searching for a song, this may take a few seconds...")
result = await self.search_song(1, song, get_url=True)
if result is None:
return await ctx.send("Sorry, I couldn't find the song you asked for. Try using my search command to find the song you want.")
song = result[0]
if ctx.voice_client.source is not None:
queue_len = len(self.song_queue[ctx.guild.id])
if queue_len < 10:
self.song_queue[ctx.guild.id].append(song)
return await ctx.send(f"Song added to the queue at position {queue_len+1}")
else:
return await ctx.send("Maximum queue limit has been reached, please wait for the current song to end to add more songs to the queue")
await self.play_song(ctx, song)
await ctx.send(f"Now playing: {song}")
#commands.command()
async def search(self, ctx, *, song=None):
if song is None:
return await ctx.send("Please include a song to search for")
await ctx.send("Searching for song, this may take a few seconds...")
info = await self.search_song(5, song)
embed = discord.Embed(title=f"Results for '{song}':", description="You can use these URL's to play the song\n", colour=discord.Colour.blue())
amount = 0
for entry in info["entries"]:
embed.description += f"[{entry['title']}]({entry['webpage_url']})\n"
amount += 1
embed.set_footer(text=f"Displaying the first {amount} results.")
await ctx.send(embed=embed)
#commands.command()
async def checkqueue(self, ctx):
if len(self.song_queue[ctx.guild.id]) == 0:
return await ctx.send("Queue is empty")
else:
embed = discord.Embed(title="Song Queue", description="", colour=discord.colour.blue())
i = 1
for url in self.song_queue[ctx.guild.id]:
embed.description += f"{i}) {url}\n"
i += 1
await ctx.send(embed=embed)
#commands.command()
async def queue(self, ctx, *, song=None):
if song is None:
return await ctx.send("You must include a song to queue.")
if not ("youtube.com/watch?" in song or "https://youtu.be/" in song):
await ctx.send("Searching for a song, this may take a few seconds...")
result = await self.search_song(1, song, get_url=True)
if result is None:
return await ctx.send("Sorry, I couldn't find the song you asked for. Try using my search command to find the song you want.")
song = result[0]
if ctx.voice_client.source is not None:
queue_len = len(self.song_queue[ctx.guild.id])
if queue_len < 10:
self.song_queue[ctx.guild.id].append(song)
return await ctx.send(f"Song added to the queue at position {queue_len+1}")
else:
return await ctx.send("Maximum queue limit has been reached, please wait for the current song to end to add more songs to the queue")
The issue that I was having with your code was the color of the Embed. Everything else went smoothly when I ran your code. When you referenced the Colour, you didn't properly access the function.
What you did:
embed = discord.Embed(title="Song Queue",
description="",
colour=discord.colour.dark_gold())
What you should do:
embed = discord.Embed(title="Song Queue",
description="",
colour=discord.Colour.dark_gold())

Resources