1import sys
2from typing import List
3from pathlib import Path
4
5from jedi.inference.arguments import ValuesArguments
6from jedi.inference.cache import inference_state_method_cache
7from jedi.inference.gradual.annotation import infer_return_types
8from jedi.inference.imports import goto_import, load_module_from_path
9from jedi.inference.filters import ParserTreeFilter
10from jedi.inference.base_value import NO_VALUES, ValueSet
11from jedi.inference.helpers import infer_call_of_leaf
12
13_PYTEST_FIXTURE_MODULES = [
14 ('_pytest', 'monkeypatch'),
15 ('_pytest', 'capture'),
16 ('_pytest', 'logging'),
17 ('_pytest', 'tmpdir'),
18 ('_pytest', 'pytester'),
19]
20
21
22def execute(callback):
23 def wrapper(value, arguments):
24 # This might not be necessary anymore in pytest 4/5, definitely needed
25 # for pytest 3.
26 if value.py__name__() == 'fixture' \
27 and value.parent_context.py__name__() == '_pytest.fixtures':
28 return NO_VALUES
29
30 return callback(value, arguments)
31 return wrapper
32
33
34def infer_anonymous_param(func):
35 def get_returns(value):
36 if value.tree_node.annotation is not None:
37 if value.tree_node.parent.type == 'async_funcdef':
38 # Async fixtures provide the awaited value, rather than a
39 # coroutine. The annotation of an async generator describes
40 # the iterator, so the value is what that iterator yields.
41 result = infer_return_types(value, ValuesArguments([]))
42 if value.as_context().is_generator():
43 return ValueSet.from_sets(
44 lazy_value.infer()
45 for v in result
46 for lazy_value in v.iterate(is_async=True)
47 )
48 return result
49
50 result = value.execute_with_values()
51 if any(v.name.get_qualified_names(include_module_names=True)
52 == ('typing', 'Generator')
53 for v in result):
54 return ValueSet.from_sets(
55 v.py__getattribute__('__next__').execute_annotation(None)
56 for v in result
57 )
58 return result
59
60 # In pytest we need to differentiate between generators and normal
61 # returns.
62 # Parameters still need to be anonymous, .as_context() ensures that.
63 function_context = value.as_context()
64 if function_context.is_generator():
65 return function_context.merge_yield_values()
66 else:
67 return function_context.get_return_values()
68
69 def wrapper(param_name):
70 # parameters with an annotation do not need special handling
71 if param_name.annotation_node:
72 return func(param_name)
73 is_pytest_param, param_name_is_function_name = \
74 _is_a_pytest_param_and_inherited(param_name)
75 if is_pytest_param:
76 module = param_name.get_root_context()
77 fixtures = _goto_pytest_fixture(
78 module,
79 param_name.string_name,
80 # This skips the current module, because we are basically
81 # inheriting a fixture from somewhere else.
82 skip_own_module=param_name_is_function_name,
83 )
84 if fixtures:
85 return ValueSet.from_sets(
86 get_returns(value)
87 for fixture in fixtures
88 for value in fixture.infer()
89 )
90 return func(param_name)
91 return wrapper
92
93
94def goto_anonymous_param(func):
95 def wrapper(param_name):
96 is_pytest_param, param_name_is_function_name = \
97 _is_a_pytest_param_and_inherited(param_name)
98 if is_pytest_param:
99 names = _goto_pytest_fixture(
100 param_name.get_root_context(),
101 param_name.string_name,
102 skip_own_module=param_name_is_function_name,
103 )
104 if names:
105 return names
106 return func(param_name)
107 return wrapper
108
109
110def complete_param_names(func):
111 def wrapper(context, func_name, decorator_nodes):
112 module_context = context.get_root_context()
113 if _is_pytest_func(func_name, decorator_nodes):
114 names = []
115 for module_context in _iter_pytest_modules(module_context):
116 names += FixtureFilter(module_context).values()
117 if names:
118 return names
119 return func(context, func_name, decorator_nodes)
120 return wrapper
121
122
123def _goto_pytest_fixture(module_context, name, skip_own_module):
124 for module_context in _iter_pytest_modules(module_context, skip_own_module=skip_own_module):
125 names = FixtureFilter(module_context).get(name)
126 if names:
127 return names
128
129
130def _is_a_pytest_param_and_inherited(param_name):
131 """
132 Pytest params are either in a `test_*` function or have a pytest fixture
133 with the decorator @pytest.fixture.
134
135 This is a heuristic and will work in most cases.
136 """
137 funcdef = param_name.tree_name.search_ancestor('funcdef')
138 if funcdef is None: # A lambda
139 return False, False
140 decorators = funcdef.get_decorators()
141 return _is_pytest_func(funcdef.name.value, decorators), \
142 funcdef.name.value == param_name.string_name
143
144
145def _is_pytest_func(func_name, decorator_nodes):
146 return func_name.startswith('test') \
147 or any('fixture' in n.get_code() for n in decorator_nodes)
148
149
150def _find_pytest_plugin_modules() -> List[List[str]]:
151 """
152 Finds pytest plugin modules hooked by setuptools entry points
153
154 See https://docs.pytest.org/en/stable/how-to/writing_plugins.html#setuptools-entry-points
155 """
156 from importlib.metadata import entry_points
157
158 if sys.version_info >= (3, 10):
159 pytest_entry_points = entry_points(group="pytest11")
160 else:
161 pytest_entry_points = entry_points().get("pytest11", ())
162
163 return [ep.module.split(".") for ep in pytest_entry_points]
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: # type: ignore # TODO
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 == "async_funcdef":
242 decorated = decorated.parent
243 if decorated.type != "decorated":
244 return False
245 decorators = decorated.children[0]
246 if decorators.type == 'decorators':
247 decorators = decorators.children
248 else:
249 decorators = [decorators]
250 for decorator in decorators:
251 dotted_name = decorator.children[1]
252 # A heuristic, this makes it faster.
253 if 'fixture' in dotted_name.get_code():
254 if dotted_name.type == 'atom_expr':
255 # Since Python3.9 a decorator does not have dotted names
256 # anymore.
257 last_trailer = dotted_name.children[-1]
258 last_leaf = last_trailer.get_last_leaf()
259 if last_leaf == ')':
260 values = infer_call_of_leaf(
261 context, last_leaf, cut_own_trailer=True
262 )
263 else:
264 values = context.infer_node(dotted_name)
265 else:
266 values = context.infer_node(dotted_name)
267 for value in values:
268 if value.name.get_qualified_names(include_module_names=True) \
269 == ('_pytest', 'fixtures', 'fixture'):
270 return True
271 return False