Skip to content

Add an @trio.not_in_async_task decorator, to error instead of stalling #2757

Closed

Description

@Zac-HD

In a large codebase, you're going to have some functions which are slow, and so you don't want them to be called from an async task (without using trio.to_thread.run_sync(slow_fn), anyway). flake8-trio's TRIO200 error can help find direct calls, but indirect calls are really hard to find via static analysis.

As I mentioned in my PyCon talk, and work we've implemented a decorator to mark sync functions which can be called in a sync context, but must be delegated to a worker thread instead of being called in an async context - no matter how many sync functions are in the intermediate call chain. Simple implementation:

def not_in_async_task(fn):
    """Decorator for sync functions which should not be called in an async context.

    <detailed docstring here>
    """
    @functools.wraps(fn)
    def decorator(*args, **kwargs):
        try:
            # Identical logic to trio.lowlevel.current_task()
            GLOBAL_RUN_CONTEXT.task
        except AttributeError:
            pass
        else:
            raise RuntimeError(
                f"{fn.__name__} should not be called in an async context.  "
                f"Use an async alternative, or trio.to_thread.run_sync({fn.__name__})."
            )
        return fn(*args, *kwargs)

    return decorator

Should we include this in trio itself? I've certainly found the pattern useful, and upstreaming it would encourage wider use of the pattern. On the other hand it's not entirely clear where this would fit, and it's neither hard to implement nor something which benefits much from standardization.

Activity

  1. oremanj commented on Aug 17, 2023

    @oremanj
    Member

    Perhaps the more fundamental primitive here is trio.in_trio_context(), which returns a bool. You can write it yourself using only public APIs, but it's weird:

    def in_trio_context() -> bool:
        try:
            trio.current_time()
        except RuntimeError:
            return False
        return True
    

    The private implementation could check for the existence of GLOBAL_RUN_CONTEXT.runner.

    (There is a subtle difference between checking for runner and checking for task: the former exists even in Trio context outside of a task, which you can observe in abort_fns and instrument calls. I think the runner version is more correct for this need.)

  2. A5rocks commented on Aug 2, 2024

    @A5rocks
    Contributor

    I think this would be helpful. Given that debugging tools seem to be in trio.lowlevel typically, should this go there too?

  3. CoolCat467 commented on Aug 2, 2024

    @CoolCat467
    Member

    I would agree that lowlevel would be a good home for in_trio_context

  4. A5rocks commented on Jan 30, 2025

    @A5rocks
    Contributor

    What's the expected behavior in guest runs? GLOBAL_RUN_CONTEXT.runner seems to exist there but... I don't think the host loop should be considered a trio context?

    (maybe we should just tell people "use sniffio.current_async_library" because that has better semantics? I don't know what it returns for to_thread.run_sync calls though.)

  5. oremanj commented on Jan 30, 2025

    @oremanj
    Member

    In a guest run, as well as some other places like abort_fn calls and some instrument calls, you are in a Trio run (runner exists) but not in a Trio task (task does not exist). These concepts are separate but people tend to confuse them (and it doesn't help that Trio's error message is "must be run from async context" for both).

    I attempted to make sniffio explicit about this using python-trio/sniffio#38 and the linked python-trio/sniffio#39 but it was never reviewed.

    I think Trio should probably provide both lowlevel.in_trio_run and lowlevel.in_trio_task.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions