|
import sys |
|
from typing import List |
|
from pathlib import Path |
|
|
|
from parso.tree import search_ancestor |
|
from jedi.inference.cache import inference_state_method_cache |
|
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 |
|
|
|
_PYTEST_FIXTURE_MODULES = [ |
|
('_pytest', 'monkeypatch'), |
|
('_pytest', 'capture'), |
|
('_pytest', 'logging'), |
|
('_pytest', 'tmpdir'), |
|
('_pytest', 'pytester'), |
|
] |
|
|
|
|
|
def execute(callback): |
|
def wrapper(value, arguments): |
|
|
|
|
|
if value.py__name__() == 'fixture' \ |
|
and value.parent_context.py__name__() == '_pytest.fixtures': |
|
return NO_VALUES |
|
|
|
return callback(value, arguments) |
|
return wrapper |
|
|
|
|
|
def infer_anonymous_param(func): |
|
def get_returns(value): |
|
if value.tree_node.annotation is not None: |
|
result = value.execute_with_values() |
|
if any(v.name.get_qualified_names(include_module_names=True) |
|
== ('typing', 'Generator') |
|
for v in result): |
|
return ValueSet.from_sets( |
|
v.py__getattribute__('__next__').execute_annotation() |
|
for v in result |
|
) |
|
return result |
|
|
|
|
|
|
|
|
|
function_context = value.as_context() |
|
if function_context.is_generator(): |
|
return function_context.merge_yield_values() |
|
else: |
|
return function_context.get_return_values() |
|
|
|
def wrapper(param_name): |
|
|
|
if param_name.annotation_node: |
|
return func(param_name) |
|
is_pytest_param, param_name_is_function_name = \ |
|
_is_a_pytest_param_and_inherited(param_name) |
|
if is_pytest_param: |
|
module = param_name.get_root_context() |
|
fixtures = _goto_pytest_fixture( |
|
module, |
|
param_name.string_name, |
|
|
|
|
|
skip_own_module=param_name_is_function_name, |
|
) |
|
if fixtures: |
|
return ValueSet.from_sets( |
|
get_returns(value) |
|
for fixture in fixtures |
|
for value in fixture.infer() |
|
) |
|
return func(param_name) |
|
return wrapper |
|
|
|
|
|
def goto_anonymous_param(func): |
|
def wrapper(param_name): |
|
is_pytest_param, param_name_is_function_name = \ |
|
_is_a_pytest_param_and_inherited(param_name) |
|
if is_pytest_param: |
|
names = _goto_pytest_fixture( |
|
param_name.get_root_context(), |
|
param_name.string_name, |
|
skip_own_module=param_name_is_function_name, |
|
) |
|
if names: |
|
return names |
|
return func(param_name) |
|
return wrapper |
|
|
|
|
|
def complete_param_names(func): |
|
def wrapper(context, func_name, decorator_nodes): |
|
module_context = context.get_root_context() |
|
if _is_pytest_func(func_name, decorator_nodes): |
|
names = [] |
|
for module_context in _iter_pytest_modules(module_context): |
|
names += FixtureFilter(module_context).values() |
|
if names: |
|
return names |
|
return func(context, func_name, decorator_nodes) |
|
return wrapper |
|
|
|
|
|
def _goto_pytest_fixture(module_context, name, skip_own_module): |
|
for module_context in _iter_pytest_modules(module_context, skip_own_module=skip_own_module): |
|
names = FixtureFilter(module_context).get(name) |
|
if names: |
|
return names |
|
|
|
|
|
def _is_a_pytest_param_and_inherited(param_name): |
|
""" |
|
Pytest params are either in a `test_*` function or have a pytest fixture |
|
with the decorator @pytest.fixture. |
|
|
|
This is a heuristic and will work in most cases. |
|
""" |
|
funcdef = search_ancestor(param_name.tree_name, 'funcdef') |
|
if funcdef is None: |
|
return False, False |
|
decorators = funcdef.get_decorators() |
|
return _is_pytest_func(funcdef.name.value, decorators), \ |
|
funcdef.name.value == param_name.string_name |
|
|
|
|
|
def _is_pytest_func(func_name, decorator_nodes): |
|
return func_name.startswith('test') \ |
|
or any('fixture' in n.get_code() for n in decorator_nodes) |
|
|
|
|
|
def _find_pytest_plugin_modules() -> List[List[str]]: |
|
""" |
|
Finds pytest plugin modules hooked by setuptools entry points |
|
|
|
See https://docs.pytest.org/en/stable/how-to/writing_plugins.html#setuptools-entry-points |
|
""" |
|
if sys.version_info >= (3, 8): |
|
from importlib.metadata import entry_points |
|
|
|
if sys.version_info >= (3, 10): |
|
pytest_entry_points = entry_points(group="pytest11") |
|
else: |
|
pytest_entry_points = entry_points().get("pytest11", ()) |
|
|
|
if sys.version_info >= (3, 9): |
|
return [ep.module.split(".") for ep in pytest_entry_points] |
|
else: |
|
|
|
|
|
matches = [ |
|
ep.pattern.match(ep.value) |
|
for ep in pytest_entry_points |
|
] |
|
return [x.group('module').split(".") for x in matches if x] |
|
|
|
else: |
|
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: |
|
yield module_context |
|
|
|
file_io = module_context.get_value().file_io |
|
if file_io is not None: |
|
folder = file_io.get_parent_folder() |
|
sys_path = module_context.inference_state.get_sys_path() |
|
|
|
|
|
last_folder = None |
|
|
|
while any(folder.path.startswith(p) for p in sys_path): |
|
file_io = folder.get_file_io('conftest.py') |
|
if Path(file_io.path) != module_context.py__file__(): |
|
try: |
|
m = load_module_from_path(module_context.inference_state, file_io) |
|
yield m.as_context() |
|
except FileNotFoundError: |
|
pass |
|
folder = folder.get_parent_folder() |
|
|
|
|
|
if last_folder is not None and folder.path == last_folder.path: |
|
break |
|
last_folder = folder |
|
|
|
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() |
|
|
|
|
|
class FixtureFilter(ParserTreeFilter): |
|
def _filter(self, names): |
|
for name in super()._filter(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 |
|
|
|
if iname.tree_name |
|
): |
|
yield name |
|
|
|
elif self._is_fixture(self.parent_context, name): |
|
yield name |
|
|
|
def _is_fixture(self, context, name): |
|
funcdef = name.parent |
|
|
|
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 |
|
else: |
|
decorators = [decorators] |
|
for decorator in decorators: |
|
dotted_name = decorator.children[1] |
|
|
|
if 'fixture' in dotted_name.get_code(): |
|
if dotted_name.type == 'atom_expr': |
|
|
|
|
|
last_trailer = dotted_name.children[-1] |
|
last_leaf = last_trailer.get_last_leaf() |
|
if last_leaf == ')': |
|
values = infer_call_of_leaf( |
|
context, last_leaf, cut_own_trailer=True |
|
) |
|
else: |
|
values = context.infer_node(dotted_name) |
|
else: |
|
values = context.infer_node(dotted_name) |
|
for value in values: |
|
if value.name.get_qualified_names(include_module_names=True) \ |
|
== ('_pytest', 'fixtures', 'fixture'): |
|
return True |
|
return False |
|
|