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

169 statements  

« prev     ^ index     » next       coverage.py v7.13.4, created at 2026-03-02 23:01 +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') 

48PYTEST84_OR_GREATER = PYTEST_VERSION >= Version('8.4.0') 

49PYTEST9_OR_GREATER = PYTEST_VERSION >= Version('9.0.0') 

50 

51 

52def get_param_argnames_as_list(argnames): 

53 """ 

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

55 This function makes sure that we always return a list 

56 :param argnames: 

57 :return: 

58 """ 

59 if isinstance(argnames, string_types): 

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

61 return list(argnames) 

62 

63 

64# noinspection PyUnusedLocal 

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

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

67 pass 

68 

69 

70def get_parametrize_signature(): 

71 """ 

72 

73 :return: a reference signature representing 

74 """ 

75 return signature(_pytest_mark_parametrize) 

76 

77 

78class _ParametrizationMark: 

79 """ 

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

81 

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

83 """ 

84 __slots__ = "param_names", "param_values", "param_ids" 

85 

86 def __init__(self, mark): 

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

88 try: 

89 remaining_kwargs = bound.arguments['kwargs'] 

90 except KeyError: 

91 pass 

92 else: 

93 if len(remaining_kwargs) > 0: 

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

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

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

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

98 try: 

99 bound.apply_defaults() 

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

101 except AttributeError: 

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

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

104 

105 

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

107class _LegacyMark: 

108 __slots__ = "args", "kwargs" 

109 

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

111 self.args = args 

112 self.kwargs = kwargs 

113 

114 

115# ---------------- working on functions 

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

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

118 from_marks = get_pytest_marks_on_function(from_f) 

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

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

121 to_f.pytestmark = to_marks + from_marks 

122 

123 

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

125 remove # type: str 

126 ): 

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

128 """ 

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

130 

131 :param marks: 

132 :param remove: 

133 :return: 

134 """ 

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

136 

137 

138def get_pytest_marks_on_function(f, 

139 as_decorators=False # type: bool 

140 ): 

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

142 """ 

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

144 Note that this also works on classes 

145 

146 :param f: 

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

148 :return: 

149 """ 

150 try: 

151 mks = f.pytestmark 

152 except AttributeError: 

153 try: 

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

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

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

157 except AttributeError: 

158 return [] 

159 

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

161 if as_decorators: 

162 return markinfos_to_markdecorators(mks, function_marks=True) 

163 else: 

164 return mks 

165 

166 

167def get_pytest_marks_on_item(item): 

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

169 if PYTEST3_OR_GREATER: 169 ↛ 172line 169 didn't jump to line 172 because the condition on line 169 was always true

170 return item.callspec.marks 

171 else: 

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

173 

174 

175def get_pytest_usefixture_marks(f): 

176 # pytest > 3.2.0 

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

178 if marks is not None: 

179 return tuple(itertools.chain.from_iterable( 

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

181 )) 

182 else: 

183 # older versions 

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

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

186 return mark_info.args 

187 else: 

188 return () 

189 

190 

191def remove_pytest_mark(f, mark_name): 

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

193 if marks is not None: 193 ↛ 199line 193 didn't jump to line 199 because the condition on line 193 was always true

194 # pytest > 3.2.0 

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

196 f.pytestmark = new_marks 

197 else: 

198 # older versions 

199 try: 

200 delattr(f, mark_name) 

201 except AttributeError: 

202 pass 

203 return f 

204 

205 

206def get_pytest_parametrize_marks( 

207 f, 

208 pop=False # type: bool 

209): 

210 """ 

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

212 

213 :param f: 

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

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

216 """ 

217 # pytest > 3.2.0 

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

219 if marks is not None: 

220 if pop: 

221 delattr(f, 'pytestmark') 

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

223 else: 

224 # older versions 

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

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

227 if pop: 

228 delattr(f, 'parametrize') 

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

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

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

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

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

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

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

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

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

238 % mark_info.kwargs) 

239 res = [] 

240 for i in range(nb_parametrize_decorations): 

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

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

243 return tuple(res) 

244 else: 

245 return () 

246 

247 

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

249 

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

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

252 

253# check if pytest.param exists 

254has_pytest_param = hasattr(pytest, 'param') 

255 

256 

257if not has_pytest_param: 257 ↛ 260line 257 didn't jump to line 260 because the condition on line 257 was never true

258 # if not this is how it was done 

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

260 def make_marked_parameter_value(argvalues_tuple, marks): 

261 if len(marks) > 1: 

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

263 else: 

264 if not isinstance(argvalues_tuple, tuple): 

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

266 

267 # get a decorator for each of the markinfo 

268 marks_mod = markinfos_to_markdecorators(marks, function_marks=False) 

269 

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

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

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

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

274else: 

275 # Otherwise pytest.param exists, it is easier 

276 def make_marked_parameter_value(argvalues_tuple, marks): 

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

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

279 

280 # get a decorator for each of the markinfo 

281 marks_mod = markinfos_to_markdecorators(marks, function_marks=False) 

282 

283 # decorate 

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

285 

286 

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

288 function_marks=False # type: bool 

289 ): 

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

291 """ 

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

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

294 

295 Returns a list. 

296 

297 :param marks: 

298 :param function_marks: 

299 :return: 

300 """ 

301 marks_mod = [] 

302 try: 

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

304 with warnings.catch_warnings(): 

305 warnings.simplefilter("ignore") 

306 for m in marks: 

307 if PYTEST3_OR_GREATER: 307 ↛ 316line 307 didn't jump to line 316 because the condition on line 307 was always true

308 if isinstance(m, MarkDecorator): 

309 # already a decorator, we can use it 

310 marks_mod.append(m) 

311 else: 

312 md = MarkDecorator(m) 

313 marks_mod.append(md) 

314 else: 

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

316 md = MarkDecorator() 

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

318 md.name = m.name 

319 

320 if function_marks: 

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

322 else: 

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

324 md.kwargs = m.kwargs 

325 

326 marks_mod.append(md) 

327 

328 except Exception as e: 

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

330 return marks_mod 

331 

332 

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

334 ): 

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

336 """ 

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

338 

339 :param marks: 

340 :return: 

341 """ 

342 if marks is None: 

343 return () 

344 

345 try: 

346 # iterable ? 

347 return tuple(marks) 

348 except TypeError: 

349 # single 

350 return (marks,) 

351 

352 

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

354 ): 

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

356 if PYTEST3_OR_GREATER: 356 ↛ 358line 356 didn't jump to line 358 because the condition on line 356 was always true

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

358 elif len(marks) == 0: 

359 return () 

360 else: 

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