diff --git a/AUTHORS.txt b/AUTHORS.txt index b8b10a93..b86030e3 100644 --- a/AUTHORS.txt +++ b/AUTHORS.txt @@ -1,4 +1,4 @@ -Main Authors +Main Authors ------------ - David Halter (@davidhalter) @@ -62,6 +62,7 @@ Code Contributors - Andrii Kolomoiets (@muffinmad) - Leo Ryu (@Leo-Ryu) - Joseph Birkner (@josephbirkner) +- Márcio Mazza (@marciomazza) And a few more "anonymous" contributors. diff --git a/jedi/plugins/pytest.py b/jedi/plugins/pytest.py index c78bdb4f..0f6b320d 100644 --- a/jedi/plugins/pytest.py +++ b/jedi/plugins/pytest.py @@ -2,7 +2,7 @@ from pathlib import Path from parso.tree import search_ancestor from jedi.inference.cache import inference_state_method_cache -from jedi.inference.imports import load_module_from_path +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 from jedi.inference.helpers import infer_call_of_leaf @@ -131,6 +131,17 @@ def _is_pytest_func(func_name, decorator_nodes): or any('fixture' in n.get_code() for n in decorator_nodes) +def _find_pytest_plugin_modules(): + """ + Finds pytest plugin modules hooked by setuptools entry points + + See https://docs.pytest.org/en/stable/how-to/writing_plugins.html#setuptools-entry-points + """ + from pkg_resources import iter_entry_points + + return [ep.module_name.split(".") for ep in iter_entry_points(group="pytest11")] + + @inference_state_method_cache() def _iter_pytest_modules(module_context, skip_own_module=False): if not skip_own_module: @@ -159,7 +170,7 @@ def _iter_pytest_modules(module_context, skip_own_module=False): break last_folder = folder # keep track of the last found parent name - for names in _PYTEST_FIXTURE_MODULES: + for names in _PYTEST_FIXTURE_MODULES + _find_pytest_plugin_modules(): for module_value in module_context.inference_state.import_module(names): yield module_value.as_context() @@ -167,14 +178,28 @@ def _iter_pytest_modules(module_context, skip_own_module=False): class FixtureFilter(ParserTreeFilter): def _filter(self, names): for name in super()._filter(names): - funcdef = name.parent - # Class fixtures are not supported - if funcdef.type == 'funcdef': - decorated = funcdef.parent - if decorated.type == 'decorated' and self._is_fixture(decorated): + # look for fixture definitions of imported names + if name.parent.type == "import_from": + imported_names = goto_import(self.parent_context, name) + if any( + self._is_fixture(iname.parent_context, iname.tree_name) + for iname in imported_names + # discard imports of whole modules, that have no tree_name + if iname.tree_name + ): yield name - def _is_fixture(self, decorated): + elif self._is_fixture(self.parent_context, name): + yield name + + def _is_fixture(self, context, name): + funcdef = name.parent + # Class fixtures are not supported + if funcdef.type != "funcdef": + return False + decorated = funcdef.parent + if decorated.type != "decorated": + return False decorators = decorated.children[0] if decorators.type == 'decorators': decorators = decorators.children @@ -191,11 +216,12 @@ class FixtureFilter(ParserTreeFilter): last_leaf = last_trailer.get_last_leaf() if last_leaf == ')': values = infer_call_of_leaf( - self.parent_context, last_leaf, cut_own_trailer=True) + context, last_leaf, cut_own_trailer=True + ) else: - values = self.parent_context.infer_node(dotted_name) + values = context.infer_node(dotted_name) else: - values = self.parent_context.infer_node(dotted_name) + values = context.infer_node(dotted_name) for value in values: if value.name.get_qualified_names(include_module_names=True) \ == ('_pytest', 'fixtures', 'fixture'): diff --git a/test/completion/pytest.py b/test/completion/pytest.py index a900dcda..a39b3fb9 100644 --- a/test/completion/pytest.py +++ b/test/completion/pytest.py @@ -183,3 +183,28 @@ def with_annot() -> Generator[float, None, None]: def test_with_annot(inheritance_fixture, with_annot): #? float() with_annot + +# ----------------- +# pytest external plugins +# ----------------- + +#? ['admin_user', 'admin_client'] +def test_z(admin + +#! 15 ['def admin_client'] +def test_p(admin_client): + #? ['login', 'logout'] + admin_client.log + +@pytest.fixture +@some_decorator +#? ['admin_user'] +def bla(admin_u + return + +@pytest.fixture +@some_decorator +#! 12 ['def admin_user'] +def bla(admin_user): + pass + diff --git a/test/examples/pytest_plugin_package/pytest_plugin/fixtures.py b/test/examples/pytest_plugin_package/pytest_plugin/fixtures.py new file mode 100644 index 00000000..a27cfc71 --- /dev/null +++ b/test/examples/pytest_plugin_package/pytest_plugin/fixtures.py @@ -0,0 +1,6 @@ +from pytest import fixture + + +@fixture() +def admin_user(): + pass diff --git a/test/examples/pytest_plugin_package/pytest_plugin/plugin.py b/test/examples/pytest_plugin_package/pytest_plugin/plugin.py new file mode 100644 index 00000000..e427ca97 --- /dev/null +++ b/test/examples/pytest_plugin_package/pytest_plugin/plugin.py @@ -0,0 +1,16 @@ +import pytest + +from .fixtures import admin_user # noqa + + +@pytest.fixture() +def admin_client(): + return Client() + + +class Client: + def login(self, **credentials): + ... + + def logout(self): + ... diff --git a/test/test_integration.py b/test/test_integration.py index 2e5703aa..2d8e118c 100644 --- a/test/test_integration.py +++ b/test/test_integration.py @@ -1,4 +1,5 @@ import os +from collections import namedtuple import pytest @@ -42,6 +43,22 @@ def test_completion(case, monkeypatch, environment, has_django): if (not has_django) and case.path.endswith('django.py'): pytest.skip('Needs django to be installed to run this test.') + + if case.path.endswith("pytest.py"): + # to test finding pytest fixtures from external plugins + # add a stub pytest plugin to the project sys_path... + pytest_plugin_dir = str(helpers.get_example_dir("pytest_plugin_package")) + case._project.added_sys_path = [pytest_plugin_dir] + + # ... and mock setuptools entry points to include it + # see https://docs.pytest.org/en/stable/how-to/writing_plugins.html#setuptools-entry-points + def mock_iter_entry_points(group): + assert group == "pytest11" + EntryPoint = namedtuple("EntryPoint", ["module_name"]) + return [EntryPoint("pytest_plugin.plugin")] + + monkeypatch.setattr("pkg_resources.iter_entry_points", mock_iter_entry_points) + repo_root = helpers.root_dir monkeypatch.chdir(os.path.join(repo_root, 'jedi')) case.run(assert_case_equal, environment)