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