Coverage for src/pytest_cases/common_pytest_marks.py: 66%

167 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2024-04-04 21:17 +0000

1# Authors: Sylvain MARIE <sylvain.marie@se.com> 

2# + All contributors to <https://github.com/smarie/python-pytest-cases> 

3# 

4# License: 3-clause BSD, <https://github.com/smarie/python-pytest-cases/blob/master/LICENSE> 

5import itertools 

6 

7import warnings 

8from packaging.version import Version 

9 

10try: # python 3.3+ 

11 from inspect import signature 

12except ImportError: 

13 from funcsigs import signature # noqa 

14 

15try: 

16 from typing import Iterable, Optional, Tuple, List, Set, Union, Sequence # noqa 

17except ImportError: 

18 pass 

19 

20import pytest 

21 

22try: 

23 from _pytest.mark.structures import MarkDecorator, Mark # noqa 

24except ImportError: 

25 from _pytest.mark import MarkDecorator, MarkInfo as Mark # noqa 

26 

27from .common_mini_six import string_types 

28 

29 

30PYTEST_VERSION = Version(pytest.__version__) 

31PYTEST3_OR_GREATER = PYTEST_VERSION >= Version('3.0.0') 

32PYTEST32_OR_GREATER = PYTEST_VERSION >= Version('3.2.0') 

33PYTEST33_OR_GREATER = PYTEST_VERSION >= Version('3.3.0') 

34PYTEST34_OR_GREATER = PYTEST_VERSION >= Version('3.4.0') 

35PYTEST35_OR_GREATER = PYTEST_VERSION >= Version('3.5.0') 

36PYTEST361_36X = Version('3.6.0') < PYTEST_VERSION < Version('3.7.0') 

37PYTEST37_OR_GREATER = PYTEST_VERSION >= Version('3.7.0') 

38PYTEST38_OR_GREATER = PYTEST_VERSION >= Version('3.8.0') 

39PYTEST46_OR_GREATER = PYTEST_VERSION >= Version('4.6.0') 

40PYTEST53_OR_GREATER = PYTEST_VERSION >= Version('5.3.0') 

41PYTEST54_OR_GREATER = PYTEST_VERSION >= Version('5.4.0') 

42PYTEST421_OR_GREATER = PYTEST_VERSION >= Version('4.2.1') 

43PYTEST6_OR_GREATER = PYTEST_VERSION >= Version('6.0.0') 

44PYTEST7_OR_GREATER = PYTEST_VERSION >= Version('7.0.0') 

45PYTEST71_OR_GREATER = PYTEST_VERSION >= Version('7.1.0') 

46PYTEST8_OR_GREATER = PYTEST_VERSION >= Version('8.0.0') 

47PYTEST811 = PYTEST_VERSION == Version('8.1.1') 

48 

49 

50def get_param_argnames_as_list(argnames): 

51 """ 

52 pytest parametrize accepts both coma-separated names and list/tuples. 

53 This function makes sure that we always return a list 

54 :param argnames: 

55 :return: 

56 """ 

57 if isinstance(argnames, string_types): 

58 argnames = argnames.replace(' ', '').split(',') 

59 return list(argnames) 

60 

61 

62# noinspection PyUnusedLocal 

63def _pytest_mark_parametrize(argnames, argvalues, ids=None, indirect=False, scope=None, **kwargs): 

64 """ Fake method to have a reference signature of pytest.mark.parametrize""" 

65 pass 

66 

67 

68def get_parametrize_signature(): 

69 """ 

70 

71 :return: a reference signature representing 

72 """ 

73 return signature(_pytest_mark_parametrize) 

74 

75 

76class _ParametrizationMark: 

77 """ 

78 Container for the mark information that we grab from the fixtures (`@fixture`) 

79 

80 Represents the information required by `@fixture` to work. 

81 """ 

82 __slots__ = "param_names", "param_values", "param_ids" 

83 

