38

I'm trying to create a WebSocket command line client that waits for messages from a WebSocket server but waits for user input at the same time.

Regularly polling multiple online sources every second works fine on the server, (the one running at localhost:6789 in this example), but instead of using Python's normal sleep() method, it uses asyncio.sleep(), which makes sense because sleeping and asynchronously sleeping aren't the same thing, at least not under the hood.

Similarly, waiting for user input and asynchronously waiting for user input aren't the same thing, but I can't figure out how to asynchronously wait for user input in the same way that I can asynchronously wait for an arbitrary amount of seconds, so that the client can deal with incoming messages from the WebSocket server while simultaneously waiting for user input.

The comment below in the else-clause of monitor_cmd() hopefully explains what I'm getting at:

import asyncio
import json
import websockets

async def monitor_ws():
    uri = 'ws://localhost:6789'
    async with websockets.connect(uri) as websocket:
        async for message in websocket:
            print(json.dumps(json.loads(message), indent=2, sort_keys=True))

async def monitor_cmd():
    while True:

        sleep_instead = False

        if sleep_instead:
            await asyncio.sleep(1)
            print('Sleeping works fine.')
        else:
            # Seems like I need the equivalent of:
            # line = await asyncio.input('Is this your line? ')
            line = input('Is this your line? ')
            print(line)
try:
    asyncio.get_event_loop().run_until_complete(asyncio.wait([
        monitor_ws(),
        monitor_cmd()
    ]))
except KeyboardInterrupt:
    quit()

This code just waits for input indefinitely and does nothing else in the meantime, and I understand why. What I don't understand, is how to fix it. :)

Of course, if I'm thinking about this problem in the wrong way, I'd be very happy to learn how to remedy that as well.

2
  • 1
    aioconsole may be what you need here. Commented Oct 18, 2019 at 19:13
  • @user4815162342: Yes! That was exactly what I needed, actually! :) The docs aren't too great, but having imported aioconsole, this was exactly the line that worked: line = await aioconsole.ainput('Is this your line? ') If you put that line in as an answer, I'll mark it as the correct one. Commented Oct 18, 2019 at 21:21

6 Answers 6

35

You can use the aioconsole third-party package to interact with stdin in an asyncio-friendly manner:

line = await aioconsole.ainput('Is this your line? ')
Sign up to request clarification or add additional context in comments.

2 Comments

aioconsole maintener here, I updated the aioconsole readme to recommend the use of prompt_toolkit.PromptSession.prompt_async instead.
@Vincent Thanks for the update. Please feel free to edit the answer to change it to a formulation you're comfortable with.
31

Borrowing heavily from aioconsole, if you would rather avoid using an external library you could define your own async input function:

async def ainput(string: str) -> str:
    await asyncio.get_event_loop().run_in_executor(
            None, lambda s=string: sys.stdout.write(s+' '))
    return await asyncio.get_event_loop().run_in_executor(
            None, sys.stdin.readline)

Comments

8

Python 3.9 introduces asyncio.to_thread, which can be used to simplify the code in mfurseman's answer:

async def ainput(string: str) -> str:
    await asyncio.to_thread(sys.stdout.write, f'{string} ')
    return await asyncio.to_thread(sys.stdin.readline)

Note that sys.stdin.readline returns the newline character '\n', while input does not. If you would like ainput to exclude the newline character, I suggest the following alteration:

async def ainput(string: str) -> str:
    await asyncio.to_thread(sys.stdout.write, f'{string} ')
    return (await asyncio.to_thread(sys.stdin.readline)).rstrip('\n')

1 Comment

As-is, this seemed to be buffering, and I found I had to manually flush the stream to replicate standard input behavior by calling sys.stdout.flush() in addition to write (I used a small helper to do both in one go instead of spawning a thread for each).
6

Most pragmatic way is to use the input function via asyncio.to_thread:

import asyncio

name = await asyncio.to_thread(input, "Your name:")

or wrapped in a function:

import asyncio

async def ainput(prompt: str) -> str:
    return await asyncio.to_thread(input, f'{prompt} ')

name = await ainput("Your name:")

1 Comment

See github.com/vxgmichel/aioconsole/issues/128 for a comparison to aioconsole.ainput.
4

Borrowing heavily from aioconsole, there are 2 ways to handle.

  1. start a new daemon thread:
import sys
import asyncio
import threading
from concurrent.futures import Future


async def run_as_daemon(func, *args):
    future = Future()
    future.set_running_or_notify_cancel()

    def daemon():
        try:
            result = func(*args)
        except Exception as e:
            future.set_exception(e)
        else:
            future.set_result(result)

    threading.Thread(target=daemon, daemon=True).start()
    return await asyncio.wrap_future(future)


async def main():
    data = await run_as_daemon(sys.stdin.readline)
    print(data)


if __name__ == "__main__":
    asyncio.run(main())

  1. use stream reader:
import sys
import asyncio


async def get_steam_reader(pipe) -> asyncio.StreamReader:
    loop = asyncio.get_event_loop()
    reader = asyncio.StreamReader(loop=loop)
    protocol = asyncio.StreamReaderProtocol(reader)
    await loop.connect_read_pipe(lambda: protocol, pipe)
    return reader


async def main():
    reader = await get_steam_reader(sys.stdin)
    data = await reader.readline()
    print(data)


if __name__ == "__main__":
    asyncio.run(main())

Comments

2
  1. solution with prompt-toolkit:

    from prompt_toolkit import PromptSession
    from prompt_toolkit.patch_stdout import patch_stdout
    
    async def my_coroutine():
        session = PromptSession()
        while True:
            with patch_stdout():
                result = await session.prompt_async('Say something: ')
            print('You said: %s' % result)
    
  2. solution with asyncio:

    import asyncio
    import sys
    
    async def get_stdin_reader() -> asyncio.StreamReader:
        stream_reader = asyncio.StreamReader()
        protocol = asyncio.StreamReaderProtocol(stream_reader)
        loop = asyncio.get_running_loop()
        await loop.connect_read_pipe(lambda: protocol, sys.stdin)
        return stream_reader
    
    async def main():
        stdin_reader = await get_stdin_reader()
        while True:
            print('input: ', end='', flush=True)
            line = await stdin_reader.readline()
            print(f'your input: {line.decode()}')
    
    asyncio.run(main())
    

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.