diff --git a/newsfragments/1191.bugfix.rst b/newsfragments/1191.bugfix.rst new file mode 100644 index 0000000000..65095542f7 --- /dev/null +++ b/newsfragments/1191.bugfix.rst @@ -0,0 +1,3 @@ +Trio no longer crashes when an async function is implemented in C or +Cython and then passed directly to `trio.run` or +``nursery.start_soon``. diff --git a/newsfragments/550.bugfix.rst b/newsfragments/550.bugfix.rst new file mode 100644 index 0000000000..65095542f7 --- /dev/null +++ b/newsfragments/550.bugfix.rst @@ -0,0 +1,3 @@ +Trio no longer crashes when an async function is implemented in C or +Cython and then passed directly to `trio.run` or +``nursery.start_soon``. diff --git a/trio/_core/_run.py b/trio/_core/_run.py index d662d0eb52..b3335ae27e 100644 --- a/trio/_core/_run.py +++ b/trio/_core/_run.py @@ -1348,6 +1348,16 @@ def _return_value_looks_like_wrong_library(value): else: context = copy_context() + if not hasattr(coro, "cr_frame"): + # This async function is implemented in C or Cython + async def python_wrapper(orig_coro): + return await orig_coro + + coro = python_wrapper(coro) + coro.cr_frame.f_locals.setdefault( + LOCALS_KEY_KI_PROTECTION_ENABLED, system_task + ) + task = Task( coro=coro, parent_nursery=nursery, @@ -1355,11 +1365,8 @@ def _return_value_looks_like_wrong_library(value): name=name, context=context, ) - self.tasks.add(task) - coro.cr_frame.f_locals.setdefault( - LOCALS_KEY_KI_PROTECTION_ENABLED, system_task - ) + self.tasks.add(task) if nursery is not None: nursery._children.add(task) task._activate_cancel_status(nursery._cancel_status) diff --git a/trio/_core/tests/test_run.py b/trio/_core/tests/test_run.py index f1ca40f4d4..09aea60eb9 100644 --- a/trio/_core/tests/test_run.py +++ b/trio/_core/tests/test_run.py @@ -8,6 +8,7 @@ import warnings from contextlib import contextmanager, ExitStack from math import inf +from textwrap import dedent import attr import outcome @@ -2389,3 +2390,40 @@ def abort_fn(_): task.coro.send(None) assert abort_fn_called + + +def test_async_function_implemented_in_C(): + # These used to crash because we'd try to mutate the coroutine object's + # cr_frame, but C functions don't have Python frames. + + ns = {"_core": _core} + try: + exec( + dedent( + """ + async def agen_fn(record): + assert not _core.currently_ki_protected() + record.append("the generator ran") + yield + """ + ), + ns, + ) + except SyntaxError: + pytest.skip("Requires Python 3.6+") + else: + agen_fn = ns["agen_fn"] + + run_record = [] + agen = agen_fn(run_record) + _core.run(agen.__anext__) + assert run_record == ["the generator ran"] + + async def main(): + start_soon_record = [] + agen = agen_fn(start_soon_record) + async with _core.open_nursery() as nursery: + nursery.start_soon(agen.__anext__) + assert start_soon_record == ["the generator ran"] + + _core.run(main)