84 def __init__(self, mark): 

85 bound = get_parametrize_signature().bind(*mark.args, **mark.kwargs) 

86 try: 

87 remaining_kwargs = bound.arguments['kwargs'] 

88 except KeyError: 

89 pass 

90 else: 

91 if len(remaining_kwargs) > 0: 

92 warnings.warn("parametrize kwargs not taken into account: %s. Please report it at" 

93 " https://github.com/smarie/python-pytest-cases/issues" % remaining_kwargs) 

94 self.param_names = get_param_argnames_as_list(bound.arguments['argnames']) 

95 self.param_values = bound.arguments['argvalues'] 

96 try: 

97 bound.apply_defaults() 

98 self.param_ids = bound.arguments['ids'] 

99 except AttributeError: 

100 # can happen if signature is from funcsigs so we have to apply ourselves 

101 self.param_ids = bound.arguments.get('ids', None) 

102 

103 

104# -------- tools to get the parametrization mark whatever the pytest version 

105class _LegacyMark: 

106 __slots__ = "args", "kwargs" 

107 

108 def __init__(self, *args, **kwargs): 

109 self.args = args 

110 self.kwargs = kwargs 

111 

112 

113# ---------------- working on functions 

114def copy_pytest_marks(from_f, to_f, override=False): 

115 """Copy all pytest marks from a function or class to another""" 

116 from_marks = get_pytest_marks_on_function(from_f) 

117 to_marks = [] if override else get_pytest_marks_on_function(to_f) 

118 # note: the new marks are appended *after* existing if no override 

119 to_f.pytestmark = to_marks + from_marks 

120 

121 

122def filter_marks(marks, # type: Iterable[Mark] 

123 remove # type: str 

124 ): 

125 # type: (...) -> Tuple[Mark] 

126 """ 

127 Returns a tuple of all marks in `marks` that do not have a 'parametrize' name. 

128 

129 :param marks: 

130 :param remove: 

131 :return: 

132 """ 

133 return tuple(m for m in marks if m.name != remove) 

134 

135 

136def get_pytest_marks_on_function(f, 

137 as_decorators=False # type: bool 

138 ): 

139 # type: (...) -> Union[List[Mark], List[MarkDecorator]] 

140 """ 

141 Utility to return a list of *ALL* pytest marks (not only parametrization) applied on a function 

142 Note that this also works on classes 

143 

144 :param f: 

145 :param as_decorators: transforms the marks into decorators before returning them 

146 :return: 

147 """ 

148 try: 

149 mks = f.pytestmark 

150 except AttributeError: 

151 try: 

152 # old pytest < 3: marks are set as fields on the function object 

153 # but they do not have a particular type, their type is 'instance'... 

154 mks = [v for v in vars(f).values() if str(v).startswith("<MarkInfo '")] 

155 except AttributeError: 

156 return [] 

157 

158 # in the new version of pytest the marks have to be transformed into decorators explicitly 

159 if as_decorators: 

160 return markinfos_to_markdecorators(mks, function_marks=True) 

161 else: 

162 return mks 

163 

164 

165def get_pytest_marks_on_item(item): 

166 """lists all marks on an item such as `request._pyfuncitem`""" 

167 if PYTEST3_OR_GREATER: 167 ↛ 170line 167 didn't jump to line 170, because the condition on line 167 was never false

168 return item.callspec.marks 

169 else: 

170 return [val for val in item.keywords.values() if isinstance(val, (MarkDecorator, Mark))] 

171 

172 

173def get_pytest_usefixture_marks(f): 

174 # pytest > 3.2.0 

175 marks = getattr(f, 'pytestmark', None) 

176 if marks is not None: 

177 return tuple(itertools.chain.from_iterable( 

178 mark.args for mark in marks if mark.name == 'usefixtures' 

179 )) 

180 else: 

181 # older versions 

182 mark_info = getattr(f, 'usefixtures', None) 

