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
« 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
7import warnings
8from packaging.version import Version
10try: # python 3.3+
11 from inspect import signature
12except ImportError:
13 from funcsigs import signature # noqa
15try:
16 from typing import Iterable, Optional, Tuple, List, Set, Union, Sequence # noqa
17except ImportError:
18 pass
20import pytest
22try:
23 from _pytest.mark.structures import MarkDecorator, Mark # noqa
24except ImportError:
25 from _pytest.mark import MarkDecorator, MarkInfo as Mark # noqa
27from .common_mini_six import string_types
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')
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)
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
70def get_parametrize_signature():
71 """
73 :return: a reference signature representing
74 """
75 return signature(_pytest_mark_parametrize)
78class _ParametrizationMark:
79 """
80 Container for the mark information that we grab from the fixtures (`@fixture`)
82 Represents the information required by `@fixture` to work.
83 """
84 __slots__ = "param_names", "param_values", "param_ids"
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)
106# -------- tools to get the parametrization mark whatever the pytest version
107class _LegacyMark:
108 __slots__ = "args", "kwargs"
110 def __init__(self, *args, **kwargs):
111 self.args = args
112 self.kwargs = kwargs
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
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.
131 :param marks:
132 :param remove:
133 :return:
134 """
135 return tuple(m for m in marks if m.name != remove)
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
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 []
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
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))]
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 ()
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
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)
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 ()
248# ---- tools to reapply marks on test parameter values, whatever the pytest version ----
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
253# check if pytest.param exists
254has_pytest_param = hasattr(pytest, 'param')
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 !")
267 # get a decorator for each of the markinfo
268 marks_mod = markinfos_to_markdecorators(marks, function_marks=False)
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 !")
280 # get a decorator for each of the markinfo
281 marks_mod = markinfos_to_markdecorators(marks, function_marks=False)
283 # decorate
284 return pytest.param(*argvalues_tuple, marks=marks_mod)
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.
295 Returns a list.
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
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
326 marks_mod.append(md)
328 except Exception as e:
329 warnings.warn("Caught exception while trying to mark case: [%s] %s" % (type(e), e))
330 return marks_mod
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
339 :param marks:
340 :return:
341 """
342 if marks is None:
343 return ()
345 try:
346 # iterable ?
347 return tuple(marks)
348 except TypeError:
349 # single
350 return (marks,)
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)