Decorator to async function python - python-asyncio

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

Related

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

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

Different behaviour of asyncio.Task.all_tasks()

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

infinite loop cannot be connected websocket server

A client connect websocket and calls tail_log method, and new client can't connect
How to solve this problem
def on_message(self, message):
def tail_log(user,ip,port,cmd,log_path,url):
cmd = "/usr/bin/ssh -p {port} {user}#{ipaddr} {command} {logpath}" \
.format(user=user, ipaddr=ip, port=port, command=cmd, logpath=log_path)
f = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True)
while True:
line = f.stdout.readline().strip()
if line == '':
self.write_message('failed')
break
self.write_message(line)
tail_log(user=SSH_USER,ip=IP_ADDR,cmd=CMD,port=SSH_PORT,log_path=LOG_PATH,url=SOCKET_URL)
Your infinite loop must yield control back to Tornado's event loop, either by executing a yield, await, or by returning from the tail_log function. Since your infinite loop does not yield control to the event loop, the event loop can never process any more events, including new websocket connections.
Try using Tornado's own process module to read from your subprocess's stdout asynchronously. Something like this:
import tornado.ioloop
import tornado.process
import tornado.web
import tornado.websocket
class TailHandler(tornado.websocket.WebSocketHandler):
def open(self):
self.write_message(u"Tailing....")
self.p = tornado.process.Subprocess(
"tail -f log.log",
stdout=tornado.process.Subprocess.STREAM,
stderr=tornado.process.Subprocess.STREAM,
shell=True)
tornado.ioloop.IOLoop.current().add_callback(
lambda: self.tail(self.p.stdout))
tornado.ioloop.IOLoop.current().add_callback(
lambda: self.tail(self.p.stderr))
self.p.set_exit_callback(self.close)
async def tail(self, stream):
try:
while True:
line = await stream.read_until(b'\n')
if line:
self.write_message(line.decode('utf-8'))
else:
# "tail" exited.
return
except tornado.iostream.StreamClosedError:
# Subprocess killed.
pass
finally:
self.close()
def on_close(self):
# Client disconnected, kill the subprocess.
self.p.proc.kill()
class MainHandler(tornado.web.RequestHandler):
def get(self):
self.write("""<html><head><script>
var ws = new WebSocket("ws://localhost:8888/tail");
ws.onmessage = function (evt) {
document.write('<p>' + evt.data + '</p>');
};</script></head></html>""")
def make_app():
return tornado.web.Application([
(r"/", MainHandler),
(r"/tail", TailHandler),
])
app = make_app()
app.listen(8888)
tornado.ioloop.IOLoop.current().start()
If you're not on Python 3.5 yet, substitute #gen.coroutine for "async def", substitute "yield" for "await", and substitute "break" for "return".

Resources