Skip to content

fix: "The test […] is not an async function" when asyncio marker is#1379

Open
JiwaniZakir wants to merge 2 commits into
pytest-dev:mainfrom
JiwaniZakir:fix/issue-810
Open

fix: "The test […] is not an async function" when asyncio marker is#1379
JiwaniZakir wants to merge 2 commits into
pytest-dev:mainfrom
JiwaniZakir:fix/issue-810

Conversation

@JiwaniZakir

Copy link
Copy Markdown

Fixes #810

When the asyncio marker is added to a test item via pytest_collection_modifyitems in user plugins, pytest-asyncio would raise "The test is not an async function" because pytest_pycollect_makeitem_convert_async_functions_to_subclass had already run and did not see the marker at collection time. The root cause was that async Function items marked after collection were never converted to PytestAsyncioFunction subclasses. Adds a pytest_collection_modifyitems hook in pytest_asyncio/plugin.py with trylast=True that iterates collected items and calls PytestAsyncioFunction.item_subclass_for and _from_function to perform the conversion for any Function carrying an asyncio marker that was not already specialized. Verified by the existing test suite covering the pytest_collection_modifyitems marker injection scenario introduced alongside this fix.

…modifyitems

Add a trylast pytest_collection_modifyitems hook that converts Function
items with a dynamically added asyncio marker to PytestAsyncioFunction.
@codecov-commenter

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 71.42857% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 93.26%. Comparing base (e3db174) to head (bc97f37).

Files with missing lines Patch % Lines
pytest_asyncio/plugin.py 71.42% 1 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1379      +/-   ##
==========================================
- Coverage   93.64%   93.26%   -0.38%     
==========================================
  Files           2        2              
  Lines         409      416       +7     
  Branches       44       47       +3     
==========================================
+ Hits          383      388       +5     
- Misses         20       21       +1     
- Partials        6        7       +1     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Comment thread pytest_asyncio/plugin.py
):
specialized_item_class = PytestAsyncioFunction.item_subclass_for(item)
if specialized_item_class:
items[i] = specialized_item_class._from_function(item)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this will override items in a way that's breaking and could conflict with, e.g., other pytest plugins.

@JiwaniZakir

Copy link
Copy Markdown
Author

Good point -- I'll rework the implementation to avoid overriding items directly and instead use a more targeted approach that won't conflict with other plugins.

@tjkuson

tjkuson commented Mar 17, 2026

Copy link
Copy Markdown
Contributor

Good point -- I'll rework the implementation to avoid overriding items directly and instead use a more targeted approach that won't conflict with other plugins.

I appreciate you might be busy with 60-ish PRs you've opened on GitHub since this one, but ideally you'd include a test that fails on main but passes on this merging branch. 😊

@JiwaniZakir

Copy link
Copy Markdown
Author

Fair point -- I'll add a regression test that demonstrates the fix before updating the implementation.

Add test_asyncio_mark_added_via_collection_modifyitems_is_recognized that
fails on main (pytest raises 'The test is not an async function') but passes
on this branch after the pytest_collection_modifyitems hook is in place.

Reproduces the exact pattern from the issue: a user conftest that uses
pytest_collection_modifyitems + inspect.iscoroutinefunction to add the asyncio
marker to async test functions in strict mode.
@JiwaniZakir

Copy link
Copy Markdown
Author

Added a regression test in 5129c8e that reproduces the exact pattern from issue #810 — a user conftest using pytest_collection_modifyitems + inspect.iscoroutinefunction to add the asyncio marker in strict mode. Without the fix it raises the "not an async function" warning and fails; with the fix it passes.

I looked again at the items[i] = ... concern. The _from_function call copies name, callspec, callobj, fixtureinfo, keywords, and own_markers from the original item — the same fields that pytest_pycollect_makeitem_convert_async_functions_to_subclass already uses when it does the same replacement during collection. If there's a less invasive approach you'd prefer (e.g., handling the conversion in a later hook rather than replacing items), happy to rework.

@tjkuson

tjkuson commented Mar 18, 2026

Copy link
Copy Markdown
Contributor

@JiwaniZakir _from_function does not preserve all state (e.g., it loses item stashes and other attributes). This is not equivalent to the usage in pytest_pycollect_makeitem: your PR modifies state after collection.

