]>
Commit | Line | Data |
---|---|---|
53e6db90 DC |
1 | import sys |
2 | from typing import List | |
3 | from pathlib import Path | |
4 | ||
5 | from parso.tree import search_ancestor | |
6 | from jedi.inference.cache import inference_state_method_cache | |
7 | from jedi.inference.imports import goto_import, load_module_from_path | |
8 | from jedi.inference.filters import ParserTreeFilter | |
9 | from jedi.inference.base_value import NO_VALUES, ValueSet | |
10 | from 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 | ||
21 | def 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 | ||
33 | def 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 | ||
80 | def 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 | ||
96 | def 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 | ||
109 | def _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 | ||
116 | def _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 | ||
131 | def _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 | ||
136 | def _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() | |
167 | def _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 | yield m.as_context() | |
185 | except FileNotFoundError: | |
186 | pass | |
187 | folder = folder.get_parent_folder() | |
188 | ||
189 | # prevent an infinite for loop if the same parent folder is return twice | |
190 | if last_folder is not None and folder.path == last_folder.path: | |
191 | break | |
192 | last_folder = folder # keep track of the last found parent name | |
193 | ||
194 | for names in _PYTEST_FIXTURE_MODULES + _find_pytest_plugin_modules(): | |
195 | for module_value in module_context.inference_state.import_module(names): | |
196 | yield module_value.as_context() | |
197 | ||
198 | ||
199 | class FixtureFilter(ParserTreeFilter): | |
200 | def _filter(self, names): | |
201 | for name in super()._filter(names): | |
202 | # look for fixture definitions of imported names | |
203 | if name.parent.type == "import_from": | |
204 | imported_names = goto_import(self.parent_context, name) | |
205 | if any( | |
206 | self._is_fixture(iname.parent_context, iname.tree_name) | |
207 | for iname in imported_names | |
208 | # discard imports of whole modules, that have no tree_name | |
209 | if iname.tree_name | |
210 | ): | |
211 | yield name | |
212 | ||
213 | elif self._is_fixture(self.parent_context, name): | |
214 | yield name | |
215 | ||
216 | def _is_fixture(self, context, name): | |
217 | funcdef = name.parent | |
218 | # Class fixtures are not supported | |
219 | if funcdef.type != "funcdef": | |
220 | return False | |
221 | decorated = funcdef.parent | |
222 | if decorated.type != "decorated": | |
223 | return False | |
224 | decorators = decorated.children[0] | |
225 | if decorators.type == 'decorators': | |
226 | decorators = decorators.children | |
227 | else: | |
228 | decorators = [decorators] | |
229 | for decorator in decorators: | |
230 | dotted_name = decorator.children[1] | |
231 | # A heuristic, this makes it faster. | |
232 | if 'fixture' in dotted_name.get_code(): | |
233 | if dotted_name.type == 'atom_expr': | |
234 | # Since Python3.9 a decorator does not have dotted names | |
235 | # anymore. | |
236 | last_trailer = dotted_name.children[-1] | |
237 | last_leaf = last_trailer.get_last_leaf() | |
238 | if last_leaf == ')': | |
239 | values = infer_call_of_leaf( | |
240 | context, last_leaf, cut_own_trailer=True | |
241 | ) | |
242 | else: | |
243 | values = context.infer_node(dotted_name) | |
244 | else: | |
245 | values = context.infer_node(dotted_name) | |
246 | for value in values: | |
247 | if value.name.get_qualified_names(include_module_names=True) \ | |
248 | == ('_pytest', 'fixtures', 'fixture'): | |
249 | return True | |
250 | return False |