From d8efff8e30b76d29c27600363c741c4e8cb88495 Mon Sep 17 00:00:00 2001 From: Abdullah Ahmed Date: Mon, 12 Oct 2026 04:05:48 -0400 Subject: [PATCH 1/2] Recognize asynchronous pytest fixtures --- AUTHORS.txt | 2 ++ CHANGELOG.rst | 1 + jedi/plugins/pytest.py | 6 +++++- test/completion/pytest.py | 38 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 46 insertions(+), 1 deletion(-) diff --git a/AUTHORS.txt b/AUTHORS.txt index 0c9f4cb7..7342b78f 100644 --- a/AUTHORS.txt +++ b/AUTHORS.txt @@ -71,6 +71,8 @@ Code Contributors - Dhanraj E (@Dhanu-dynamic) - Charan Rathore (@charan-rathore) +- Abdullah Ahmed (@abdull-ah-med) (with OpenAI Codex assistance) + And a few more "anonymous" contributors. Note: (@user) means a github user name. diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 5ed0f90c..c8d5edf9 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -6,6 +6,7 @@ Changelog Unreleased ++++++++++ +- Complete, infer and navigate to asynchronous pytest fixtures - Avoid changing the global recursion limit 0.20.0 (2026-05-02) diff --git a/jedi/plugins/pytest.py b/jedi/plugins/pytest.py index f7e88d2c..edaab3a7 100644 --- a/jedi/plugins/pytest.py +++ b/jedi/plugins/pytest.py @@ -31,7 +31,9 @@ def execute(callback): def infer_anonymous_param(func): def get_returns(value): - if value.tree_node.annotation is not None: + # Async fixtures provide the awaited value, rather than a coroutine. + if (value.tree_node.annotation is not None + and value.tree_node.parent.type != 'async_funcdef'): result = value.execute_with_values() if any(v.name.get_qualified_names(include_module_names=True) == ('typing', 'Generator') @@ -223,6 +225,8 @@ class FixtureFilter(ParserTreeFilter): if funcdef.type != "funcdef": return False decorated = funcdef.parent + if decorated.type == "async_funcdef": + decorated = decorated.parent if decorated.type != "decorated": return False decorators = decorated.children[0] diff --git a/test/completion/pytest.py b/test/completion/pytest.py index 1f52c4f2..d84ef58a 100644 --- a/test/completion/pytest.py +++ b/test/completion/pytest.py @@ -208,3 +208,41 @@ def bla(admin_u def bla(admin_user): pass + + +# ----------------- +# asynchronous fixtures +# ----------------- + +@pytest.fixture +async def async_fixture(): + return 1 + + +@pytest.fixture(scope='module') +async def async_annotated_fixture() -> str: + return '' + + +@fixture +async def async_yield_fixture(): + yield 1.0 + + +#? ['async_fixture'] +def test_async_fixture_completion(async_fixt + pass + + +def test_async_fixture_inference(async_fixture, async_annotated_fixture, async_yield_fixture): + #? int() + async_fixture + #? str() + async_annotated_fixture + #? float() + async_yield_fixture + + +#! 30 ['def async_fixture'] +def test_async_fixture_goto(async_fixture): + pass From 3f7e8adddc520079692abba4d0a18be1ca1b5066 Mon Sep 17 00:00:00 2001 From: Abdullah Ahmed Date: Mon, 12 Oct 2026 04:52:32 -0400 Subject: [PATCH 2/2] Honor return annotations of async pytest fixtures Async fixtures skipped the annotation branch, so a `-> T` annotation was ignored and only the body was inferred. Infer the annotation directly: the awaited value for coroutines, and the yielded value for annotated async generators. Add cases where the body is not inferable. Also drop the tooling note from the AUTHORS entry. Co-Authored-By: Claude Sonnet 5.5 --- AUTHORS.txt | 2 +- jedi/plugins/pytest.py | 19 ++++++++++++++++--- test/completion/pytest.py | 19 ++++++++++++++++++- 3 files changed, 35 insertions(+), 5 deletions(-) diff --git a/AUTHORS.txt b/AUTHORS.txt index 7342b78f..222d9c10 100644 --- a/AUTHORS.txt +++ b/AUTHORS.txt @@ -71,7 +71,7 @@ Code Contributors - Dhanraj E (@Dhanu-dynamic) - Charan Rathore (@charan-rathore) -- Abdullah Ahmed (@abdull-ah-med) (with OpenAI Codex assistance) +- Abdullah Ahmed (@abdull-ah-med) And a few more "anonymous" contributors. diff --git a/jedi/plugins/pytest.py b/jedi/plugins/pytest.py index edaab3a7..ac8c2952 100644 --- a/jedi/plugins/pytest.py +++ b/jedi/plugins/pytest.py @@ -2,7 +2,9 @@ import sys from typing import List from pathlib import Path +from jedi.inference.arguments import ValuesArguments from jedi.inference.cache import inference_state_method_cache +from jedi.inference.gradual.annotation import infer_return_types from jedi.inference.imports import goto_import, load_module_from_path from jedi.inference.filters import ParserTreeFilter from jedi.inference.base_value import NO_VALUES, ValueSet @@ -31,9 +33,20 @@ def execute(callback): def infer_anonymous_param(func): def get_returns(value): - # Async fixtures provide the awaited value, rather than a coroutine. - if (value.tree_node.annotation is not None - and value.tree_node.parent.type != 'async_funcdef'): + if value.tree_node.annotation is not None: + if value.tree_node.parent.type == 'async_funcdef': + # Async fixtures provide the awaited value, rather than a + # coroutine. The annotation of an async generator describes + # the iterator, so the value is what that iterator yields. + result = infer_return_types(value, ValuesArguments([])) + if value.as_context().is_generator(): + return ValueSet.from_sets( + lazy_value.infer() + for v in result + for lazy_value in v.iterate(is_async=True) + ) + return result + result = value.execute_with_values() if any(v.name.get_qualified_names(include_module_names=True) == ('typing', 'Generator') diff --git a/test/completion/pytest.py b/test/completion/pytest.py index d84ef58a..1e898136 100644 --- a/test/completion/pytest.py +++ b/test/completion/pytest.py @@ -1,4 +1,4 @@ -from typing import Generator +from typing import AsyncGenerator, Generator import pytest from pytest import fixture @@ -229,6 +229,16 @@ async def async_yield_fixture(): yield 1.0 +@pytest.fixture +async def async_annotated_opaque() -> int: + return opaque_value + + +@pytest.fixture +async def async_generator_annotated() -> AsyncGenerator[str, None]: + yield opaque_value + + #? ['async_fixture'] def test_async_fixture_completion(async_fixt pass @@ -243,6 +253,13 @@ def test_async_fixture_inference(async_fixture, async_annotated_fixture, async_y async_yield_fixture +def test_async_fixture_annotation(async_annotated_opaque, async_generator_annotated): + #? int() + async_annotated_opaque + #? str() + async_generator_annotated + + #! 30 ['def async_fixture'] def test_async_fixture_goto(async_fixture): pass