Skip to content

Switch to using anyio for async #1291

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

Closed
wants to merge 2 commits into from
Closed

Conversation

sirg3
Copy link

@sirg3 sirg3 commented Nov 26, 2022

This is a draft that switches from asyncio to anyio, which is necessary for
trio compatibility. I've tested it with a version of calculator.py that
runs under Trio:

import anyio

async def main():
    await CalculatorApp().run_async()

if __name__ == "__main__":
    anyio.run(main, backend="trio")

Here's a short list of design decisions and changes:

  • There is a single task group that lives as long as the app is running and
    all background tasks (e.g. the driver, timers, and the message pump) run in
    it. The app will not finish running until all of these tasks stop, which is
    part of the shutdown process. Any hangs in exiting an app are likely due to
    a task that is still living, indicating an issue in the shutdown process.
  • The task group must be exited on the same task as it was entered, which
    makes App.run_test unable to be used in a pytest fixture.
  • anyio does not know which backend to use when creating objects outside of an
    async context. This breaks the common idiom of MyApp().run() being called
    outside of an async context. To work around this, we force anyio to believe
    it's running under asyncio if there is no async context. This does
    introduce the possibility for a mismatch and App.run_async checks for this.
    Trio users should create the app inside of the Trio context and perhaps the
    Textual can be reworked a bit to make this better (e.g. lazily creating locks
    and other anyio objects once we know what backend we're running under).
  • The message pump switched to using a memory send / receive stream. This is
    analagous to asyncio's queue class but has a built-in notion of whether
    the stream is closed or not.
  • Reworked message pump state handling, which turned out to be somewhat tricky.
    This was necessary because send / receive streams can't be used once they're
    closed, but message pumps can be re-used multiple times. It also addresses some
    potential race conditions between starting the pump and waiting for it to close.
  • The devtools are only compatible with the asyncio backend because
    aiohttp requires asyncio.
  • Removed references to uvloop with the thought being that users can
    explicitly opt into that. An alternate approach would be to do that in the
    _spoof_asyncio_if_needed when we default to asyncio.
  • anyio.Event lacks clear(), so a new class, Flag, was created to provide
    that functionality.
  • The Timer implementation got a bit messy due to a race condition between
    start enqueueing the task, stop getting called, and the task actually
    executing. Making Timer.start async would allow this to be implemented
    more cleanly but is an API breakage.
  • Timer.start no longer returns a Task object.
  • All the tests continue to run under asyncio.

Almost all tests pass, with some exceptions:

  • test_screens: Has an error because the app isn't actually running, but
    _remove_nodes needs to schedule a task to run in its task group. Might
    need to restructure this test to use App.run_test.
  • test_animate_height: Fails with an assertion about the height being 0. This
    stems from Screen.find_widget being unable to find the widget. Adding
    await anyio.wait_all_tasks_blocked() right after the async with makes the
    test pass, which makes me think there's some async race conditions going
    on.

Outstanding tasks:

  • Fix the above test issues.
  • The Win32 driver was not updated, but should be relatively straightforward to
    do based off of the Linux driver.
  • Updated documentation
  • Updated CHANGELOG.md (where appropriate)

@rodrigogiraoserrao
Copy link
Contributor

rodrigogiraoserrao commented Dec 3, 2022

Hey there!
This is going to sound like a dumb request, but could you write a couple of paragraphs explaining what this PR (draft) is for? I am fairly new to Textual and I don't think I understand what you are trying to do.
I mean, I can read “This is a draft that switches from asyncio to anyio, which is necessary for trio compatibility.”, but I don't understand why you think Textual should have trio compatibility.

Thanks a lot for being understanding :)

@willmcgugan
Copy link
Member

@rodrigogiraoserrao My understanding is that AnyIO is an abstraction over asyncio and other async libs, which would otherwise be mutually incompatible. It means that we could mix components built for these other libs.

@sirg3 Very interested in this. I was planning to look at it myself at some point. Let me know if you have any questions.

@sirg3
Copy link
Author

sirg3 commented Dec 10, 2022

Hey there! This is going to sound like a dumb request, but could you write a couple of paragraphs explaining what this PR (draft) is for? I am fairly new to Textual and I don't think I understand what you are trying to do. I mean, I can read “This is a draft that switches from asyncio to anyio, which is necessary for trio compatibility.”, but I don't understand why you think Textual should have trio compatibility.

Thanks a lot for being understanding :)

@rodrigogiraoserrao There are multiple different async libraries for Python, though the big two are asyncio (in the Python standard library) and trio. I want to use Textual in a program that is already using trio but that’s currently not possible because Textual uses asyncio directly.

@sirg3 Very interested in this. I was planning to look at it myself at some point. Let me know if you have any questions.

@willmcgugan Thanks for the interest. I’m unlikely to be able to get back to this until Christmas, but I would like your thoughts on the MR as-is, along with:

  • To what degree is API stability required? There are a few cases where making async a bit more pervasive could solve some race conditions.
  • Why is there an is-a relationship between DOM nodes and message pumps instead of a has-a relationship? I think composition instead of inheritance would render the message pump class easier to reason about.
  • Do you have any thoughts on the two test failures I mentioned in the MR description?

@sirg3
Copy link
Author

sirg3 commented Dec 23, 2022

@willmcgugan Ping?

@willmcgugan
Copy link
Member

To what degree is API stability required? There are a few cases where making async a bit more pervasive could solve some race conditions.

Textual is zero-ver, so API is subject to change. However, I'd prefer to keep most API calls async agnostic.

Why is there an is-a relationship between DOM nodes and message pumps instead of a has-a relationship? I think composition instead of inheritance would render the message pump class easier to reason about.

I suspect it is too tightly coupled to Textual to make it an independent concern.

Do you have any thoughts on the two test failures I mentioned in the MR description?

Afraid not. The project has moved on quite a bit. Not sure if these are relevant.

@sirg3
Copy link
Author

sirg3 commented Jun 11, 2023

Unfortunately I cannot keep up with the pace of Textual development and won’t be able to get back to this MR in the foreseeable future. Hopefully all of my notes will be helpful in case anyone else tries this.

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

Successfully merging this pull request may close these issues.

3 participants