Merge pull request #1879 from marciomazza/find-external-pytest-fixtures

Find external pytest fixtures
This commit is contained in:
Dave Halter
2022-11-11 15:46:40 +00:00
committed by GitHub
6 changed files with 103 additions and 12 deletions

View File

@@ -1,4 +1,4 @@
Main Authors Main Authors
------------ ------------
- David Halter (@davidhalter) <davidhalter88@gmail.com> - David Halter (@davidhalter) <davidhalter88@gmail.com>
@@ -62,6 +62,7 @@ Code Contributors
- Andrii Kolomoiets (@muffinmad) - Andrii Kolomoiets (@muffinmad)
- Leo Ryu (@Leo-Ryu) - Leo Ryu (@Leo-Ryu)
- Joseph Birkner (@josephbirkner) - Joseph Birkner (@josephbirkner)
- Márcio Mazza (@marciomazza)
And a few more "anonymous" contributors. And a few more "anonymous" contributors.

View File

@@ -2,7 +2,7 @@ from pathlib import Path
from parso.tree import search_ancestor from parso.tree import search_ancestor
from jedi.inference.cache import inference_state_method_cache 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.filters import ParserTreeFilter
from jedi.inference.base_value import NO_VALUES, ValueSet from jedi.inference.base_value import NO_VALUES, ValueSet
from jedi.inference.helpers import infer_call_of_leaf 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) 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() @inference_state_method_cache()
def _iter_pytest_modules(module_context, skip_own_module=False): def _iter_pytest_modules(module_context, skip_own_module=False):
if not skip_own_module: if not skip_own_module:
@@ -159,7 +170,7 @@ def _iter_pytest_modules(module_context, skip_own_module=False):
break break
last_folder = folder # keep track of the last found parent name 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): for module_value in module_context.inference_state.import_module(names):
yield module_value.as_context() yield module_value.as_context()
@@ -167,14 +178,28 @@ def _iter_pytest_modules(module_context, skip_own_module=False):
class FixtureFilter(ParserTreeFilter): class FixtureFilter(ParserTreeFilter):
def _filter(self, names): def _filter(self, names):
for name in super()._filter(names): for name in super()._filter(names):
funcdef = name.parent # look for fixture definitions of imported names
# Class fixtures are not supported if name.parent.type == "import_from":
if funcdef.type == 'funcdef': imported_names = goto_import(self.parent_context, name)
decorated = funcdef.parent if any(
if decorated.type == 'decorated' and self._is_fixture(decorated): 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 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] decorators = decorated.children[0]
if decorators.type == 'decorators': if decorators.type == 'decorators':
decorators = decorators.children decorators = decorators.children
@@ -191,11 +216,12 @@ class FixtureFilter(ParserTreeFilter):
last_leaf = last_trailer.get_last_leaf() last_leaf = last_trailer.get_last_leaf()
if last_leaf == ')': if last_leaf == ')':
values = infer_call_of_leaf( values = infer_call_of_leaf(
self.parent_context, last_leaf, cut_own_trailer=True) context, last_leaf, cut_own_trailer=True
)
else: else:
values = self.parent_context.infer_node(dotted_name) values = context.infer_node(dotted_name)
else: else:
values = self.parent_context.infer_node(dotted_name) values = context.infer_node(dotted_name)
for value in values: for value in values:
if value.name.get_qualified_names(include_module_names=True) \ if value.name.get_qualified_names(include_module_names=True) \
== ('_pytest', 'fixtures', 'fixture'): == ('_pytest', 'fixtures', 'fixture'):

View File

@@ -183,3 +183,28 @@ def with_annot() -> Generator[float, None, None]:
def test_with_annot(inheritance_fixture, with_annot): def test_with_annot(inheritance_fixture, with_annot):
#? float() #? float()
with_annot 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

View File

@@ -0,0 +1,6 @@
from pytest import fixture
@fixture()
def admin_user():
pass

View File

@@ -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):
...

View File

@@ -1,4 +1,5 @@
import os import os
from collections import namedtuple
import pytest import pytest
@@ -42,6 +43,22 @@ def test_completion(case, monkeypatch, environment, has_django):
if (not has_django) and case.path.endswith('django.py'): if (not has_django) and case.path.endswith('django.py'):
pytest.skip('Needs django to be installed to run this test.') 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 repo_root = helpers.root_dir
monkeypatch.chdir(os.path.join(repo_root, 'jedi')) monkeypatch.chdir(os.path.join(repo_root, 'jedi'))
case.run(assert_case_equal, environment) case.run(assert_case_equal, environment)