diff --git a/AUTHORS.txt b/AUTHORS.txt index 0c9f4cb7..222d9c10 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) + 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..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 @@ -32,6 +34,19 @@ def execute(callback): def infer_anonymous_param(func): def get_returns(value): 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') @@ -223,6 +238,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..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 @@ -208,3 +208,58 @@ 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 + + +@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 + + +def test_async_fixture_inference(async_fixture, async_annotated_fixture, async_yield_fixture): + #? int() + async_fixture + #? str() + async_annotated_fixture + #? float() + 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