Using Async¶
It’s easy to do async wrong. Check out some of bottom’s past issues for an
example of how easy it is to use the async
and await
constructs
incorrectly.
To simplify things, bottom lets us to pass both synchronous and
async functions as callbacks. Both of these are valid handlers for the
privmsg
event:
@client.on('privmsg')
def synchronous_handler(**kwargs):
print("Synchronous call")
@client.on('privmsg')
async def async_handler(**kwargs):
await asyncio.sleep(1, loop=client.loop)
print("Async call")
Event Loop Gotchas¶
If none is provided, Client
will use the default event loop. This is fine
if we’re only running the client by itself, but it’s recommended to still
parameterize any async calls that have a loop parameter.
Here’s an easy way to hang the client forever:
import asyncio
import bottom
client = bottom.Client(
host='localhost', port=6697, ssl=True,
loop=asyncio.new_event_loop())
@client.on('client_connect')
async def handle(**kwargs):
print("Before await")
await asyncio.sleep(1)
print("After await")
client.loop.run_until_complete(client.connect())
client.loop.run_forever()
See the bug? Try running it.
In the second line of handle
, asyncio.sleep
takes an optional
loop kwarg, which is the event loop to run on. This defaults to
asyncio.get_event_loop
. However, we’re running the client on
client.loop
. Since the default loop never runs, the code will
wait forever.
Here’s the correct handle:
@client.on('client_connect')
async def handle(**kwargs):
print("Before await")
await asyncio.sleep(1, loop=client.loop)
print("After await")
Connect/Disconnect¶
Client connect and disconnect are coroutines so that we can easily wait for their completion before performing more actions in a handler. However, we don’t always want to wait for the action to complete. How can we do both?
Let’s say that on disconnect we want to reconnect, then notify the room that
we’re back. We need to await
for the connection before sending anything:
@client.on('client_disconnect')
async def reconnect(**kwargs):
# Wait a second so we don't flood
await asyncio.sleep(2, loop=client.loop)
# Wait until we've reconnected
await client.connect()
# Notify the room
client.send('privmsg', target='#bottom-dev',
message="I'm baaack!")
What about a handler that doesn’t need an established connection to finish? Instead of notifying the room, let’s log the reconnect time and return:
import arrow
import logging
logger = logging.getLogger(__name__)
@client.on('client_disconnect')
async def reconnect(**kwargs):
# Wait a second so we don't flood
await asyncio.sleep(2, loop=client.loop)
# Schedule a connection when the loop's next available
client.loop.create_task(client.connect())
# Record the time of the disconnect event
now = arrow.now()
logger.info("Reconnect started at " + now.isoformat())
We can also wait for the client_connect
event to trigger, which is slightly
different than waiting for client.connect to complete:
@client.on('client_disconnect')
async def reconnect(**kwargs):
# Wait a second so we don't flood
await asyncio.sleep(2, loop=client.loop)
# Schedule a connection when the loop's next available
client.loop.create_task(client.connect())
# Wait until client_connect has triggered
await client.wait("client_connect")
# Notify the room
client.send('privmsg', target='#bottom-dev',
message="I'm baaack!")
Existing Event Loop¶
We can specify an event loop that the client will run on:
client = bottom.Client(..., loop=my_existing_event_loop)
Debugging¶
You can get more asyncio debugging info by setting up an event loop with debugging enabled,
and pass that loop to bottom.Client
:
import asyncio
loop = asyncio.get_event_loop()
loop.set_debug(True)
bot = bottom.Client(..., loop=loop)