Coverage for src/pytest_cases/common_pytest.py: 71%
348 statements
« prev ^ index » next coverage.py v7.8.2, created at 2025-06-09 20:03 +0000
« prev ^ index » next coverage.py v7.8.2, created at 2025-06-09 20:03 +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, PYTEST84_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 always true
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)]
88if PYTEST84_OR_GREATER: 88 ↛ 100line 88 didn't jump to line 100 because the condition on line 88 was always true
89 def is_fixture(fixture_fun # type: Any
90 ):
91 """
92 Returns True if the provided function is a fixture
94 :param fixture_fun:
95 :return:
96 """
97 from _pytest.fixtures import FixtureFunctionDefinition
98 return safe_isinstance(fixture_fun, FixtureFunctionDefinition)
99else:
100 def is_fixture(fixture_fun # type: Any
101 ):
102 """
103 Returns True if the provided function is a fixture
105 :param fixture_fun:
106 :return:
107 """
108 try:
109 fixture_fun._pytestfixturefunction # noqa
110 return True
111 except AttributeError:
112 # not a fixture ?
113 return False
116if PYTEST84_OR_GREATER: 116 ↛ 140line 116 didn't jump to line 140 because the condition on line 116 was always true
117 def list_all_fixtures_in(cls_or_module, return_names=True, recurse_to_module=False):
118 """
119 Returns a list containing all fixture names (or symbols if `return_names=False`)
120 in the given class or module.
122 Note that `recurse_to_module` can be used so that the fixtures in the parent
123 module of a class are listed too.
125 :param cls_or_module:
126 :param return_names:
127 :param recurse_to_module:
128 :return:
129 """
130 res = [get_fixture_name(symb) if return_names else symb
131 for n, symb in inspect.getmembers(cls_or_module, is_fixture)]
133 if recurse_to_module and not inspect.ismodule(cls_or_module): 133 ↛ 135line 133 didn't jump to line 135 because the condition on line 133 was never true
134 # TODO currently this only works for a single level of nesting, we should use __qualname__ (py3) or .im_class
135 host = import_module(cls_or_module.__module__)
136 res += list_all_fixtures_in(host, recurse_to_module=True, return_names=return_names)
138 return res
139else:
140 def list_all_fixtures_in(cls_or_module, return_names=True, recurse_to_module=False):
141 """
142 Returns a list containing all fixture names (or symbols if `return_names=False`)
143 in the given class or module.
145 Note that `recurse_to_module` can be used so that the fixtures in the parent
146 module of a class are listed too.
148 :param cls_or_module:
149 :param return_names:
150 :param recurse_to_module:
151 :return:
152 """
153 res = [get_fixture_name(symb) if return_names else symb
154 for n, symb in inspect.getmembers(cls_or_module, lambda f: inspect.isfunction(f) or inspect.ismethod(f))
155 if is_fixture(symb)]
157 if recurse_to_module and not inspect.ismodule(cls_or_module):
158 # TODO currently this only works for a single level of nesting, we should use __qualname__ (py3) or .im_class
159 host = import_module(cls_or_module.__module__)
160 res += list_all_fixtures_in(host, recurse_to_module=True, return_names=return_names)
162 return res
165def safe_isclass(obj # type: object
166 ):
167 # type: (...) -> bool
168 """Ignore any exception via isinstance on Python 3."""
169 try:
170 return isclass(obj)
171 except Exception: # noqa
172 return False
175def safe_isinstance(obj, # type: object
176 cls):
177 # type: (...) -> bool
178 """Ignore any exception via isinstance"""
179 try:
180 return isinstance(obj, cls)
181 except Exception: # noqa
182 return False
185def assert_is_fixture(fixture_fun # type: Any
186 ):
187 """
188 Raises a ValueError if the provided fixture function is not a fixture.
190 :param fixture_fun:
191 :return:
192 """
193 if not is_fixture(fixture_fun):
194 raise ValueError("The provided fixture function does not seem to be a fixture: %s. Did you properly decorate "
195 "it ?" % fixture_fun)
198if PYTEST84_OR_GREATER: 198 ↛ 224line 198 didn't jump to line 224 because the condition on line 198 was always true
199 def get_fixture_name(fixture_fun # type: Union[str, Callable]
200 ):
201 """
202 Internal utility to retrieve the fixture name corresponding to the given fixture function.
203 Indeed there is currently no pytest API to do this.
205 Note: this function can receive a string, in which case it is directly returned.
207 :param fixture_fun:
208 :return:
209 """
210 if isinstance(fixture_fun, string_types):
211 return fixture_fun
213 assert_is_fixture(fixture_fun)
215 if fixture_fun.name is None: 215 ↛ 219line 215 didn't jump to line 219 because the condition on line 215 was never true
216 # As opposed to pytest < 8.4.0, the merge between custom name and function name has already been made,
217 # this should not happen.
218 # See https://github.com/nicoddemus/pytest/commit/ecde993e17efb3f34157642a111ba20f476aa80a
219 raise NotImplementedError
221 return fixture_fun.name
223else:
224 def get_fixture_name(fixture_fun # type: Union[str, Callable]
225 ):
226 """
227 Internal utility to retrieve the fixture name corresponding to the given fixture function.
228 Indeed there is currently no pytest API to do this.
230 Note: this function can receive a string, in which case it is directly returned.
232 :param fixture_fun:
233 :return:
234 """
235 if isinstance(fixture_fun, string_types):
236 return fixture_fun
237 assert_is_fixture(fixture_fun)
238 try: # pytest 3
239 custom_fixture_name = fixture_fun._pytestfixturefunction.name # noqa
240 except AttributeError:
241 try: # pytest 2
242 custom_fixture_name = fixture_fun.func_name # noqa
243 except AttributeError:
244 custom_fixture_name = None
246 if custom_fixture_name is not None:
247 # there is a custom fixture name
248 return custom_fixture_name
249 else:
250 obj__name = getattr(fixture_fun, '__name__', None)
251 if obj__name is not None:
252 # a function, probably
253 return obj__name
254 else:
255 # a callable object probably
256 return str(fixture_fun)
259if PYTEST84_OR_GREATER: 259 ↛ 272line 259 didn't jump to line 272 because the condition on line 259 was always true
260 def get_fixture_scope(fixture_fun):
261 """
262 Internal utility to retrieve the fixture scope corresponding to the given fixture function .
263 Indeed there is currently no pytest API to do this.
265 :param fixture_fun:
266 :return:
267 """
268 assert_is_fixture(fixture_fun)
269 # See https://github.com/nicoddemus/pytest/commit/ecde993e17efb3f34157642a111ba20f476aa80a
270 return fixture_fun._fixture_function_marker.scope # noqa
271else:
272 def get_fixture_scope(fixture_fun):
273 """
274 Internal utility to retrieve the fixture scope corresponding to the given fixture function .
275 Indeed there is currently no pytest API to do this.
277 :param fixture_fun:
278 :return:
279 """
280 assert_is_fixture(fixture_fun)
281 return fixture_fun._pytestfixturefunction.scope # noqa
282 # except AttributeError:
283 # # pytest 2
284 # return fixture_fun.func_scope
287# ---------------- working on pytest nodes (e.g. Function)
289def is_function_node(node):
290 try:
291 node.function # noqa
292 return True
293 except AttributeError:
294 return False
297def get_parametrization_markers(fnode):
298 """
299 Returns the parametrization marks on a pytest Function node.
300 :param fnode:
301 :return:
302 """
303 if PYTEST34_OR_GREATER: 303 ↛ 306line 303 didn't jump to line 306 because the condition on line 303 was always true
304 return list(fnode.iter_markers(name="parametrize"))
305 else:
306 return list(fnode.parametrize)
309def get_param_names(fnode):
310 """
311 Returns a list of parameter names for the given pytest Function node.
312 parameterization marks containing several names are split
314 :param fnode:
315 :return:
316 """
317 p_markers = get_parametrization_markers(fnode)
318 param_names = []
319 for paramz_mark in p_markers:
320 argnames = paramz_mark.args[0] if len(paramz_mark.args) > 0 else paramz_mark.kwargs['argnames']
321 param_names += get_param_argnames_as_list(argnames)
322 return param_names
325# ---------- test ids utils ---------
326def combine_ids(paramid_tuples):
327 """
328 Receives a list of tuples containing ids for each parameterset.
329 Returns the final ids, that are obtained by joining the various param ids by '-' for each test node
331 :param paramid_tuples:
332 :return:
333 """
334 #
335 return ['-'.join(pid for pid in testid) for testid in paramid_tuples]
338def make_test_ids(global_ids, id_marks, argnames=None, argvalues=None, precomputed_ids=None):
339 """
340 Creates the proper id for each test based on (higher precedence first)
342 - any specific id mark from a `pytest.param` (`id_marks`)
343 - the global `ids` argument of pytest parametrize (`global_ids`)
344 - the name and value of parameters (`argnames`, `argvalues`) or the precomputed ids(`precomputed_ids`)
346 See also _pytest.python._idvalset method
348 :param global_ids:
349 :param id_marks:
350 :param argnames:
351 :param argvalues:
352 :param precomputed_ids:
353 :return:
354 """
355 if global_ids is not None:
356 # overridden at global pytest.mark.parametrize level - this takes precedence.
357 # resolve possibly infinite generators of ids here
358 p_ids = resolve_ids(global_ids, argvalues, full_resolve=True)
359 else:
360 # default: values-based
361 if precomputed_ids is not None: 361 ↛ 362line 361 didn't jump to line 362 because the condition on line 361 was never true
362 if argnames is not None or argvalues is not None:
363 raise ValueError("Only one of `precomputed_ids` or argnames/argvalues should be provided.")
364 p_ids = precomputed_ids
365 else:
366 p_ids = make_test_ids_from_param_values(argnames, argvalues)
368 # Finally, local pytest.param takes precedence over everything else
369 for i, _id in enumerate(id_marks):
370 if _id is not None:
371 p_ids[i] = _id
372 return p_ids
375def resolve_ids(ids, # type: Optional[Union[Callable, Iterable[str]]]
376 argvalues, # type: Sized(Any)
377 full_resolve=False # type: bool
378 ):
379 # type: (...) -> Union[List[str], Callable]
380 """
381 Resolves the `ids` argument of a parametrized fixture.
383 If `full_resolve` is False (default), iterable ids will be resolved, but not callable ids. This is useful if the
384 `argvalues` have not yet been cleaned of possible `pytest.param` wrappers.
386 If `full_resolve` is True, callable ids will be called using the argvalues, so the result is guaranteed to be a
387 list.
388 """
389 try:
390 # an explicit list or generator of ids ?
391 iter(ids)
392 except TypeError:
393 # a callable to apply on the values
394 if full_resolve:
395 return [ids(v) for v in argvalues]
396 else:
397 # return the callable without resolving
398 return ids
399 else:
400 # iterable.
401 try:
402 # a sized container ? (list, set, tuple)
403 nb_ids = len(ids)
404 # convert to list
405 ids = list(ids)
406 except TypeError:
407 # a generator. Consume it
408 ids = [id for id, v in zip(ids, argvalues)]
409 nb_ids = len(ids)
411 if nb_ids != len(argvalues): 411 ↛ 412line 411 didn't jump to line 412 because the condition on line 411 was never true
412 raise ValueError("Explicit list or generator of `ids` provided has a different length (%s) than the number "
413 "of argvalues (%s). Ids provided: %r" % (len(ids), len(argvalues), ids))
414 return ids
417def make_test_ids_from_param_values(param_names,
418 param_values,
419 ):
420 """
421 Replicates pytest behaviour to generate the ids when there are several parameters in a single `parametrize.
422 Note that param_values should not contain marks.
424 :param param_names:
425 :param param_values:
426 :return: a list of param ids
427 """
428 if isinstance(param_names, string_types): 428 ↛ 429line 428 didn't jump to line 429 because the condition on line 428 was never true
429 raise TypeError("param_names must be an iterable. Found %r" % param_names)
431 nb_params = len(param_names)
432 if nb_params == 0: 432 ↛ 433line 432 didn't jump to line 433 because the condition on line 432 was never true
433 raise ValueError("empty list provided")
434 elif nb_params == 1:
435 paramids = []
436 for _idx, v in enumerate(param_values):
437 _id = mini_idvalset(param_names, (v,), _idx)
438 paramids.append(_id)
439 else:
440 paramids = []
441 for _idx, vv in enumerate(param_values):
442 if len(vv) != nb_params: 442 ↛ 443line 442 didn't jump to line 443 because the condition on line 442 was never true
443 raise ValueError("Inconsistent lengths for parameter names and values: '%s' and '%s'"
444 "" % (param_names, vv))
445 _id = mini_idvalset(param_names, vv, _idx)
446 paramids.append(_id)
447 return paramids
450# ---- ParameterSet api ---
451# def analyze_parameter_set(pmark=None, argnames=None, argvalues=None, ids=None, check_nb=True):
452# """
453# analyzes a parameter set passed either as a pmark or as distinct
454# (argnames, argvalues, ids) to extract/construct the various ids, marks, and
455# values
456#
457# See also pytest.Metafunc.parametrize method, that calls in particular
458# pytest.ParameterSet._for_parametrize and _pytest.python._idvalset
459#
460# :param pmark:
461# :param argnames:
462# :param argvalues:
463# :param ids:
464# :param check_nb: a bool indicating if we should raise an error if len(argnames) > 1 and any argvalue has
465# a different length than len(argnames)
466# :return: ids, marks, values
467# """
468# if pmark is not None:
469# if any(a is not None for a in (argnames, argvalues, ids)):
470# raise ValueError("Either provide a pmark OR the details")
471# argnames = pmark.param_names
472# argvalues = pmark.param_values
473# ids = pmark.param_ids
474#
475# # extract all parameters that have a specific configuration (pytest.param())
476# custom_pids, p_marks, p_values = extract_parameterset_info(argnames, argvalues, check_nb=check_nb)
477#
478# # get the ids by merging/creating the various possibilities
479# p_ids = make_test_ids(argnames=argnames, argvalues=p_values, global_ids=ids, id_marks=custom_pids)
480#
481# return p_ids, p_marks, p_values
484def extract_parameterset_info(argnames, argvalues, check_nb=True):
485 """
487 :param argnames: the names in this parameterset
488 :param argvalues: the values in this parameterset
489 :param check_nb: a bool indicating if we should raise an error if len(argnames) > 1 and any argvalue has
490 a different length than len(argnames)
491 :return:
492 """
493 pids = []
494 pmarks = []
495 pvalues = []
496 if isinstance(argnames, string_types): 496 ↛ 497line 496 didn't jump to line 497 because the condition on line 496 was never true
497 raise TypeError("argnames must be an iterable. Found %r" % argnames)
498 nbnames = len(argnames)
499 for v in argvalues:
500 _pid, _pmark, _pvalue = extract_pset_info_single(nbnames, v)
502 pids.append(_pid)
503 pmarks.append(_pmark)
504 pvalues.append(_pvalue)
506 if check_nb and nbnames > 1 and (len(_pvalue) != nbnames): 506 ↛ 507line 506 didn't jump to line 507 because the condition on line 506 was never true
507 raise ValueError("Inconsistent number of values in pytest parametrize: %s items found while the "
508 "number of parameters is %s: %s." % (len(_pvalue), nbnames, _pvalue))
510 return pids, pmarks, pvalues
513def extract_pset_info_single(nbnames, argvalue):
514 """Return id, marks, value"""
515 if is_marked_parameter_value(argvalue):
516 # --id
517 _id = get_marked_parameter_id(argvalue)
518 # --marks
519 marks = get_marked_parameter_marks(argvalue)
520 # --value(a tuple if this is a tuple parameter)
521 argvalue = get_marked_parameter_values(argvalue, nbargs=nbnames)
522 return _id, marks, argvalue[0] if nbnames == 1 else argvalue
523 else:
524 # normal argvalue
525 return None, None, argvalue
528try: # pytest 3.x+
529 from _pytest.mark import ParameterSet # noqa
531 def is_marked_parameter_value(v):
532 return isinstance(v, ParameterSet)
534 def get_marked_parameter_marks(v):
535 return v.marks
537 def get_marked_parameter_values(v, nbargs):
538 """This always returns a tuple. nbargs is useful for pytest2 compatibility """
539 return v.values
541 def get_marked_parameter_id(v):
542 return v.id
544except ImportError: # pytest 2.x
545 from _pytest.mark import MarkDecorator
547 # noinspection PyPep8Naming
548 def ParameterSet(values,
549 id, # noqa
550 marks):
551 """ Dummy function (not a class) used only by `parametrize` """
552 if id is not None:
553 raise ValueError("This should not happen as `pytest.param` does not exist in pytest 2")
555 # smart unpack is required for compatibility
556 val = values[0] if len(values) == 1 else values
557 nbmarks = len(marks)
559 if nbmarks == 0:
560 return val
561 elif nbmarks > 1:
562 raise ValueError("Multiple marks on parameters not supported for old versions of pytest")
563 else:
564 # decorate with the MarkDecorator
565 return marks[0](val)
567 def is_marked_parameter_value(v):
568 return isinstance(v, MarkDecorator)
570 def get_marked_parameter_marks(v):
571 return [v]
573 def get_marked_parameter_values(v, nbargs):
574 """Returns a tuple containing the values"""
576 # v.args[-1] contains the values.
577 # see MetaFunc.parametrize in pytest 2 to be convinced :)
579 # if v.name in ('skip', 'skipif'):
580 if nbargs == 1:
581 # the last element of args is not a tuple when there is a single arg.
582 return (v.args[-1],)
583 else:
584 return v.args[-1]
585 # else:
586 # raise ValueError("Unsupported mark")
588 def get_marked_parameter_id(v):
589 return v.kwargs.get('id', None)
592def get_pytest_nodeid(metafunc):
593 try:
594 return metafunc.definition.nodeid
595 except AttributeError:
596 return "unknown"
599try:
600 # pytest 7+ : scopes is an enum
601 from _pytest.scope import Scope
603 def get_pytest_function_scopeval():
604 return Scope.Function
606 def has_function_scope(fixdef):
607 return fixdef._scope is Scope.Function
609 def set_callspec_arg_scope_to_function(callspec, arg_name):
610 callspec._arg2scope[arg_name] = Scope.Function
612except ImportError:
613 try:
614 # pytest 3+
615 from _pytest.fixtures import scopes as pt_scopes
616 except ImportError:
617 # pytest 2
618 from _pytest.python import scopes as pt_scopes
620 # def get_pytest_scopenum(scope_str):
621 # return pt_scopes.index(scope_str)
623 def get_pytest_function_scopeval():
624 return pt_scopes.index("function")
626 def has_function_scope(fixdef):
627 return fixdef.scopenum == get_pytest_function_scopeval()
629 def set_callspec_arg_scope_to_function(callspec, arg_name):
630 callspec._arg2scopenum[arg_name] = get_pytest_function_scopeval() # noqa
633def in_callspec_explicit_args(
634 callspec, # type: CallSpec2
635 name # type: str
636): # type: (...) -> bool
637 """Return True if name is explicitly used in callspec args"""
638 return (name in callspec.params) or (not PYTEST8_OR_GREATER and name in callspec.funcargs)
641if PYTEST71_OR_GREATER: 641 ↛ 647line 641 didn't jump to line 647 because the condition on line 641 was always true
642 from _pytest.python import IdMaker # noqa
644 _idval = IdMaker([], [], None, None, None, None, None)._idval
645 _idval_kwargs = dict()
646else:
647 from _pytest.python import _idval # noqa
649 if PYTEST6_OR_GREATER:
650 _idval_kwargs = dict(idfn=None,
651 nodeid=None, # item is not used in pytest(>=6.0.0) nodeid is only used by idfn
652 config=None # if a config hook was available it would be used before this is called)
653 )
654 elif PYTEST38_OR_GREATER:
655 _idval_kwargs = dict(idfn=None,
656 item=None, # item is only used by idfn
657 config=None # if a config hook was available it would be used before this is called)
658 )
659 else:
660 _idval_kwargs = dict(idfn=None,
661 # config=None # if a config hook was available it would be used before this is called)
662 )
665def mini_idval(
666 val, # type: object
667 argname, # type: str
668 idx, # type: int
669):
670 """
671 A simplified version of idval where idfn, item and config do not need to be passed.
673 :param val:
674 :param argname:
675 :param idx:
676 :return:
677 """
678 return _idval(val=val, argname=argname, idx=idx, **_idval_kwargs)
681def mini_idvalset(argnames, argvalues, idx):
682 """ mimic _pytest.python._idvalset but can handle lazyvalues used for tuples or args
684 argvalues should not be a pytest.param (ParameterSet)
685 This function returns a SINGLE id for a single test node
686 """
687 if len(argnames) > 1 and is_lazy(argvalues):
688 # handle the case of LazyTuple used for several args
689 return argvalues.get_id()
691 this_id = [
692 _idval(val, argname, idx=idx, **_idval_kwargs)
693 for val, argname in zip(argvalues, argnames)
694 ]
695 return "-".join(this_id)
698try:
699 from _pytest.compat import getfuncargnames # noqa
700except ImportError:
701 def num_mock_patch_args(function):
702 """ return number of arguments used up by mock arguments (if any) """
703 patchings = getattr(function, "patchings", None)
704 if not patchings:
705 return 0
707 mock_sentinel = getattr(sys.modules.get("mock"), "DEFAULT", object())
708 ut_mock_sentinel = getattr(sys.modules.get("unittest.mock"), "DEFAULT", object())
710 return len(
711 [p for p in patchings if not p.attribute_name and (p.new is mock_sentinel or p.new is ut_mock_sentinel)]
712 )
714 # noinspection SpellCheckingInspection
715 def getfuncargnames(function, cls=None):
716 """Returns the names of a function's mandatory arguments."""
717 parameters = signature(function).parameters
719 arg_names = tuple(
720 p.name
721 for p in parameters.values()
722 if (
723 p.kind is Parameter.POSITIONAL_OR_KEYWORD
724 or p.kind is Parameter.KEYWORD_ONLY
725 )
726 and p.default is Parameter.empty
727 )
729 # If this function should be treated as a bound method even though
730 # it's passed as an unbound method or function, remove the first
731 # parameter name.
732 if cls and not isinstance(cls.__dict__.get(function.__name__, None), staticmethod):
733 arg_names = arg_names[1:]
734 # Remove any names that will be replaced with mocks.
735 if hasattr(function, "__wrapped__"):
736 arg_names = arg_names[num_mock_patch_args(function):]
737 return arg_names
740class FakeSession(object):
741 __slots__ = ('_fixturemanager',)
743 def __init__(self):
744 self._fixturemanager = None
747class MiniFuncDef(object):
748 __slots__ = ('nodeid', 'session')
750 def __init__(self, nodeid):
751 self.nodeid = nodeid
752 if PYTEST8_OR_GREATER: 752 ↛ exitline 752 didn't return from function '__init__' because the condition on line 752 was always true
753 self.session = FakeSession()
756class MiniMetafunc(Metafunc):
757 """
758 A class to know what pytest *would* do for a given function in terms of callspec.
759 It is ONLY used in function `case_to_argvalues` and only the following are read:
761 - is_parametrized (bool)
762 - requires_fixtures (bool)
763 - fixturenames_not_in_sig (declared used fixtures with @pytest.mark.usefixtures)
765 Computation of the latter requires
767 """
768 # noinspection PyMissingConstructor
769 def __init__(self, func):
770 from .plugin import PYTEST_CONFIG # late import to ensure config has been loaded by now
772 self.config = PYTEST_CONFIG
774 # self.config can be `None` if the same module is reloaded by another thread/process inside a test (parallelism)
775 # In that case, a priori we are outside the pytest main runner so we can silently ignore, this
776 # MetaFunc will not be used/read by anyone.
777 # See https://github.com/smarie/python-pytest-cases/issues/242
778 #
779 # if self.config is None:
780 # if pytest_is_running():
781 # raise ValueError("Internal error - config has not been correctly loaded. Please report")
783 self.function = func
784 self.definition = MiniFuncDef(func.__name__)
785 self._calls = []
786 self._params_directness = {}
787 # non-default parameters
788 self.fixturenames = getfuncargnames(func)
789 # add declared used fixtures with @pytest.mark.usefixtures
790 self.fixturenames_not_in_sig = [f for f in get_pytest_usefixture_marks(func) if f not in self.fixturenames]
791 if self.fixturenames_not_in_sig:
792 self.fixturenames = tuple(self.fixturenames_not_in_sig + list(self.fixturenames))
794 if PYTEST8_OR_GREATER: 794 ↛ 799line 794 didn't jump to line 799 because the condition on line 794 was always true
795 # dummy
796 self._arg2fixturedefs = dict() # type: dict[str, Sequence["FixtureDef[Any]"]]
798 # get parametrization marks
799 self.pmarks = get_pytest_parametrize_marks(self.function)
800 if self.is_parametrized:
801 self.update_callspecs()
802 # preserve order
803 ref_names = self._calls[0].params if PYTEST8_OR_GREATER else self._calls[0].funcargs
804 self.required_fixtures = tuple(f for f in self.fixturenames if f not in ref_names)
805 else:
806 self.required_fixtures = self.fixturenames
808 @property
809 def is_parametrized(self):
810 return len(self.pmarks) > 0
812 @property
813 def requires_fixtures(self):
814 return len(self.required_fixtures) > 0
816 def update_callspecs(self):
817 """
819 :return:
820 """
821 for pmark in self.pmarks:
822 if len(pmark.param_names) == 1:
823 if PYTEST3_OR_GREATER: 823 ↛ 826line 823 didn't jump to line 826 because the condition on line 823 was always true
824 argvals = tuple(v if is_marked_parameter_value(v) else (v,) for v in pmark.param_values)
825 else:
826 argvals = []
827 for v in pmark.param_values:
828 if is_marked_parameter_value(v):
829 newmark = MarkDecorator(v.markname, v.args[:-1] + ((v.args[-1],),), v.kwargs)
830 argvals.append(newmark)
831 else:
832 argvals.append((v,))
833 argvals = tuple(argvals)
834 else:
835 argvals = pmark.param_values
836 self.parametrize(argnames=pmark.param_names, argvalues=argvals, ids=pmark.param_ids,
837 # use indirect = False and scope = 'function' to avoid having to implement complex patches
838 indirect=False, scope='function')
840 if not PYTEST33_OR_GREATER: 840 ↛ 843line 840 didn't jump to line 843 because the condition on line 840 was never true
841 # fix the CallSpec2 instances so that the marks appear in an attribute "mark"
842 # noinspection PyProtectedMember
843 for c in self._calls:
844 c.marks = list(c.keywords.values())
847def add_fixture_params(func, new_names):
848 """Creates a wrapper of the given function with additional arguments"""
850 old_sig = signature(func)
852 # prepend all new parameters if needed
853 for n in new_names:
854 if n in old_sig.parameters: 854 ↛ 855line 854 didn't jump to line 855 because the condition on line 854 was never true
855 raise ValueError("argument named %s already present in signature" % n)
856 new_sig = add_signature_parameters(old_sig,
857 first=[Parameter(n, kind=Parameter.POSITIONAL_OR_KEYWORD) for n in new_names])
859 assert not isgeneratorfunction(func)
861 # normal function with return statement
862 @wraps(func, new_sig=new_sig)
863 def wrapped_func(**kwargs):
864 for n in new_names:
865 kwargs.pop(n)
866 return func(**kwargs)
868 # else:
869 # # generator function (with a yield statement)
870 # @wraps(fixture_func, new_sig=new_sig)
871 # def wrapped_fixture_func(*args, **kwargs):
872 # request = kwargs['request'] if func_needs_request else kwargs.pop('request')
873 # if is_used_request(request):
874 # for res in fixture_func(*args, **kwargs):
875 # yield res
876 # else:
877 # yield NOT_USED
879 return wrapped_func
882def get_callspecs(func):
883 """
884 Returns a list of pytest CallSpec objects corresponding to calls that should be made for this parametrized function.
885 This mini-helper assumes no complex things (scope='function', indirect=False, no fixtures, no custom configuration)
887 Note that this function is currently only used in tests.
888 """
889 meta = MiniMetafunc(func)
890 # meta.update_callspecs()
891 # noinspection PyProtectedMember
892 return meta._calls
895def cart_product_pytest(argnames, argvalues):
896 """
897 - do NOT use `itertools.product` as it fails to handle MarkDecorators
898 - we also unpack tuples associated with several argnames ("a,b") if needed
899 - we also propagate marks
901 :param argnames:
902 :param argvalues:
903 :return:
904 """
905 # transform argnames into a list of lists
906 argnames_lists = [get_param_argnames_as_list(_argnames) if len(_argnames) > 0 else [] for _argnames in argnames]
908 # make the cartesian product per se
909 argvalues_prod = _cart_product_pytest(argnames_lists, argvalues)
911 # flatten the list of argnames
912 argnames_list = [n for nlist in argnames_lists for n in nlist]
914 # apply all marks to the arvalues
915 argvalues_prod = [make_marked_parameter_value(tuple(argvalues), marks=marks) if len(marks) > 0 else tuple(argvalues)
916 for marks, argvalues in argvalues_prod]
918 return argnames_list, argvalues_prod
921def _cart_product_pytest(argnames_lists, argvalues):
922 result = []
924 # first perform the sub cartesian product with entries [1:]
925 sub_product = _cart_product_pytest(argnames_lists[1:], argvalues[1:]) if len(argvalues) > 1 else None
927 # then do the final product with entry [0]
928 for x in argvalues[0]:
929 # handle x
930 nb_names = len(argnames_lists[0])
932 # (1) extract meta-info
933 x_id, x_marks, x_value = extract_pset_info_single(nb_names, x)
934 x_marks_lst = list(x_marks) if x_marks is not None else []
935 if x_id is not None: 935 ↛ 936line 935 didn't jump to line 936 because the condition on line 935 was never true
936 raise ValueError("It is not possible to specify a sub-param id when using the new parametrization style. "
937 "Either use the traditional style or customize all ids at once in `idgen`")
939 # (2) possibly unpack
940 if nb_names > 1:
941 # if lazy value, we have to do something
942 if is_lazy_value(x_value):
943 x_value_lst = x_value.as_lazy_items_list(nb_names)
944 else:
945 x_value_lst = list(x_value)
946 else:
947 x_value_lst = [x_value]
949 # product
950 if len(argvalues) > 1:
951 for m, p in sub_product:
952 # combine marks and values
953 result.append((x_marks_lst + m, x_value_lst + p))
954 else:
955 result.append((x_marks_lst, x_value_lst))
957 return result
960def inject_host(apply_decorator):
961 """
962 A decorator for function with signature `apply_decorator(f, host)`, in order to inject 'host', the host of f.
964 Since it is not entirely feasible to detect the host in python, my first implementation was a bit complex: it was
965 returning an object with custom implementation of __call__ and __get__ methods, both reacting when pytest collection
966 happens.
968 That was very complex. Now we rely on an approximate but good enough alternative with `get_function_host`
970 :param apply_decorator:
971 :return:
972 """
973 # class _apply_decorator_with_host_tracking(object):
974 # def __init__(self, _target):
975 # # This is called when the decorator is applied on the target. Remember the target and result of paramz
976 # self._target = _target
977 # self.__wrapped__ = None
978 #
979 # def __get__(self, obj, type_=None):
980 # """
981 # When the decorated test function or fixture sits in a cl
982 # :param obj:
983 # :param type_:
984 # :return:
985 # """
986 # # We now know that the parametrized function/fixture self._target sits in obj (a class or a module)
987 # # We can therefore apply our parametrization accordingly (we need a reference to this host container in
988 # # order to store fixtures there)
989 # if self.__wrapped__ is None:
990 # self.__wrapped__ = 1 # means 'pending', to protect against infinite recursion
991 # try:
992 # self.__wrapped__ = apply_decorator(self._target, obj)
993 # except Exception as e:
994 # traceback = sys.exc_info()[2]
995 # reraise(BaseException, e.args, traceback)
996 #
997 # # path, lineno = get_fslocation_from_item(self)
998 # # warn_explicit(
999 # # "Error parametrizing function %s : [%s] %s" % (self._target, e.__class__, e),
1000 # # category=None,
1001 # # filename=str(path),
1002 # # lineno=lineno + 1 if lineno is not None else None,
1003 # # )
1004 # #
1005 # # @wraps(self._target)
1006 # # def _exc_raiser(*args, **kwargs):
1007 # # raise e
1008 # # # remove this metadata otherwise pytest will unpack it
1009 # # del _exc_raiser.__wrapped__
1010 # # self.__wrapped__ = _exc_raiser
1011 #
1012 # return self.__wrapped__
1013 #
1014 # def __getattribute__(self, item):
1015 # if item == '__call__':
1016 # # direct call means that the parametrized function sits in a module. import it
1017 # host_module = import_module(self._target.__module__)
1018 #
1019 # # next time the __call__ attribute will be set so callable() will work
1020 # self.__call__ = self.__get__(host_module)
1021 # return self.__call__
1022 # else:
1023 # return object.__getattribute__(self, item)
1024 #
1025 # return _apply_decorator_with_host_tracking
1027 def apply(test_or_fixture_func):
1028 # approximate: always returns the module and not the class :(
1029 #
1030 # indeed when this is called, the function exists (and its qualname mentions the host class) but the
1031 # host class is not yet created in the module, so it is not found by our `get_class_that_defined_method`
1032 #
1033 # but still ... this is far less complex to debug than the above attempt and it does not yet have side effects..
1034 container = get_function_host(test_or_fixture_func)
1035 return apply_decorator(test_or_fixture_func, container)
1037 return apply
1040def get_pytest_request_and_item(request_or_item):
1041 """Return the `request` and `item` (node) from whatever is provided"""
1042 try:
1043 item = request_or_item.node
1044 except AttributeError:
1045 item = request_or_item
1046 request = item._request
1047 else:
1048 request = request_or_item
1050 return item, request