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

168 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2024-09-26 21:52 +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') 

47PYTEST81_OR_GREATER = PYTEST_VERSION >= Version('8.1.0') 

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

49 

50 

51def get_param_argnames_as_list(argnames): 

52 """ 

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

54 This function makes sure that we always return a list 

55 :param argnames: 

56 :return: 

57 """ 

58 if isinstance(argnames, string_types): 

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

60 return list(argnames) 

61 

62 

63# noinspection PyUnusedLocal 

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

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

66 pass 

67 

68 

69def get_parametrize_signature(): 

70 """ 

71 

72 :return: a reference signature representing 

73 """ 

74 return signature(_pytest_mark_parametrize) 

75 

76 

77class _ParametrizationMark: 

78 """ 

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

80 

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

82 """ 

83 __slots__ = "param_names", "param_values", "param_ids" 

84 

85 def __init__(self, mark): 

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

87 try: 

88 remaining_kwargs = bound.arguments['kwargs'] 

89 except KeyError: 

90 pass 

91 else: 

92 if len(remaining_kwargs) > 0: 

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

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

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

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

97 try: 

98 bound.apply_defaults() 

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

100 except AttributeError: 

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

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

103 

104 

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

106class _LegacyMark: 

107 __slots__ = "args", "kwargs" 

108 

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

110 self.args = args 

111 self.kwargs = kwargs 

112 

113 

114# ---------------- working on functions 

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

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

117 from_marks = get_pytest_marks_on_function(from_f) 

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

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

120 to_f.pytestmark = to_marks + from_marks 

121 

122 

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

124 remove # type: str 

125 ): 

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

127 """ 

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

129 

130 :param marks: 

131 :param remove: 

132 :return: 

133 """ 

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

135 

136 

137def get_pytest_marks_on_function(f, 

138 as_decorators=False # type: bool 

139 ): 

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

141 """ 

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

143 Note that this also works on classes 

144 

145 :param f: 

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

147 :return: 

148 """ 

149 try: 

150 mks = f.pytestmark 

151 except AttributeError: 

152 try: 

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

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

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

156 except AttributeError: 

157 return [] 

158 

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

160 if as_decorators: 

161 return markinfos_to_markdecorators(mks, function_marks=True) 

162 else: 

163 return mks 

164 

165 

166def get_pytest_marks_on_item(item): 

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

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

169 return item.callspec.marks 

170 else: 

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

172 

173 

174def get_pytest_usefixture_marks(f): 

175 # pytest > 3.2.0 

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

177 if marks is not None: 

178 return tuple(itertools.chain.from_iterable( 

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

180 )) 

181 else: 

182 # older versions 

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

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

185 return mark_info.args 

186 else: 

187 return () 

188 

189 

190def remove_pytest_mark(f, mark_name): 

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

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

193 # pytest > 3.2.0 

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

195 f.pytestmark = new_marks 

196 else: 

197 # older versions 

198 try: 

199 delattr(f, mark_name) 

200 except AttributeError: 

201 pass 

202 return f 

203 

204 

205def get_pytest_parametrize_marks( 

206 f, 

207 pop=False # type: bool 

208): 

209 """ 

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

211 

212 :param f: 

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

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

215 """ 

216 # pytest > 3.2.0 

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

218 if marks is not None: 

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

220 delattr(f, 'pytestmark') 

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

222 else: 

223 # older versions 

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

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

226 if pop: 

227 delattr(f, 'parametrize') 

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

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

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

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

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

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

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

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

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

237 % mark_info.kwargs) 

238 res = [] 

239 for i in range(nb_parametrize_decorations): 

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

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

242 return tuple(res) 

243 else: 

244 return () 

245 

246 

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

248 

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

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

251 

252# check if pytest.param exists 

253has_pytest_param = hasattr(pytest, 'param') 

254 

255 

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

257 # if not this is how it was done 

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

259 def make_marked_parameter_value(argvalues_tuple, marks): 

260 if len(marks) > 1: 

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

262 else: 

263 if not isinstance(argvalues_tuple, tuple): 

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

265 

266 # get a decorator for each of the markinfo 

267 marks_mod = markinfos_to_markdecorators(marks, function_marks=False) 

268 

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

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

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

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

273else: 

274 # Otherwise pytest.param exists, it is easier 

275 def make_marked_parameter_value(argvalues_tuple, marks): 

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

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

278 

279 # get a decorator for each of the markinfo 

280 marks_mod = markinfos_to_markdecorators(marks, function_marks=False) 

281 

282 # decorate 

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

284 

285 

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

287 function_marks=False # type: bool 

288 ): 

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

290 """ 

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

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

293 

294 Returns a list. 

295 

296 :param marks: 

297 :param function_marks: 

298 :return: 

299 """ 

300 marks_mod = [] 

301 try: 

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

303 with warnings.catch_warnings(): 

304 warnings.simplefilter("ignore") 

305 for m in marks: 

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

307 if isinstance(m, MarkDecorator): 

308 # already a decorator, we can use it 

309 marks_mod.append(m) 

310 else: 

311 md = MarkDecorator(m) 

312 marks_mod.append(md) 

313 else: 

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

315 md = MarkDecorator() 

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

317 md.name = m.name 

318 

319 if function_marks: 

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

321 else: 

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

323 md.kwargs = m.kwargs 

324 

325 marks_mod.append(md) 

326 

327 except Exception as e: 

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

329 return marks_mod 

330 

331 

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

333 ): 

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

335 """ 

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

337 

338 :param marks: 

339 :return: 

340 """ 

341 if marks is None: 

342 return () 

343 

344 try: 

345 # iterable ? 

346 return tuple(marks) 

347 except TypeError: 

348 # single 

349 return (marks,) 

350 

351 

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

353 ): 

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

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

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

357 elif len(marks) == 0: 

358 return () 

359 else: 

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