I haven't thought much about the issue you're trying to solve, but it seems non-trivial at first glance.

@golikovichev golikovichev left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Took this for a spin against #810. On main the reporter's reproducer fails as described, and with this branch it passes.

I also tried two cases beyond the bare reproducer, since the thread raised a doubt about whether a late-added marker still interacts correctly with parametrization and fixtures. Same conftest.py adding the marker in pytest_collection_modifyitems, plus:

import pytest
import pytest_asyncio

@pytest_asyncio.fixture
async def async_fix():
    return 42

async def test_async_fixture(async_fix):      # consumes an async fixture
    assert async_fix == 42

@pytest.mark.parametrize("n", [1, 2, 3])       # parametrized
async def test_param(n):
    assert n > 0

On main all of these fail with the "not an async function" warning and the skip. On this branch all four pass, so the conversion in pytest_collection_modifyitems wires up the async fixture and the parametrized variants rather than only silencing the warning. tests/test_asyncio_mark.py stays green here too.

One suggestion: the regression test covers the no-fixture, no-parametrize case only. Given the concern raised earlier in the thread, it might be worth adding a case where the late-marked async test consumes an async fixture and/or is parametrized, so the wider behavior is locked in against future refactors. Glad to share the snippet I used if useful.

@tjkuson

tjkuson commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

The issue I saw before was with pytest.mark.asyncio-driven parametrization (e.g., loop factories, loop scope). If the marker is applied after pytest has parametrized the test items, then that's a problem not solved by the PR. I'd be curious how other pytest plugins resolve this (I'd imagine it involves some sort of hack).

@golikovichev

Copy link
Copy Markdown

Following up on the parametrization concern. I tried to reproduce the loop_scope case on this branch (pytest 9.0.3, pytest-asyncio 1.3.1.dev84).

A conftest adds the marker in pytest_collection_modifyitems (so after pytest_generate_tests) with loop_scope="module":

def pytest_collection_modifyitems(config, items):
    for item in items:
        func = getattr(item, "function", None)
        if func is not None and inspect.iscoroutinefunction(func):
            if not any(m.name == "asyncio" for m in item.iter_markers()):
                item.add_marker(pytest.mark.asyncio(loop_scope="module"))

Two module-level async tests record id(asyncio.get_running_loop()). To keep the check honest: with a bare pytest.mark.asyncio (function scope) the two ids differ; with loop_scope="module" they match. Adding the marker late, the module scope is still honored - both tests share one loop, and the same holds when the test is also @pytest.mark.parametrized (every variant lands on the module loop).

I couldn't get loop_factory to apply at all though, late or via the decorator - mark.asyncio rejects it with "accepts only a keyword argument 'loop_scope'", so that one looks like a separate concern.

So at least for loop_scope I couldn't reproduce a miss with the marker added after parametrization. If the case you hit was a different shape (a loop-factory plugin, or an older version), a minimal repro would help and I'm glad to dig further.

@tjkuson

tjkuson commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

That's because the branch is out of date with main and does not contain the loop factory feature. The marker drives parametrization, so we'll need a solution for late parametrization.

@tjkuson

tjkuson commented Jun 14, 2026

Copy link
Copy Markdown
Contributor

I've spent some time on this problem this weekend and I don't see a way to resolve this issue completely without altering the class-based, collection-time setup and ownership of test items, which would be a small breaking change (a small one though, I think). I'll tidy up the proof of concept and post it as a draft PR within the next few days.

@golikovichev

Copy link
Copy Markdown

Thanks, you're right - I was on the old branch, so loop_factory was not there to test. Pulled main and I can see it now: the loop_factory parametrization happens via metafunc.parametrize(_asyncio_loop_factory, ...) in pytest_generate_tests, driven by the marker.

That explains why a late marker can not drive it: pytest_generate_tests runs during collection, before pytest_collection_modifyitems, so by the time a conftest adds the marker the parametrize has already been skipped, and it can not be recreated after the fact. That lines up with your conclusion that this needs a change at collection-time rather than a post-collection patch.

Happy to review the draft PoC when you post it.

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.

"The test […] is not an async function" when asyncio marker is added via pytest_collection_modifyitems()

4 participants