183 if mark_info is not None: 183 ↛ 184line 183 didn't jump to line 184, because the condition on line 183 was never true

184 return mark_info.args 

185 else: 

186 return () 

187 

188 

189def remove_pytest_mark(f, mark_name): 

190 marks = getattr(f, 'pytestmark', None) 

191 if marks is not None: 191 ↛ 197line 191 didn't jump to line 197, because the condition on line 191 was never false

192 # pytest > 3.2.0 

193 new_marks = [m for m in marks if m.name != mark_name] 

194 f.pytestmark = new_marks 

195 else: 

196 # older versions 

197 try: 

198 delattr(f, mark_name) 

199 except AttributeError: 

200 pass 

201 return f 

202 

203 

204def get_pytest_parametrize_marks( 

205 f, 

206 pop=False # type: bool 

207): 

208 """ 

209 Returns the @pytest.mark.parametrize marks associated with a function (and only those) 

210 

211 :param f: 

212 :param pop: boolean flag, when True the marks will be removed from f. 

213 :return: a tuple containing all 'parametrize' marks 

214 """ 

215 # pytest > 3.2.0 

216 marks = getattr(f, 'pytestmark', None) 

217 if marks is not None: 

218 if pop: 218 ↛ 219line 218 didn't jump to line 219, because the condition on line 218 was never true

219 delattr(f, 'pytestmark') 

220 return tuple(_ParametrizationMark(m) for m in marks if m.name == 'parametrize') 

221 else: 

222 # older versions 

223 mark_info = getattr(f, 'parametrize', None) 

224 if mark_info is not None: 224 ↛ 225line 224 didn't jump to line 225, because the condition on line 224 was never true

225 if pop: 

226 delattr(f, 'parametrize') 

227 # mark_info.args contains a list of (name, values) 

228 if len(mark_info.args) % 2 != 0: 

229 raise ValueError("internal pytest compatibility error - please report") 

230 nb_parametrize_decorations = len(mark_info.args) // 2 

231 if nb_parametrize_decorations > 1 and len(mark_info.kwargs) > 0: 

232 raise ValueError("Unfortunately with this old pytest version it is not possible to have several " 

233 "parametrization decorators while specifying **kwargs, as all **kwargs are " 

234 "merged, leading to inconsistent results. Either upgrade pytest, remove the **kwargs," 

235 "or merge all the @parametrize decorators into a single one. **kwargs: %s" 

236 % mark_info.kwargs) 

237 res = [] 

238 for i in range(nb_parametrize_decorations): 

239 param_name, param_values = mark_info.args[2*i:2*(i+1)] 

240 res.append(_ParametrizationMark(_LegacyMark(param_name, param_values, **mark_info.kwargs))) 

241 return tuple(res) 

242 else: 

243 return () 

244 

245 

246# ---- tools to reapply marks on test parameter values, whatever the pytest version ---- 

247 

248# Compatibility for the way we put marks on single parameters in the list passed to @pytest.mark.parametrize 

249# see https://docs.pytest.org/en/3.3.0/skipping.html?highlight=mark%20parametrize#skip-xfail-with-parametrize 

250 

251# check if pytest.param exists 

252has_pytest_param = hasattr(pytest, 'param') 

253 

254 

255if not has_pytest_param: 255 ↛ 258line 255 didn't jump to line 258, because the condition on line 255 was never true

256 # if not this is how it was done 

257 # see e.g. https://docs.pytest.org/en/2.9.2/skipping.html?highlight=mark%20parameter#skip-xfail-with-parametrize 

258 def make_marked_parameter_value(argvalues_tuple, marks): 

259 if len(marks) > 1: 

260 raise ValueError("Multiple marks on parameters not supported for old versions of pytest") 

261 else: 

262 if not isinstance(argvalues_tuple, tuple): 

263 raise TypeError("argvalues must be a tuple !") 

264 

265 # get a decorator for each of the markinfo 

