1import sys
2from typing import List
3from pathlib import Path
4
5from parso.tree import search_ancestor
6from jedi.inference.cache import inference_state_method_cache
7from jedi.inference.imports import goto_import, load_module_from_path
8from jedi.inference.filters import ParserTreeFilter
9from jedi.inference.base_value import NO_VALUES, ValueSet
10from jedi.inference.helpers import infer_call_of_leaf
11
12_PYTEST_FIXTURE_MODULES = [
13 ('_pytest', 'monkeypatch'),
14 ('_pytest', 'capture'),
15 ('_pytest', 'logging'),
16 ('_pytest', 'tmpdir'),
17 ('_pytest', 'pytester'),
18]
19
20
21def execute(callback):
22 def wrapper(value, arguments):
23 # This might not be necessary anymore in pytest 4/5, definitely needed
24 # for pytest 3.
25 if value.py__name__() == 'fixture' \
26 and value.parent_context.py__name__() == '_pytest.fixtures':
27 return NO_VALUES
28
29 return callback(value, arguments)
30 return wrapper
31
32
33def infer_anonymous_param(func):
34 def get_returns(value):
35 if value.tree_node.annotation is not None:
36 result = value.execute_with_values()
37 if any(v.name.get_qualified_names(include_module_names=True)
38 == ('typing', 'Generator')
39 for v in result):
40 return ValueSet.from_sets(
41 v.py__getattribute__('__next__').execute_annotation()
42 for v in result
43 )
44 return result
45
46 # In pytest we need to differentiate between generators and normal
47 # returns.
48 # Parameters still need to be anonymous, .as_context() ensures that.
49 function_context = value.as_context()
50 if function_context.is_generator():
51 return function_context.merge_yield_values()
52 else:
53 return function_context.get_return_values()
54
55 def wrapper(param_name):
56 # parameters with an annotation do not need special handling
57 if param_name.annotation_node:
58 return func(param_name)
59 is_pytest_param, param_name_is_function_name = \
60 _is_a_pytest_param_and_inherited(param_name)
61 if is_pytest_param:
62 module = param_name.get_root_context()
63 fixtures = _goto_pytest_fixture(
64 module,
65 param_name.string_name,
66 # This skips the current module, because we are basically
67 # inheriting a fixture from somewhere else.
68 skip_own_module=param_name_is_function_name,
69 )
70 if fixtures:
71 return ValueSet.from_sets(
72 get_returns(value)
73 for fixture in fixtures
74 for value in fixture.infer()
75 )
76 return func(param_name)
77 return wrapper
78
79
80def goto_anonymous_param(func):
81 def wrapper(param_name):
82 is_pytest_param, param_name_is_function_name = \
83 _is_a_pytest_param_and_inherited(param_name)
84 if is_pytest_param:
85 names = _goto_pytest_fixture(
86 param_name.get_root_context(),
87 param_name.string_name,
88 skip_own_module=param_name_is_function_name,
89 )
90 if names:
91 return names
92 return func(param_name)
93 return wrapper
94
95
96def complete_param_names(func):
97 def wrapper(context, func_name, decorator_nodes):
98 module_context = context.get_root_context()
99 if _is_pytest_func(func_name, decorator_nodes):
100 names = []
101 for module_context in _iter_pytest_modules(module_context):
102 names += FixtureFilter(module_context).values()
103 if names:
104 return names
105 return func(context, func_name, decorator_nodes)
106 return wrapper
107
108
109def _goto_pytest_fixture(module_context, name, skip_own_module):
110 for module_context in _iter_pytest_modules(module_context, skip_own_module=skip_own_module):
111 names = FixtureFilter(module_context).get(name)
112 if names:
113 return names
114
115
116def _is_a_pytest_param_and_inherited(param_name):
117 """
118 Pytest params are either in a `test_*` function or have a pytest fixture
119 with the decorator @pytest.fixture.
120
121 This is a heuristic and will work in most cases.
122 """
123 funcdef = search_ancestor(param_name.tree_name, 'funcdef')
124 if funcdef is None: # A lambda
125 return False, False
126 decorators = funcdef.get_decorators()
127 return _is_pytest_func(funcdef.name.value, decorators), \
128 funcdef.name.value == param_name.string_name
129
130
131def _is_pytest_func(func_name, decorator_nodes):
132 return func_name.startswith('test') \
133 or any('fixture' in n.get_code() for n in decorator_nodes)
134
135
136def _find_pytest_plugin_modules() -> List[List[str]]:
137 """
138 Finds pytest plugin modules hooked by setuptools entry points
139
140 See https://docs.pytest.org/en/stable/how-to/writing_plugins.html#setuptools-entry-points
141 """
142 if sys.version_info >= (3, 8):
143 from importlib.metadata import entry_points
144
145 if sys.version_info >= (3, 10):
146 pytest_entry_points = entry_points(group="pytest11")
147 else:
148 pytest_entry_points = entry_points().get("pytest11", ())
149
150 if sys.version_info >= (3, 9):
151 return [ep.module.split(".") for ep in pytest_entry_points]
152 else:
153 # Python 3.8 doesn't have `EntryPoint.module`. Implement equivalent
154 # to what Python 3.9 does (with additional None check to placate `mypy`)
155 matches = [
156 ep.pattern.match(ep.value)
157 for ep in pytest_entry_points
158 ]
159 return [x.group('module').split(".") for x in matches if x]
160
161 else:
162 from pkg_resources import iter_entry_points
163 return [ep.module_name.split(".") for ep in iter_entry_points(group="pytest11")]
164
165
166@inference_state_method_cache()
167def _iter_pytest_modules(module_context, skip_own_module=False):
168 if not skip_own_module:
169 yield module_context
170
171 file_io = module_context.get_value().file_io
172 if file_io is not None:
173 folder = file_io.get_parent_folder()
174 sys_path = module_context.inference_state.get_sys_path()
175
176 # prevent an infinite loop when reaching the root of the current drive
177 last_folder = None
178
179 while any(folder.path.startswith(p) for p in sys_path):
180 file_io = folder.get_file_io('conftest.py')
181 if Path(file_io.path) != module_context.py__file__():
182 try:
183 m = load_module_from_path(module_context.inference_state, file_io)
184 conftest_module = m.as_context()
185 yield conftest_module
186
187 plugins_list = m.tree_node.get_used_names().get("pytest_plugins")
188 if plugins_list:
189 name = conftest_module.create_name(plugins_list[0])
190 yield from _load_pytest_plugins(module_context, name)
191 except FileNotFoundError:
192 pass
193 folder = folder.get_parent_folder()
194
195 # prevent an infinite for loop if the same parent folder is return twice
196 if last_folder is not None and folder.path == last_folder.path:
197 break
198 last_folder = folder # keep track of the last found parent name
199
200 for names in _PYTEST_FIXTURE_MODULES + _find_pytest_plugin_modules():
201 for module_value in module_context.inference_state.import_module(names):
202 yield module_value.as_context()
203
204
205def _load_pytest_plugins(module_context, name):
206 from jedi.inference.helpers import get_str_or_none
207
208 for inferred in name.infer():
209 for seq_value in inferred.py__iter__():
210 for value in seq_value.infer():
211 fq_name = get_str_or_none(value)
212 if fq_name:
213 names = fq_name.split(".")
214 for module_value in module_context.inference_state.import_module(names):
215 yield module_value.as_context()
216
217
218class FixtureFilter(ParserTreeFilter):
219 def _filter(self, names):
220 for name in super()._filter(names):
221 # look for fixture definitions of imported names
222 if name.parent.type == "import_from":
223 imported_names = goto_import(self.parent_context, name)
224 if any(
225 self._is_fixture(iname.parent_context, iname.tree_name)
226 for iname in imported_names
227 # discard imports of whole modules, that have no tree_name
228 if iname.tree_name
229 ):
230 yield name
231
232 elif self._is_fixture(self.parent_context, name):
233 yield name
234
235 def _is_fixture(self, context, name):
236 funcdef = name.parent
237 # Class fixtures are not supported
238 if funcdef.type != "funcdef":
239 return False
240 decorated = funcdef.parent
241 if decorated.type != "decorated":
242 return False
243 decorators = decorated.children[0]
244 if decorators.type == 'decorators':
245 decorators = decorators.children
246 else:
247 decorators = [decorators]
248 for decorator in decorators:
249 dotted_name = decorator.children[1]
250 # A heuristic, this makes it faster.
251 if 'fixture' in dotted_name.get_code():
252 if dotted_name.type == 'atom_expr':
253 # Since Python3.9 a decorator does not have dotted names
254 # anymore.
255 last_trailer = dotted_name.children[-1]
256 last_leaf = last_trailer.get_last_leaf()
257 if last_leaf == ')':
258 values = infer_call_of_leaf(
259 context, last_leaf, cut_own_trailer=True
260 )
261 else:
262 values = context.infer_node(dotted_name)
263 else:
264 values = context.infer_node(dotted_name)
265 for value in values:
266 if value.name.get_qualified_names(include_module_names=True) \
267 == ('_pytest', 'fixtures', 'fixture'):
268 return True
269 return False