Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ainput won't work correctly after another ainput call has been cancelled #113

Open
samskiter opened this issue Dec 4, 2023 · 8 comments

Comments

@samskiter
Copy link

Similar to #104

I have a task that waits for user input but I'd like to be able to cancel that task - unfortunately is seems that I can't do that when using ainput - the cancellation is simply ignored and the program continues to wait for user input.

Or am I missing something?

@vxgmichel
Copy link
Owner

Hi @samskiter and thanks for the report :)

I just tried the following example on linux with python 3.11 and it seems to work fine:

import asyncio
import aioconsole

async def main():
    try:
        async with asyncio.timeout(3):
            print(await aioconsole.ainput())
    except TimeoutError:
        print("Timeout!")

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

Could provide more information about your code, OS and python version?

@disketten
Copy link

I see this bug on Windows.

Try this:

import asyncio
import aioconsole


async def main():
    try:
        async with asyncio.timeout(3):
            print(await aioconsole.ainput())
    except TimeoutError:
        print("Timeout!")
        print(await aioconsole.ainput("Double 'Enter' needed on Windows: "))


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

This is on Python 3.12.7 on Windows 10 and 11.

@relsqui
Copy link

relsqui commented Jan 31, 2025

I'm also having this problem. I have ainput in a loop as one of several coroutines interacting with a subprocess as part of a task group. (We're intercepting user input here so we can also insert data from elsewhere into the same queue for the server to handle.)

# async def server_listener ...
# async def server_input ...

async def user_listener(subproc):
  while subproc.returncode is None:
    userInput = await ainput()
    await inputQueue.put(userInput)

async def main():
  subproc = await asyncio.create_subprocess_exec('server')
  async with asyncio.TaskGroup() as tg:
    tg.create_task(server_listener(subproc))
    tg.create_task(server_input(subproc))
    tg.create_task(user_listener(subproc))

This mostly works, but when the subprocess ends, user_listener waits for me to hit enter before the whole task group can finish. The timeout version doesn't work for me because I don't want to interrupt the user every N seconds while the subprocess is still running. It's not a huge deal to have to hit enter to end the program, but is there any way to avoid needing to?

@disketten
Copy link

For those that are looking for a working solution, the "prompt-toolkit" library has a working solution.

@vxgmichel vxgmichel changed the title ainput can't be cancelled ainput won't work correctly after another ainput call has been cancelled Jan 31, 2025
@vxgmichel
Copy link
Owner

@disketten
I see, thanks for the report. The problem here is that the ainput call on windows runs the sys.stdin methods in a daemon thread, and wait for the result using asyncio.wrap_future. So when ainput is cancelled, the waiting for the future is cancelled but not the daemon task. So if you perform another ainput() after that, the first deamon thread would still need to complete first, so it will consume the first line being entered. That means it's the second line entered that will be returned by the second ainput. Unfortunately, this is quite hard to fix without using the proper windows API like prompt-toolkit does. Still, I updated the issue title accordingly.

@vxgmichel
Copy link
Owner

@relsqui

It's not a huge deal to have to hit enter to end the program, but is there any way to avoid needing to?

If I understand correctly, you should be able to simply cancel the user_listener task when the subprocess ends. Consider this working example which I think does what you're looking for:

import asyncio
import aioconsole


async def user_listener():
    while True:
        print(await aioconsole.ainput("prompt> "))


async def do_something():
    await asyncio.sleep(3)
    print("\nDONE")


async def main():
    async with asyncio.TaskGroup() as tg:
        user_listener_task = tg.create_task(user_listener())
        other_task = tg.create_task(do_something())
        await other_task
        user_listener_task.cancel()


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

@vxgmichel
Copy link
Owner

@disketten

For those that are looking for a working solution, the "prompt-toolkit" library has a working solution.

You're absolutely right. I already have a limitations section in the project readme but it only addresses the need for an async-compatible python console. I'll add another section about the need for proper windows support.

For some background, aioconsole is 8 years old and started back at a time when other projects like prompt-toolkit did not have async support because asyncio was still very new and under development.

@relsqui
Copy link

relsqui commented Jan 31, 2025

Thanks @vxgmichel! I needed to adapt that a little for my use case (subproc.wait() instead of await other_task) but I think my fundamental problem here was a misunderstanding of how task groups work -- I thought a CancelledError was being sent to all the tasks in the group when I cancel one, and that the ainput was ignoring it, but it just wasn't being sent. (In retrospect I can't figure out why I thought that, apart from "reading documentation while tired.") My bad, appreciate the help.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants