Coverage for src/pytest_cases/common_pytest.py: 78%
321 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>
5from __future__ import division
7import inspect
8import sys
9import os
10from importlib import import_module
12from makefun import add_signature_parameters, wraps
14try: # python 3.3+
15 from inspect import signature, Parameter
16except ImportError:
17 from funcsigs import signature, Parameter # noqa
19from inspect import isgeneratorfunction, isclass
21try:
22 from typing import Union, Callable, Any, Optional, Tuple, Type, Iterable, Sized, List # noqa
23except ImportError:
24 pass
26import pytest
27from _pytest.python import Metafunc
29from .common_mini_six import string_types
30from .common_others import get_function_host
31from .common_pytest_marks import make_marked_parameter_value, get_param_argnames_as_list, \
32 get_pytest_parametrize_marks, get_pytest_usefixture_marks, PYTEST3_OR_GREATER, PYTEST6_OR_GREATER, \
33 PYTEST38_OR_GREATER, PYTEST34_OR_GREATER, PYTEST33_OR_GREATER, PYTEST32_OR_GREATER, PYTEST71_OR_GREATER, \
34 PYTEST8_OR_GREATER
35from .common_pytest_lazy_values import is_lazy_value, is_lazy
38# A decorator that will work to create a fixture containing 'yield', whatever the pytest version, and supports hooks
39if PYTEST3_OR_GREATER: 39 ↛ 50line 39 didn't jump to line 50, because the condition on line 39 was never false
40 def pytest_fixture(hook=None, **kwargs):
41 def _decorate(f):
42 # call hook if needed
43 if hook is not None:
44 f = hook(f)
46 # create the fixture
47 return pytest.fixture(**kwargs)(f)
48 return _decorate
49else:
50 def pytest_fixture(hook=None, name=None, **kwargs):
51 """Generator-aware pytest.fixture decorator for legacy pytest versions"""
52 def _decorate(f):
53 if name is not None:
54 # 'name' argument is not supported in this old version, use the __name__ trick.
55 f.__name__ = name
57 # call hook if needed
58 if hook is not None:
59 f = hook(f)
61 # create the fixture
62 if isgeneratorfunction(f):
63 return pytest.yield_fixture(**kwargs)(f)
64 else:
65 return pytest.fixture(**kwargs)(f)
66 return _decorate
69def pytest_is_running():
70 """Return True if the current process is a pytest run
72 See https://stackoverflow.com/questions/25188119/test-if-code-is-executed-from-within-a-py-test-session
73 """
74 if PYTEST32_OR_GREATER:
75 return "PYTEST_CURRENT_TEST" in os.environ
76 else:
77 import re
78 return any(re.findall(r'pytest|py.test', sys.argv[0]))
81def remove_duplicates(lst):
82 dset = set()
83 # relies on the fact that dset.add() always returns None.
84 return [item for item in lst
85 if item not in dset and not dset.add(item)]
88def is_fixture(fixture_fun # type: Any
89 ):
90 """
91 Returns True if the provided function is a fixture
93 :param fixture_fun:
94 :return:
95 """
96 try:
97 fixture_fun._pytestfixturefunction # noqa
98 return True
99 except AttributeError:
100 # not a fixture ?
101 return False
104def list_all_fixtures_in(cls_or_module, return_names=True, recurse_to_module=False):
105 """
106 Returns a list containing all fixture names (or symbols if `return_names=False`)
107 in the given class or module.
109 Note that `recurse_to_module` can be used so that the fixtures in the parent
110 module of a class are listed too.
112 :param cls_or_module:
113 :param return_names:
114 :param recurse_to_module:
115 :return:
116 """
117 res = [get_fixture_name(symb) if return_names else symb
118 for n, symb in inspect.getmembers(cls_or_module, lambda f: inspect.isfunction(f) or inspect.ismethod(f))
119 if is_fixture(symb)]
121 if recurse_to_module and not inspect.ismodule(cls_or_module): 121 ↛ 123line 121 didn't jump to line 123, because the condition on line 121 was never true
122 # TODO currently this only works for a single level of nesting, we should use __qualname__ (py3) or .im_class
123 host = import_module(cls_or_module.__module__)
124 res += list_all_fixtures_in(host, recurse_to_module=True, return_names=return_names)
126 return res
129def safe_isclass(obj # type: object
130 ):
131 # type: (...) -> bool
132 """Ignore any exception via isinstance on Python 3."""
133 try:
134 return isclass(obj)
135 except Exception: # noqa
136 return False
139def safe_isinstance(obj, # type: object
140 cls):
141 # type: (...) -> bool
142 """Ignore any exception via isinstance"""
143 try:
144 return isinstance(obj, cls)
145 except Exception: # noqa
146 return False
149def assert_is_fixture(fixture_fun # type: Any
150 ):
151 """
152 Raises a ValueError if the provided fixture function is not a fixture.
154 :param fixture_fun:
155 :return:
156 """
157 if not is_fixture(fixture_fun):
158 raise ValueError("The provided fixture function does not seem to be a fixture: %s. Did you properly decorate "
159 "it ?" % fixture_fun)
162def get_fixture_name(fixture_fun # type: Union[str, Callable]
163 ):
164 """
165 Internal utility to retrieve the fixture name corresponding to the given fixture function.
166 Indeed there is currently no pytest API to do this.
168 Note: this function can receive a string, in which case it is directly returned.
170 :param fixture_fun:
171 :return:
172 """
173 if isinstance(fixture_fun, string_types):
174 return fixture_fun
175 assert_is_fixture(fixture_fun)
176 try: # pytest 3
177 custom_fixture_name = fixture_fun._pytestfixturefunction.name # noqa
178 except AttributeError:
179 try: # pytest 2
180 custom_fixture_name = fixture_fun.func_name # noqa
181 except AttributeError:
182 custom_fixture_name = None
184 if custom_fixture_name is not None:
185 # there is a custom fixture name
186 return custom_fixture_name
187 else:
188 obj__name = getattr(fixture_fun, '__name__', None)
189 if obj__name is not None: 189 ↛ 194line 189 didn't jump to line 194, because the condition on line 189 was never false
190 # a function, probably
191 return obj__name
192 else:
193 # a callable object probably
194 return str(fixture_fun)
197def get_fixture_scope(fixture_fun):
198 """
199 Internal utility to retrieve the fixture scope corresponding to the given fixture function .
200 Indeed there is currently no pytest API to do this.
202 :param fixture_fun:
203 :return:
204 """
205 assert_is_fixture(fixture_fun)
206 return fixture_fun._pytestfixturefunction.scope # noqa
207 # except AttributeError:
208 # # pytest 2
209 # return fixture_fun.func_scope
212# ---------------- working on pytest nodes (e.g. Function)
214def is_function_node(node):
215 try:
216 node.function # noqa
217 return True
218 except AttributeError:
219 return False
222def get_parametrization_markers(fnode):
223 """
224 Returns the parametrization marks on a pytest Function node.
225 :param fnode:
226 :return:
227 """
228 if PYTEST34_OR_GREATER: 228 ↛ 231line 228 didn't jump to line 231, because the condition on line 228 was never false
229 return list(fnode.iter_markers(name="parametrize"))
230 else:
231 return list(fnode.parametrize)
234def get_param_names(fnode):
235 """
236 Returns a list of parameter names for the given pytest Function node.
237 parameterization marks containing several names are split
239 :param fnode:
240 :return:
241 """
242 p_markers = get_parametrization_markers(fnode)
243 param_names = []
244 for paramz_mark in p_markers:
245 argnames = paramz_mark.args[0] if len(paramz_mark.args) > 0 else paramz_mark.kwargs['argnames']
246 param_names += get_param_argnames_as_list(argnames)
247 return param_names
250# ---------- test ids utils ---------
251def combine_ids(paramid_tuples):
252 """
253 Receives a list of tuples containing ids for each parameterset.
254 Returns the final ids, that are obtained by joining the various param ids by '-' for each test node
256 :param paramid_tuples:
257 :return:
258 """
259 #
260 return ['-'.join(pid for pid in testid) for testid in paramid_tuples]
263def make_test_ids(global_ids, id_marks, argnames=None, argvalues=None, precomputed_ids=None):
264 """
265 Creates the proper id for each test based on (higher precedence first)
267 - any specific id mark from a `pytest.param` (`id_marks`)
268 - the global `ids` argument of pytest parametrize (`global_ids`)
269 - the name and value of parameters (`argnames`, `argvalues`) or the precomputed ids(`precomputed_ids`)
271 See also _pytest.python._idvalset method
273 :param global_ids:
274 :param id_marks:
275 :param argnames:
276 :param argvalues:
277 :param precomputed_ids:
278 :return:
279 """
280 if global_ids is not None:
281 # overridden at global pytest.mark.parametrize level - this takes precedence.
282 # resolve possibly infinite generators of ids here
283 p_ids = resolve_ids(global_ids, argvalues, full_resolve=True)
284 else:
285 # default: values-based
286 if precomputed_ids is not None: 286 ↛ 287line 286 didn't jump to line 287, because the condition on line 286 was never true
287 if argnames is not None or argvalues is not None:
288 raise ValueError("Only one of `precomputed_ids` or argnames/argvalues should be provided.")
289 p_ids = precomputed_ids
290 else:
291 p_ids = make_test_ids_from_param_values(argnames, argvalues)
293 # Finally, local pytest.param takes precedence over everything else
294 for i, _id in enumerate(id_marks):
295 if _id is not None:
296 p_ids[i] = _id
297 return p_ids
300def resolve_ids(ids, # type: Optional[Union[Callable, Iterable[str]]]
301 argvalues, # type: Sized(Any)
302 full_resolve=False # type: bool
303 ):
304 # type: (...) -> Union[List[str], Callable]
305 """
306 Resolves the `ids` argument of a parametrized fixture.
308 If `full_resolve` is False (default), iterable ids will be resolved, but not callable ids. This is useful if the
309 `argvalues` have not yet been cleaned of possible `pytest.param` wrappers.
311 If `full_resolve` is True, callable ids will be called using the argvalues, so the result is guaranteed to be a
312 list.
313 """
314 try:
315 # an explicit list or generator of ids ?
316 iter(ids)
317 except TypeError:
318 # a callable to apply on the values
319 if full_resolve:
320 return [ids(v) for v in argvalues]
321 else:
322 # return the callable without resolving
323 return ids
324 else:
325 # iterable.
326 try:
327 # a sized container ? (list, set, tuple)
328 nb_ids = len(ids)
329 # convert to list
330 ids = list(ids)
331 except TypeError:
332 # a generator. Consume it
333 ids = [id for id, v in zip(ids, argvalues)]
334 nb_ids = len(ids)
336 if nb_ids != len(argvalues): 336 ↛ 337line 336 didn't jump to line 337, because the condition on line 336 was never true
337 raise ValueError("Explicit list or generator of `ids` provided has a different length (%s) than the number "
338 "of argvalues (%s). Ids provided: %r" % (len(ids), len(argvalues), ids))
339 return ids
342def make_test_ids_from_param_values(param_names,
343 param_values,
344 ):
345 """
346 Replicates pytest behaviour to generate the ids when there are several parameters in a single `parametrize.
347 Note that param_values should not contain marks.
349 :param param_names:
350 :param param_values:
351 :return: a list of param ids
352 """
353 if isinstance(param_names, string_types): 353 ↛ 354line 353 didn't jump to line 354, because the condition on line 353 was never true
354 raise TypeError("param_names must be an iterable. Found %r" % param_names)
356 nb_params = len(param_names)
357 if nb_params == 0: 357 ↛ 358line 357 didn't jump to line 358, because the condition on line 357 was never true
358 raise ValueError("empty list provided")
359 elif nb_params == 1:
360 paramids = []
361 for _idx, v in enumerate(param_values):
362 _id = mini_idvalset(param_names, (v,), _idx)
363 paramids.append(_id)
364 else:
365 paramids = []
366 for _idx, vv in enumerate(param_values):
367 if len(vv) != nb_params: 367 ↛ 368line 367 didn't jump to line 368, because the condition on line 367 was never true
368 raise ValueError("Inconsistent lengths for parameter names and values: '%s' and '%s'"
369 "" % (param_names, vv))
370 _id = mini_idvalset(param_names, vv, _idx)
371 paramids.append(_id)
372 return paramids
375# ---- ParameterSet api ---
376# def analyze_parameter_set(pmark=None, argnames=None, argvalues=None, ids=None, check_nb=True):
377# """
378# analyzes a parameter set passed either as a pmark or as distinct
379# (argnames, argvalues, ids) to extract/construct the various ids, marks, and
380# values
381#
382# See also pytest.Metafunc.parametrize method, that calls in particular
383# pytest.ParameterSet._for_parametrize and _pytest.python._idvalset
384#
385# :param pmark:
386# :param argnames:
387# :param argvalues:
388# :param ids:
389# :param check_nb: a bool indicating if we should raise an error if len(argnames) > 1 and any argvalue has
390# a different length than len(argnames)
391# :return: ids, marks, values
392# """
393# if pmark is not None:
394# if any(a is not None for a in (argnames, argvalues, ids)):
395# raise ValueError("Either provide a pmark OR the details")
396# argnames = pmark.param_names
397# argvalues = pmark.param_values
398# ids = pmark.param_ids
399#
400# # extract all parameters that have a specific configuration (pytest.param())
401# custom_pids, p_marks, p_values = extract_parameterset_info(argnames, argvalues, check_nb=check_nb)
402#
403# # get the ids by merging/creating the various possibilities
404# p_ids = make_test_ids(argnames=argnames, argvalues=p_values, global_ids=ids, id_marks=custom_pids)
405#
406# return p_ids, p_marks, p_values
409def extract_parameterset_info(argnames, argvalues, check_nb=True):
410 """
412 :param argnames: the names in this parameterset
413 :param argvalues: the values in this parameterset
414 :param check_nb: a bool indicating if we should raise an error if len(argnames) > 1 and any argvalue has
415 a different length than len(argnames)
416 :return:
417 """
418 pids = []
419 pmarks = []
420 pvalues = []
421 if isinstance(argnames, string_types): 421 ↛ 422line 421 didn't jump to line 422, because the condition on line 421 was never true
422 raise TypeError("argnames must be an iterable. Found %r" % argnames)
423 nbnames = len(argnames)
424 for v in argvalues:
425 _pid, _pmark, _pvalue = extract_pset_info_single(nbnames, v)
427 pids.append(_pid)
428 pmarks.append(_pmark)
429 pvalues.append(_pvalue)
431 if check_nb and nbnames > 1 and (len(_pvalue) != nbnames): 431 ↛ 432line 431 didn't jump to line 432, because the condition on line 431 was never true
432 raise ValueError("Inconsistent number of values in pytest parametrize: %s items found while the "
433 "number of parameters is %s: %s." % (len(_pvalue), nbnames, _pvalue))
435 return pids, pmarks, pvalues
438def extract_pset_info_single(nbnames, argvalue):
439 """Return id, marks, value"""
440 if is_marked_parameter_value(argvalue):
441 # --id
442 _id = get_marked_parameter_id(argvalue)
443 # --marks
444 marks = get_marked_parameter_marks(argvalue)
445 # --value(a tuple if this is a tuple parameter)
446 argvalue = get_marked_parameter_values(argvalue, nbargs=nbnames)
447 return _id, marks, argvalue[0] if nbnames == 1 else argvalue
448 else:
449 # normal argvalue
450 return None, None, argvalue
453try: # pytest 3.x+
454 from _pytest.mark import ParameterSet # noqa
456 def is_marked_parameter_value(v):
457 return isinstance(v, ParameterSet)
459 def get_marked_parameter_marks(v):
460 return v.marks
462 def get_marked_parameter_values(v, nbargs):
463 """This always returns a tuple. nbargs is useful for pytest2 compatibility """
464 return v.values
466 def get_marked_parameter_id(v):
467 return v.id
469except ImportError: # pytest 2.x
470 from _pytest.mark import MarkDecorator
472 # noinspection PyPep8Naming
473 def ParameterSet(values,
474 id, # noqa
475 marks):
476 """ Dummy function (not a class) used only by `parametrize` """
477 if id is not None:
478 raise ValueError("This should not happen as `pytest.param` does not exist in pytest 2")
480 # smart unpack is required for compatibility
481 val = values[0] if len(values) == 1 else values
482 nbmarks = len(marks)
484 if nbmarks == 0:
485 return val
486 elif nbmarks > 1:
487 raise ValueError("Multiple marks on parameters not supported for old versions of pytest")
488 else:
489 # decorate with the MarkDecorator
490 return marks[0](val)
492 def is_marked_parameter_value(v):
493 return isinstance(v, MarkDecorator)
495 def get_marked_parameter_marks(v):
496 return [v]
498 def get_marked_parameter_values(v, nbargs):
499 """Returns a tuple containing the values"""
501 # v.args[-1] contains the values.
502 # see MetaFunc.parametrize in pytest 2 to be convinced :)
504 # if v.name in ('skip', 'skipif'):
505 if nbargs == 1:
506 # the last element of args is not a tuple when there is a single arg.
507 return (v.args[-1],)
508 else:
509 return v.args[-1]
510 # else:
511 # raise ValueError("Unsupported mark")
513 def get_marked_parameter_id(v):
514 return v.kwargs.get('id', None)
517def get_pytest_nodeid(metafunc):
518 try:
519 return metafunc.definition.nodeid
520 except AttributeError:
521 return "unknown"
524try:
525 # pytest 7+ : scopes is an enum
526 from _pytest.scope import Scope
528 def get_pytest_function_scopeval():
529 return Scope.Function
531 def has_function_scope(fixdef):
532 return fixdef._scope is Scope.Function
534 def set_callspec_arg_scope_to_function(callspec, arg_name):
535 callspec._arg2scope[arg_name] = Scope.Function
537except ImportError:
538 try:
539 # pytest 3+
540 from _pytest.fixtures import scopes as pt_scopes
541 except ImportError:
542 # pytest 2
543 from _pytest.python import scopes as pt_scopes
545 # def get_pytest_scopenum(scope_str):
546 # return pt_scopes.index(scope_str)
548 def get_pytest_function_scopeval():
549 return pt_scopes.index("function")
551 def has_function_scope(fixdef):
552 return fixdef.scopenum == get_pytest_function_scopeval()
554 def set_callspec_arg_scope_to_function(callspec, arg_name):
555 callspec._arg2scopenum[arg_name] = get_pytest_function_scopeval() # noqa
558def in_callspec_explicit_args(
559 callspec, # type: CallSpec2
560 name # type: str
561): # type: (...) -> bool
562 """Return True if name is explicitly used in callspec args"""
563 return (name in callspec.params) or (not PYTEST8_OR_GREATER and name in callspec.funcargs)
566if PYTEST71_OR_GREATER: 566 ↛ 572line 566 didn't jump to line 572, because the condition on line 566 was never false
567 from _pytest.python import IdMaker # noqa
569 _idval = IdMaker([], [], None, None, None, None, None)._idval
570 _idval_kwargs = dict()
571else:
572 from _pytest.python import _idval # noqa
574 if PYTEST6_OR_GREATER:
575 _idval_kwargs = dict(idfn=None,
576 nodeid=None, # item is not used in pytest(>=6.0.0) nodeid is only used by idfn
577 config=None # if a config hook was available it would be used before this is called)
578 )
579 elif PYTEST38_OR_GREATER:
580 _idval_kwargs = dict(idfn=None,
581 item=None, # item is only used by idfn
582 config=None # if a config hook was available it would be used before this is called)
583 )
584 else:
585 _idval_kwargs = dict(idfn=None,
586 # config=None # if a config hook was available it would be used before this is called)
587 )
590def mini_idval(
591 val, # type: object
592 argname, # type: str
593 idx, # type: int
594):
595 """
596 A simplified version of idval where idfn, item and config do not need to be passed.
598 :param val:
599 :param argname:
600 :param idx:
601 :return:
602 """
603 return _idval(val=val, argname=argname, idx=idx, **_idval_kwargs)
606def mini_idvalset(argnames, argvalues, idx):
607 """ mimic _pytest.python._idvalset but can handle lazyvalues used for tuples or args
609 argvalues should not be a pytest.param (ParameterSet)
610 This function returns a SINGLE id for a single test node
611 """
612 if len(argnames) > 1 and is_lazy(argvalues):
613 # handle the case of LazyTuple used for several args
614 return argvalues.get_id()
616 this_id = [
617 _idval(val, argname, idx=idx, **_idval_kwargs)
618 for val, argname in zip(argvalues, argnames)
619 ]
620 return "-".join(this_id)
623try:
624 from _pytest.compat import getfuncargnames # noqa
625except ImportError:
626 def num_mock_patch_args(function):
627 """ return number of arguments used up by mock arguments (if any) """
628 patchings = getattr(function, "patchings", None)
629 if not patchings:
630 return 0
632 mock_sentinel = getattr(sys.modules.get("mock"), "DEFAULT", object())
633 ut_mock_sentinel = getattr(sys.modules.get("unittest.mock"), "DEFAULT", object())
635 return len(
636 [p for p in patchings if not p.attribute_name and (p.new is mock_sentinel or p.new is ut_mock_sentinel)]
637 )
639 # noinspection SpellCheckingInspection
640 def getfuncargnames(function, cls=None):
641 """Returns the names of a function's mandatory arguments."""
642 parameters = signature(function).parameters
644 arg_names = tuple(
645 p.name
646 for p in parameters.values()
647 if (
648 p.kind is Parameter.POSITIONAL_OR_KEYWORD
649 or p.kind is Parameter.KEYWORD_ONLY
650 )
651 and p.default is Parameter.empty
652 )
654 # If this function should be treated as a bound method even though
655 # it's passed as an unbound method or function, remove the first
656 # parameter name.
657 if cls and not isinstance(cls.__dict__.get(function.__name__, None), staticmethod):
658 arg_names = arg_names[1:]
659 # Remove any names that will be replaced with mocks.
660 if hasattr(function, "__wrapped__"):
661 arg_names = arg_names[num_mock_patch_args(function):]
662 return arg_names
665class FakeSession(object):
666 __slots__ = ('_fixturemanager',)
668 def __init__(self):
669 self._fixturemanager = None
672class MiniFuncDef(object):
673 __slots__ = ('nodeid', 'session')
675 def __init__(self, nodeid):
676 self.nodeid = nodeid
677 if PYTEST8_OR_GREATER: 677 ↛ 678line 677 didn't jump to line 678, because the condition on line 677 was never true
678 self.session = FakeSession()
681class MiniMetafunc(Metafunc):
682 """
683 A class to know what pytest *would* do for a given function in terms of callspec.
684 It is used in function `case_to_argvalues`
685 """
686 # noinspection PyMissingConstructor
687 def __init__(self, func):
688 from .plugin import PYTEST_CONFIG # late import to ensure config has been loaded by now
690 self.config = PYTEST_CONFIG
692 # self.config can be `None` if the same module is reloaded by another thread/process inside a test (parallelism)
693 # In that case, a priori we are outside the pytest main runner so we can silently ignore, this
694 # MetaFunc will not be used/read by anyone.
695 # See https://github.com/smarie/python-pytest-cases/issues/242
696 #
697 # if self.config is None:
698 # if pytest_is_running():
699 # raise ValueError("Internal error - config has not been correctly loaded. Please report")
701 self.function = func
702 self.definition = MiniFuncDef(func.__name__)
703 self._calls = []
704 # non-default parameters
705 self.fixturenames = getfuncargnames(func)
706 # add declared used fixtures with @pytest.mark.usefixtures
707 self.fixturenames_not_in_sig = [f for f in get_pytest_usefixture_marks(func) if f not in self.fixturenames]
708 if self.fixturenames_not_in_sig:
709 self.fixturenames = tuple(self.fixturenames_not_in_sig + list(self.fixturenames))
711 if PYTEST8_OR_GREATER: 711 ↛ 713line 711 didn't jump to line 713, because the condition on line 711 was never true
712 # dummy
713 self._arg2fixturedefs = dict() # type: dict[str, Sequence["FixtureDef[Any]"]]
715 # get parametrization marks
716 self.pmarks = get_pytest_parametrize_marks(self.function)
717 if self.is_parametrized:
718 self.update_callspecs()
719 # preserve order
720 ref_names = self._calls[0].params if PYTEST8_OR_GREATER else self._calls[0].funcargs
721 self.required_fixtures = tuple(f for f in self.fixturenames if f not in ref_names)
722 else:
723 self.required_fixtures = self.fixturenames
725 @property
726 def is_parametrized(self):
727 return len(self.pmarks) > 0
729 @property
730 def requires_fixtures(self):
731 return len(self.required_fixtures) > 0
733 def update_callspecs(self):
734 """
736 :return:
737 """
738 for pmark in self.pmarks:
739 if len(pmark.param_names) == 1:
740 if PYTEST3_OR_GREATER: 740 ↛ 743line 740 didn't jump to line 743, because the condition on line 740 was never false
741 argvals = tuple(v if is_marked_parameter_value(v) else (v,) for v in pmark.param_values)
742 else:
743 argvals = []
744 for v in pmark.param_values:
745 if is_marked_parameter_value(v):
746 newmark = MarkDecorator(v.markname, v.args[:-1] + ((v.args[-1],),), v.kwargs)
747 argvals.append(newmark)
748 else:
749 argvals.append((v,))
750 argvals = tuple(argvals)
751 else:
752 argvals = pmark.param_values
753 self.parametrize(argnames=pmark.param_names, argvalues=argvals, ids=pmark.param_ids,
754 # use indirect = False and scope = 'function' to avoid having to implement complex patches
755 indirect=False, scope='function')
757 if not PYTEST33_OR_GREATER: 757 ↛ 760line 757 didn't jump to line 760, because the condition on line 757 was never true
758 # fix the CallSpec2 instances so that the marks appear in an attribute "mark"
759 # noinspection PyProtectedMember
760 for c in self._calls:
761 c.marks = list(c.keywords.values())
764def add_fixture_params(func, new_names):
765 """Creates a wrapper of the given function with additional arguments"""
767 old_sig = signature(func)
769 # prepend all new parameters if needed
770 for n in new_names:
771 if n in old_sig.parameters: 771 ↛ 772line 771 didn't jump to line 772, because the condition on line 771 was never true
772 raise ValueError("argument named %s already present in signature" % n)
773 new_sig = add_signature_parameters(old_sig,
774 first=[Parameter(n, kind=Parameter.POSITIONAL_OR_KEYWORD) for n in new_names])
776 assert not isgeneratorfunction(func)
778 # normal function with return statement
779 @wraps(func, new_sig=new_sig)
780 def wrapped_func(**kwargs):
781 for n in new_names:
782 kwargs.pop(n)
783 return func(**kwargs)
785 # else:
786 # # generator function (with a yield statement)
787 # @wraps(fixture_func, new_sig=new_sig)
788 # def wrapped_fixture_func(*args, **kwargs):
789 # request = kwargs['request'] if func_needs_request else kwargs.pop('request')
790 # if is_used_request(request):
791 # for res in fixture_func(*args, **kwargs):
792 # yield res
793 # else:
794 # yield NOT_USED
796 return wrapped_func
799def get_callspecs(func):
800 """
801 Returns a list of pytest CallSpec objects corresponding to calls that should be made for this parametrized function.
802 This mini-helper assumes no complex things (scope='function', indirect=False, no fixtures, no custom configuration)
804 Note that this function is currently only used in tests.
805 """
806 meta = MiniMetafunc(func)
807 # meta.update_callspecs()
808 # noinspection PyProtectedMember
809 return meta._calls
812def cart_product_pytest(argnames, argvalues):
813 """
814 - do NOT use `itertools.product` as it fails to handle MarkDecorators
815 - we also unpack tuples associated with several argnames ("a,b") if needed
816 - we also propagate marks
818 :param argnames:
819 :param argvalues:
820 :return:
821 """
822 # transform argnames into a list of lists
823 argnames_lists = [get_param_argnames_as_list(_argnames) if len(_argnames) > 0 else [] for _argnames in argnames]
825 # make the cartesian product per se
826 argvalues_prod = _cart_product_pytest(argnames_lists, argvalues)
828 # flatten the list of argnames
829 argnames_list = [n for nlist in argnames_lists for n in nlist]
831 # apply all marks to the arvalues
832 argvalues_prod = [make_marked_parameter_value(tuple(argvalues), marks=marks) if len(marks) > 0 else tuple(argvalues)
833 for marks, argvalues in argvalues_prod]
835 return argnames_list, argvalues_prod
838def _cart_product_pytest(argnames_lists, argvalues):
839 result = []
841 # first perform the sub cartesian product with entries [1:]
842 sub_product = _cart_product_pytest(argnames_lists[1:], argvalues[1:]) if len(argvalues) > 1 else None
844 # then do the final product with entry [0]
845 for x in argvalues[0]:
846 # handle x
847 nb_names = len(argnames_lists[0])
849 # (1) extract meta-info
850 x_id, x_marks, x_value = extract_pset_info_single(nb_names, x)
851 x_marks_lst = list(x_marks) if x_marks is not None else []
852 if x_id is not None: 852 ↛ 853line 852 didn't jump to line 853, because the condition on line 852 was never true
853 raise ValueError("It is not possible to specify a sub-param id when using the new parametrization style. "
854 "Either use the traditional style or customize all ids at once in `idgen`")
856 # (2) possibly unpack
857 if nb_names > 1:
858 # if lazy value, we have to do something
859 if is_lazy_value(x_value):
860 x_value_lst = x_value.as_lazy_items_list(nb_names)
861 else:
862 x_value_lst = list(x_value)
863 else:
864 x_value_lst = [x_value]
866 # product
867 if len(argvalues) > 1:
868 for m, p in sub_product:
869 # combine marks and values
870 result.append((x_marks_lst + m, x_value_lst + p))
871 else:
872 result.append((x_marks_lst, x_value_lst))
874 return result
877def inject_host(apply_decorator):
878 """
879 A decorator for function with signature `apply_decorator(f, host)`, in order to inject 'host', the host of f.
881 Since it is not entirely feasible to detect the host in python, my first implementation was a bit complex: it was
882 returning an object with custom implementation of __call__ and __get__ methods, both reacting when pytest collection
883 happens.
885 That was very complex. Now we rely on an approximate but good enough alternative with `get_function_host`
887 :param apply_decorator:
888 :return:
889 """
890 # class _apply_decorator_with_host_tracking(object):
891 # def __init__(self, _target):
892 # # This is called when the decorator is applied on the target. Remember the target and result of paramz
893 # self._target = _target
894 # self.__wrapped__ = None
895 #
896 # def __get__(self, obj, type_=None):
897 # """
898 # When the decorated test function or fixture sits in a cl
899 # :param obj:
900 # :param type_:
901 # :return:
902 # """
903 # # We now know that the parametrized function/fixture self._target sits in obj (a class or a module)
904 # # We can therefore apply our parametrization accordingly (we need a reference to this host container in
905 # # order to store fixtures there)
906 # if self.__wrapped__ is None:
907 # self.__wrapped__ = 1 # means 'pending', to protect against infinite recursion
908 # try:
909 # self.__wrapped__ = apply_decorator(self._target, obj)
910 # except Exception as e:
911 # traceback = sys.exc_info()[2]
912 # reraise(BaseException, e.args, traceback)
913 #
914 # # path, lineno = get_fslocation_from_item(self)
915 # # warn_explicit(
916 # # "Error parametrizing function %s : [%s] %s" % (self._target, e.__class__, e),
917 # # category=None,
918 # # filename=str(path),
919 # # lineno=lineno + 1 if lineno is not None else None,
920 # # )
921 # #
922 # # @wraps(self._target)
923 # # def _exc_raiser(*args, **kwargs):
924 # # raise e
925 # # # remove this metadata otherwise pytest will unpack it
926 # # del _exc_raiser.__wrapped__
927 # # self.__wrapped__ = _exc_raiser
928 #
929 # return self.__wrapped__
930 #
931 # def __getattribute__(self, item):
932 # if item == '__call__':
933 # # direct call means that the parametrized function sits in a module. import it
934 # host_module = import_module(self._target.__module__)
935 #
936 # # next time the __call__ attribute will be set so callable() will work
937 # self.__call__ = self.__get__(host_module)
938 # return self.__call__
939 # else:
940 # return object.__getattribute__(self, item)
941 #
942 # return _apply_decorator_with_host_tracking
944 def apply(test_or_fixture_func):
945 # approximate: always returns the module and not the class :(
946 #
947 # indeed when this is called, the function exists (and its qualname mentions the host class) but the
948 # host class is not yet created in the module, so it is not found by our `get_class_that_defined_method`
949 #
950 # but still ... this is far less complex to debug than the above attempt and it does not yet have side effects..
951 container = get_function_host(test_or_fixture_func)
952 return apply_decorator(test_or_fixture_func, container)
954 return apply
957def get_pytest_request_and_item(request_or_item):
958 """Return the `request` and `item` (node) from whatever is provided"""
959 try:
960 item = request_or_item.node
961 except AttributeError:
962 item = request_or_item
963 request = item._request
964 else:
965 request = request_or_item
967 return item, request