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