Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/jedi/plugins/pytest.py: 21%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

149 statements  

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(None) 

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 from importlib.metadata import entry_points 

142 

143 if sys.version_info >= (3, 10): 

144 pytest_entry_points = entry_points(group="pytest11") 

145 else: 

146 pytest_entry_points = entry_points().get("pytest11", ()) 

147 

148 return [ep.module.split(".") for ep in pytest_entry_points] 

149 

150 

151@inference_state_method_cache() 

152def _iter_pytest_modules(module_context, skip_own_module=False): 

153 if not skip_own_module: 

154 yield module_context 

155 

156 file_io = module_context.get_value().file_io 

157 if file_io is not None: 

158 folder = file_io.get_parent_folder() 

159 sys_path = module_context.inference_state.get_sys_path() 

160 

161 # prevent an infinite loop when reaching the root of the current drive 

162 last_folder = None 

163 

164 while any(folder.path.startswith(p) for p in sys_path): 

165 file_io = folder.get_file_io('conftest.py') 

166 if Path(file_io.path) != module_context.py__file__(): 

167 try: 

168 m = load_module_from_path(module_context.inference_state, file_io) 

169 conftest_module = m.as_context() 

170 yield conftest_module 

171 

172 plugins_list = m.tree_node.get_used_names().get("pytest_plugins") 

173 if plugins_list: 

174 name = conftest_module.create_name(plugins_list[0]) 

175 yield from _load_pytest_plugins(module_context, name) 

176 except FileNotFoundError: 

177 pass 

178 folder = folder.get_parent_folder() 

179 

180 # prevent an infinite for loop if the same parent folder is return twice 

181 if last_folder is not None and folder.path == last_folder.path: # type: ignore # TODO 

182 break 

183 last_folder = folder # keep track of the last found parent name 

184 

185 for names in _PYTEST_FIXTURE_MODULES + _find_pytest_plugin_modules(): 

186 for module_value in module_context.inference_state.import_module(names): 

187 yield module_value.as_context() 

188 

189 

190def _load_pytest_plugins(module_context, name): 

191 from jedi.inference.helpers import get_str_or_none 

192 

193 for inferred in name.infer(): 

194 for seq_value in inferred.py__iter__(): 

195 for value in seq_value.infer(): 

196 fq_name = get_str_or_none(value) 

197 if fq_name: 

198 names = fq_name.split(".") 

199 for module_value in module_context.inference_state.import_module(names): 

200 yield module_value.as_context() 

201 

202 

203class FixtureFilter(ParserTreeFilter): 

204 def _filter(self, names): 

205 for name in super()._filter(names): 

206 # look for fixture definitions of imported names 

207 if name.parent.type == "import_from": 

208 imported_names = goto_import(self.parent_context, name) 

209 if any( 

210 self._is_fixture(iname.parent_context, iname.tree_name) 

211 for iname in imported_names 

212 # discard imports of whole modules, that have no tree_name 

213 if iname.tree_name 

214 ): 

215 yield name 

216 

217 elif self._is_fixture(self.parent_context, name): 

218 yield name 

219 

220 def _is_fixture(self, context, name): 

221 funcdef = name.parent 

222 # Class fixtures are not supported 

223 if funcdef.type != "funcdef": 

224 return False 

225 decorated = funcdef.parent 

226 if decorated.type != "decorated": 

227 return False 

228 decorators = decorated.children[0] 

229 if decorators.type == 'decorators': 

230 decorators = decorators.children 

231 else: 

232 decorators = [decorators] 

233 for decorator in decorators: 

234 dotted_name = decorator.children[1] 

235 # A heuristic, this makes it faster. 

236 if 'fixture' in dotted_name.get_code(): 

237 if dotted_name.type == 'atom_expr': 

238 # Since Python3.9 a decorator does not have dotted names 

239 # anymore. 

240 last_trailer = dotted_name.children[-1] 

241 last_leaf = last_trailer.get_last_leaf() 

242 if last_leaf == ')': 

243 values = infer_call_of_leaf( 

244 context, last_leaf, cut_own_trailer=True 

245 ) 

246 else: 

247 values = context.infer_node(dotted_name) 

248 else: 

249 values = context.infer_node(dotted_name) 

250 for value in values: 

251 if value.name.get_qualified_names(include_module_names=True) \ 

252 == ('_pytest', 'fixtures', 'fixture'): 

253 return True 

254 return False