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
« 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
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')
48PYTEST811 = PYTEST_VERSION == Version('8.1.1')
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)
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
69def get_parametrize_signature():
70 """
72 :return: a reference signature representing
73 """
74 return signature(_pytest_mark_parametrize)
77class _ParametrizationMark:
78 """
79 Container for the mark information that we grab from the fixtures (`@fixture`)
81 Represents the information required by `@fixture` to work.
82 """
83 __slots__ = "param_names", "param_values", "param_ids"
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)
105# -------- tools to get the parametrization mark whatever the pytest version
106class _LegacyMark:
107 __slots__ = "args", "kwargs"
109 def __init__(self, *args, **kwargs):
110 self.args = args
111 self.kwargs = kwargs
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
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.
130 :param marks:
131 :param remove:
132 :return:
133 """
134 return tuple(m for m in marks if m.name != remove)
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
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 []
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
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))]
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 ()
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
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)
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 ()
247# ---- tools to reapply marks on test parameter values, whatever the pytest version ----
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
252# check if pytest.param exists
253has_pytest_param = hasattr(pytest, 'param')
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 !")
266 # get a decorator for each of the markinfo
267 marks_mod = markinfos_to_markdecorators(marks, function_marks=False)
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 !")
279 # get a decorator for each of the markinfo
280 marks_mod = markinfos_to_markdecorators(marks, function_marks=False)
282 # decorate
283 return pytest.param(*argvalues_tuple, marks=marks_mod)
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.
294 Returns a list.
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
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
325 marks_mod.append(md)
327 except Exception as e:
328 warnings.warn("Caught exception while trying to mark case: [%s] %s" % (type(e), e))
329 return marks_mod
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
338 :param marks:
339 :return:
340 """
341 if marks is None:
342 return ()
344 try:
345 # iterable ?
346 return tuple(marks)
347 except TypeError:
348 # single
349 return (marks,)
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)