266 marks_mod = markinfos_to_markdecorators(marks, function_marks=False) 

267 

268 # decorate. We need to distinguish between single value and multiple values 

269 # indeed in pytest 2 a single arg passed to the decorator is passed directly 

270 # (for example: @pytest.mark.skip(1) in parametrize) 

271 return marks_mod[0](argvalues_tuple) if len(argvalues_tuple) > 1 else marks_mod[0](argvalues_tuple[0]) 

272else: 

273 # Otherwise pytest.param exists, it is easier 

274 def make_marked_parameter_value(argvalues_tuple, marks): 

275 if not isinstance(argvalues_tuple, tuple): 275 ↛ 276line 275 didn't jump to line 276, because the condition on line 275 was never true

276 raise TypeError("argvalues must be a tuple !") 

277 

278 # get a decorator for each of the markinfo 

279 marks_mod = markinfos_to_markdecorators(marks, function_marks=False) 

280 

281 # decorate 

282 return pytest.param(*argvalues_tuple, marks=marks_mod) 

283 

284 

285def markinfos_to_markdecorators(marks, # type: Iterable[Mark] 

286 function_marks=False # type: bool 

287 ): 

288 # type: (...) -> List[MarkDecorator] 

289 """ 

290 Transforms the provided marks (MarkInfo or Mark in recent pytest) obtained from marked cases, into MarkDecorator so 

291 that they can be re-applied to generated pytest parameters in the global @pytest.mark.parametrize. 

292 

293 Returns a list. 

294 

295 :param marks: 

296 :param function_marks: 

297 :return: 

298 """ 

299 marks_mod = [] 

300 try: 

301 # suppress the warning message that pytest generates when calling pytest.mark.MarkDecorator() directly 

302 with warnings.catch_warnings(): 

303 warnings.simplefilter("ignore") 

304 for m in marks: 

305 if PYTEST3_OR_GREATER: 305 ↛ 314line 305 didn't jump to line 314, because the condition on line 305 was never false

306 if isinstance(m, MarkDecorator): 

307 # already a decorator, we can use it 

308 marks_mod.append(m) 

309 else: 

310 md = MarkDecorator(m) 

311 marks_mod.append(md) 

312 else: 

313 # create a dummy new MarkDecorator named "MarkDecorator" for reference 

314 md = MarkDecorator() 

315 # always recreate one, type comparison does not work (all generic stuff) 

316 md.name = m.name 

317 

318 if function_marks: 

319 md.args = m.args # a mark on a function does not include the function in the args 

320 else: 

321 md.args = m.args[:-1] # not a function: the value is in the args, remove it 

322 md.kwargs = m.kwargs 

323 

324 marks_mod.append(md) 

325 

326 except Exception as e: 

327 warnings.warn("Caught exception while trying to mark case: [%s] %s" % (type(e), e)) 

328 return marks_mod 

329 

330 

331def markdecorators_as_tuple(marks # type: Optional[Union[MarkDecorator, Iterable[MarkDecorator]]] 

332 ): 

333 # type: (...) -> Tuple[MarkDecorator, ...] 

334 """ 

335 Internal routine used to normalize marks received from users in a `marks=` parameter 

336 

337 :param marks: 

338 :return: 

339 """ 

340 if marks is None: 

341 return () 

342 

343 try: 

344 # iterable ? 

345 return tuple(marks) 

346 except TypeError: 

347 # single 

348 return (marks,) 

349 

350 

351def markdecorators_to_markinfos(marks # type: Sequence[MarkDecorator] 

352 ): 

353 # type: (...) -> Tuple[Mark, ...] 

354 if PYTEST3_OR_GREATER: 354 ↛ 356line 354 didn't jump to line 356, because the condition on line 354 was never false

355 return tuple(m.mark for m in marks) 

356 elif len(marks) == 0: 

357 return () 

358 else: 

359 return tuple(Mark(m.name, m.args, m.kwargs) for m in marks)