Coverage for src/pytest_cases/fixture_parametrize_plus.py: 88%
522 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 inspect import isgeneratorfunction
6from warnings import warn
9try: # python 3.3+
10 from inspect import signature, Parameter
11except ImportError:
12 from funcsigs import signature, Parameter # noqa
14try: # native coroutines, python 3.5+
15 from inspect import iscoroutinefunction
16except ImportError:
17 def iscoroutinefunction(obj):
18 return False
20try: # native async generators, python 3.6+
21 from inspect import isasyncgenfunction
22except ImportError:
23 def isasyncgenfunction(obj):
24 return False
26try:
27 from collections.abc import Iterable
28except ImportError: # noqa
29 from collections import Iterable
31try:
32 from typing import Union, Callable, List, Any, Sequence, Optional, Type, Tuple, TypeVar # noqa
33 from types import ModuleType # noqa
35 T = TypeVar('T', bound=Union[Type, Callable])
36except ImportError:
37 pass
39import pytest
40import sys
41from makefun import with_signature, remove_signature_parameters, add_signature_parameters, wraps
43from .common_mini_six import string_types
44from .common_others import AUTO, robust_isinstance, replace_list_contents
45from .common_pytest_marks import has_pytest_param, get_param_argnames_as_list
46from .common_pytest_lazy_values import is_lazy_value, get_lazy_args
47from .common_pytest import get_fixture_name, remove_duplicates, mini_idvalset, is_marked_parameter_value, \
48 extract_parameterset_info, ParameterSet, cart_product_pytest, mini_idval, inject_host, \
49 get_marked_parameter_values, resolve_ids, get_marked_parameter_id, get_marked_parameter_marks, is_fixture, \
50 safe_isclass
52from .fixture__creation import check_name_available, CHANGE, WARN
53from .fixture_core1_unions import InvalidParamsList, NOT_USED, UnionFixtureAlternative, _make_fixture_union, \
54 _make_unpack_fixture, UnionIdMakers
55from .fixture_core2 import _create_param_fixture, fixture
58def _fixture_product(fixtures_dest,
59 name, # type: str
60 fixtures_or_values,
61 fixture_positions,
62 scope="function", # type: str
63 unpack_into=None, # type: Iterable[str]
64 autouse=False, # type: bool
65 hook=None, # type: Callable[[Callable], Callable]
66 caller=None, # type: Callable
67 **kwargs):
68 """
69 Internal implementation for fixture products created by pytest parametrize plus.
71 :param fixtures_dest:
72 :param name:
73 :param fixtures_or_values:
74 :param fixture_positions:
75 :param idstyle:
76 :param scope:
77 :param ids:
78 :param unpack_into:
79 :param autouse:
80 :param kwargs:
81 :return:
82 """
83 # test the `fixtures` argument to avoid common mistakes
84 if not isinstance(fixtures_or_values, (tuple, set, list)): 84 ↛ 85line 84 didn't jump to line 85, because the condition on line 84 was never true
85 raise TypeError("fixture_product: the `fixtures_or_values` argument should be a tuple, set or list")
86 else:
87 has_lazy_vals = any(is_lazy_value(v) for v in fixtures_or_values)
89 _tuple_size = len(fixtures_or_values)
91 # first get all required fixture names
92 f_names = [None] * _tuple_size
93 for f_pos in fixture_positions:
94 # possibly get the fixture name if the fixture symbol was provided
95 f = fixtures_or_values[f_pos]
96 if isinstance(f, fixture_ref): 96 ↛ 99line 96 didn't jump to line 99, because the condition on line 96 was never false
97 f = f.fixture
98 # and remember the position in the tuple
99 f_names[f_pos] = get_fixture_name(f)
101 # remove duplicates by making it an ordered set
102 all_names = remove_duplicates((n for n in f_names if n is not None))
103 if len(all_names) < 1: 103 ↛ 104line 103 didn't jump to line 104, because the condition on line 103 was never true
104 raise ValueError("Empty fixture products are not permitted")
106 def _tuple_generator(request, all_fixtures):
107 for i in range(_tuple_size):
108 fix_at_pos_i = f_names[i]
109 if fix_at_pos_i is None:
110 # fixed value
111 # note: wouldn't it be almost as efficient but more readable to *always* call handle_lazy_args?
112 yield get_lazy_args(fixtures_or_values[i], request) if has_lazy_vals else fixtures_or_values[i]
113 else:
114 # fixture value
115 yield all_fixtures[fix_at_pos_i]
117 # then generate the body of our product fixture. It will require all of its dependent fixtures
118 @with_signature("(request, %s)" % ', '.join(all_names))
119 def _new_fixture(request, **all_fixtures):
120 return tuple(_tuple_generator(request, all_fixtures))
122 _new_fixture.__name__ = name
124 # finally create the fixture per se.
125 # WARNING we do not use pytest.fixture but fixture so that NOT_USED is discarded
126 f_decorator = fixture(scope=scope, autouse=autouse, hook=hook, **kwargs)
127 fix = f_decorator(_new_fixture)
129 # Dynamically add fixture to caller's module as explained in https://github.com/pytest-dev/pytest/issues/2424
130 check_name_available(fixtures_dest, name, if_name_exists=WARN, caller=caller)
131 setattr(fixtures_dest, name, fix)
133 # if unpacking is requested, do it here
134 if unpack_into is not None: 134 ↛ 137line 134 didn't jump to line 137, because the condition on line 134 was never true
135 # note that as for fixture unions, we can not expose the `in_cls` parameter.
136 # but there is an easy workaround if unpacking is needed: call unpack_fixture separately
137 _make_unpack_fixture(fixtures_dest, argnames=unpack_into, fixture=name, hook=hook, in_cls=False)
139 return fix
142_make_fixture_product = _fixture_product
143"""A readable alias for callers not using the returned symbol"""
146class fixture_ref(object): # noqa
147 """
148 A reference to a fixture, to be used in `@parametrize`.
149 You can create it from a fixture name or a fixture object (function).
150 """
151 __slots__ = 'fixture', 'theoretical_size', '_id'
153 def __init__(self,
154 fixture, # type: Union[str, Callable]
155 id=None, # type: str # noqa
156 ):
157 """
159 :param fixture: the name of the fixture to reference, or the fixture function itself
160 :param id: an optional custom id to override the fixture name in ids.
161 """
162 self.fixture = get_fixture_name(fixture)
163 self._id = id
164 self.theoretical_size = None # we dont know yet, will be filled by @parametrize
166 def get_name_for_id(self):
167 """return the name to use in ids."""
168 return self._id if self._id is not None else self.fixture
170 def __str__(self):
171 # used in mini_idval for example
172 return self.get_name_for_id()
174 def __repr__(self):
175 if self._id is not None: 175 ↛ 176line 175 didn't jump to line 176, because the condition on line 175 was never true
176 return "fixture_ref<%s, id=%s>" % (self.fixture, self._id)
177 else:
178 return "fixture_ref<%s>" % self.fixture
180 def _check_iterable(self):
181 """Raise a TypeError if this fixture reference is not iterable, that is, it does not represent a tuple"""
182 if self.theoretical_size is None:
183 raise TypeError("This `fixture_ref` has not yet been initialized, so it cannot be unpacked/iterated upon. "
184 "This is not supposed to happen when a `fixture_ref` is used correctly, i.e. as an item in"
185 " the `argvalues` of a `@parametrize` decorator. Please check the documentation for "
186 "details.")
187 if self.theoretical_size == 1: 187 ↛ 188line 187 didn't jump to line 188, because the condition on line 187 was never true
188 raise TypeError("This fixture_ref does not represent a tuple of arguments, it is not iterable")
190 def __len__(self):
191 self._check_iterable()
192 return self.theoretical_size
194 def __getitem__(self, item):
195 """
196 Returns an item in the tuple described by this fixture_ref.
197 This is just a facade, a FixtureRefItem.
198 Note: this is only used when a custom `idgen` is passed to @parametrized
199 """
200 self._check_iterable()
201 return FixtureRefItem(self, item)
204class FixtureRefItem(object):
205 """An item in a fixture_ref when this fixture_ref is used as a tuple."""
206 __slots__ = 'host', 'item'
208 def __init__(self,
209 host, # type: fixture_ref
210 item # type: int
211 ):
212 self.host = host
213 self.item = item
215 def __repr__(self):
216 return "%r[%s]" % (self.host, self.item)
219# Fix for https://github.com/smarie/python-pytest-cases/issues/71
220# In order for pytest to allow users to import this symbol in conftest.py
221# they should be declared as optional plugin hooks.
222# A workaround otherwise would be to remove the 'pytest_' name prefix
223# See https://github.com/pytest-dev/pytest/issues/6475
224@pytest.hookimpl(optionalhook=True)
225def pytest_parametrize_plus(*args,
226 **kwargs):
227 warn("`pytest_parametrize_plus` and `parametrize_plus` are deprecated. Please use the new alias `parametrize`. "
228 "See https://github.com/pytest-dev/pytest/issues/6475", category=DeprecationWarning, stacklevel=2)
229 return parametrize(*args, **kwargs)
232parametrize_plus = pytest_parametrize_plus
235class ParamAlternative(UnionFixtureAlternative):
236 """Defines an "alternative", used to parametrize a fixture union in the context of parametrize
238 It is similar to a union fixture alternative, except that it also remembers the parameter argnames.
239 They are used to generate the test id corresponding to this alternative. See `_get_minimal_id` implementations.
240 `ParamIdMakers` overrides some of the idstyles in `UnionIdMakers` so as to adapt them to these `ParamAlternative`
241 objects.
242 """
243 __slots__ = ('argnames', 'decorated')
245 def __init__(self,
246 union_name, # type: str
247 alternative_name, # type: str
248 param_index, # type: int
249 argnames, # type: Sequence[str]
250 decorated # type: Callable
251 ):
252 """
254 :param union_name: the name of the union fixture created by @parametrize to switch between param alternatives
255 :param alternative_name: the name of the fixture created by @parametrize to represent this alternative
256 :param param_index: the index of this parameter in the list of argvalues passed to @parametrize
257 :param argnames: the list of parameter names in @parametrize
258 :param decorated: the test function or fixture that this alternative refers to
259 """
260 super(ParamAlternative, self).__init__(union_name=union_name, alternative_name=alternative_name,
261 alternative_index=param_index)
262 self.argnames = argnames
263 self.decorated = decorated
265 def get_union_id(self):
266 return ("(%s)" % ",".join(self.argnames)) if len(self.argnames) > 1 else self.argnames[0]
268 def get_alternative_idx(self):
269 return "P%s" % self.alternative_index
271 def get_alternative_id(self):
272 """Subclasses should return the smallest id representing this parametrize fixture union alternative"""
273 raise NotImplementedError()
276class SingleParamAlternative(ParamAlternative):
277 """alternative class for single parameter value"""
278 __slots__ = 'argval', 'id'
280 def __init__(self,
281 union_name, # type: str
282 alternative_name, # type: str
283 param_index, # type: int
284 argnames, # type: Sequence[str]
285 argval, # type: Any
286 id, # type: Optional[str]
287 decorated # type: Callable
288 ):
289 """
290 :param union_name: the name of the union fixture created by @parametrize to switch between param alternatives
291 :param alternative_name: the name of the fixture created by @parametrize to represent this alternative
292 :param param_index: the index of this parameter in the list of argvalues passed to @parametrize
293 :param argnames: the list of parameter names in @parametrize
294 :param argval: the value used by this parameter
295 """
296 super(SingleParamAlternative, self).__init__(union_name=union_name, alternative_name=alternative_name,
297 param_index=param_index, argnames=argnames, decorated=decorated)
298 self.argval = argval
299 self.id = id
301 def get_alternative_id(self):
302 """Since this alternative has no further parametrization (simplification for 1-param alternative),
303 we create here the equivalent of the id of the argvalue if it was used as a parameter"""
304 if self.id is not None:
305 # custom id from `@parametrize(ids=<callable_or_list>)`
306 return self.id
307 else:
308 return mini_idvalset(self.argnames, self.argval, idx=self.alternative_index)
310 @classmethod
311 def create(cls,
312 new_fixture_host, # type: Union[Type, ModuleType]
313 test_func, # type: Callable
314 param_union_name, # type: str
315 argnames, # type: Sequence[str]
316 i, # type: int
317 argvalue, # type: Any
318 id, # type: Union[str, Callable]
319 scope=None, # type: str
320 hook=None, # type: Callable
321 debug=False # type: bool
322 ):
323 # type: (...) -> SingleParamAlternative
324 """
325 Creates an alternative for fixture union `param_union_name`, to handle single parameter value
326 argvalue = argvalues[i] in @parametrize.
328 This alternative will refer to a newly created fixture in `new_fixture_host`, that will return `argvalue`.
330 :param new_fixture_host: host (class, module) where the new fixture should be created
331 :param test_func:
332 :param param_union_name:
333 :param argnames:
334 :param i:
335 :param argvalue: a (possibly marked with pytest.param) argvalue
336 :param hook:
337 :param debug:
338 :return:
339 """
340 nb_params = len(argnames)
341 param_names_str = '_'.join(argnames).replace(' ', '')
343 # Create a unique fixture name
344 p_fix_name = "%s_%s_P%s" % (test_func.__name__, param_names_str, i)
345 p_fix_name = check_name_available(new_fixture_host, p_fix_name, if_name_exists=CHANGE, caller=parametrize)
347 if debug:
348 print(" - Creating new fixture %r to handle parameter %s" % (p_fix_name, i))
350 # Now we'll create the fixture that will return the unique parameter value
351 # since this parameter is unique, we do not parametrize the fixture (_create_param_fixture "auto_simplify" flag)
352 # for this reason the possible pytest.param ids and marks have to be set somewhere else: we move them
353 # to the alternative.
355 # unwrap possible pytest.param on the argvalue to move them on the SingleParamAlternative
356 has_pytestparam_wrapper = is_marked_parameter_value(argvalue)
357 if has_pytestparam_wrapper:
358 p_id = get_marked_parameter_id(argvalue)
359 p_marks = get_marked_parameter_marks(argvalue)
360 argvalue = get_marked_parameter_values(argvalue, nbargs=nb_params)
361 if nb_params == 1:
362 argvalue = argvalue[0]
364 # Create the fixture. IMPORTANT auto_simplify=True : we create a NON-parametrized fixture.
365 _create_param_fixture(new_fixture_host, argname=p_fix_name, argvalues=(argvalue,),
366 scope=scope, hook=hook, auto_simplify=True, debug=debug)
368 # Create the alternative
369 argvals = (argvalue,) if nb_params == 1 else argvalue
370 p_fix_alt = SingleParamAlternative(union_name=param_union_name, alternative_name=p_fix_name,
371 argnames=argnames, param_index=i, argval=argvals, id=id,
372 decorated=test_func)
374 # Finally copy the custom id/marks on the ParamAlternative if any
375 if has_pytestparam_wrapper:
376 p_fix_alt = ParameterSet(values=(p_fix_alt,), id=p_id, marks=p_marks) # noqa
378 return p_fix_alt
381class MultiParamAlternative(ParamAlternative):
382 """alternative class for multiple parameter values"""
383 __slots__ = 'param_index_from', 'param_index_to'
385 def __init__(self,
386 union_name, # type: str
387 alternative_name, # type: str
388 argnames, # type: Sequence[str]
389 param_index_from, # type: int
390 param_index_to, # type: int
391 decorated # type: Callable
392 ):
393 """
395 :param union_name: the name of the union fixture created by @parametrize to switch between param alternatives
396 :param alternative_name: the name of the fixture created by @parametrize to represent this alternative
397 :param argnames: the list of parameter names in @parametrize
398 :param param_index_from: the beginning index of the parameters covered by <alternative_name> in the list of
399 argvalues passed to @parametrize
400 :param param_index_to: the ending index of the parameters covered by <alternative_name> in the list of
401 argvalues passed to @parametrize
402 """
403 # set the param_index to be None since here we represent several indices
404 super(MultiParamAlternative, self).__init__(union_name=union_name, alternative_name=alternative_name,
405 argnames=argnames, param_index=None, decorated=decorated # noqa
406 )
407 self.param_index_from = param_index_from
408 self.param_index_to = param_index_to
410 def __str__(self):
411 return "%s/%s/" % (self.get_union_id(), self.get_alternative_idx())
413 def get_alternative_idx(self):
414 return "P%s:%s" % (self.param_index_from, self.param_index_to)
416 def get_alternative_id(self):
417 # The alternative id is the parameter range - the parameter themselves appear on the referenced fixture
418 return self.get_alternative_idx()
420 @classmethod
421 def create(cls,
422 new_fixture_host, # type: Union[Type, ModuleType]
423 test_func, # type: Callable
424 param_union_name, # type: str
425 argnames, # type: Sequence[str]
426 from_i, # type: int
427 to_i, # type: int
428 argvalues, # type: Any
429 ids, # type: Union[Sequence[str], Callable]
430 scope="function", # type: str
431 hook=None, # type: Callable
432 debug=False # type: bool
433 ):
434 # type: (...) -> MultiParamAlternative
435 """
436 Creates an alternative for fixture union `param_union_name`, to handle a group of consecutive parameters
437 argvalues[from_i:to_i] in @parametrize. Note that here the received `argvalues` should be already sliced
439 This alternative will refer to a newly created fixture in `new_fixture_host`, that will be parametrized to
440 return each of `argvalues`.
442 :param new_fixture_host:
443 :param test_func:
444 :param param_union_name:
445 :param argnames:
446 :param from_i:
447 :param to_i:
448 :param argvalues:
449 :param hook:
450 :param debug:
451 :return:
452 """
453 nb_params = len(argnames)
454 param_names_str = '_'.join(argnames).replace(' ', '')
456 # Create a unique fixture name
457 p_fix_name = "%s_%s_is_P%stoP%s" % (test_func.__name__, param_names_str, from_i, to_i - 1)
458 p_fix_name = check_name_available(new_fixture_host, p_fix_name, if_name_exists=CHANGE, caller=parametrize)
460 if debug:
461 print(" - Creating new fixture %r to handle parameters %s to %s" % (p_fix_name, from_i, to_i - 1))
463 # Create the fixture
464 # - it will be parametrized to take all the values in argvalues
465 # - therefore it will use the custom ids and marks if any
466 # - it will be unique (not unfolded) so if there are more than 1 argnames we have to add a layer of tuple in the
467 # values
469 if nb_params > 1:
470 # we have to create a tuple around the vals because we have a SINGLE parameter that is a tuple
471 unmarked_argvalues = []
472 new_argvals = []
473 for v in argvalues:
474 if is_marked_parameter_value(v):
475 # transform the parameterset so that it contains a tuple of length 1
476 vals = get_marked_parameter_values(v, nbargs=nb_params)
477 if nb_params == 1: 477 ↛ 478line 477 didn't jump to line 478, because the condition on line 477 was never true
478 vals = vals[0]
479 unmarked_argvalues.append(vals)
480 new_argvals.append(ParameterSet((vals,),
481 id=get_marked_parameter_id(v),
482 marks=get_marked_parameter_marks(v)))
483 else:
484 # nothing special to do since there is no pytest.param here
485 new_argvals.append(v)
486 unmarked_argvalues.append(v)
487 argvalues = new_argvals
489 # we also have to generate the ids correctly "as if they were multiple"
490 try:
491 iter(ids)
492 except TypeError:
493 if ids is not None: 493 ↛ 494line 493 didn't jump to line 494, because the condition on line 493 was never true
494 ids = ["-".join(ids(vi) for vi in v) for v in unmarked_argvalues]
495 else:
496 ids = [mini_idvalset(argnames, vals, i) for i, vals in enumerate(unmarked_argvalues)]
498 _create_param_fixture(new_fixture_host, argname=p_fix_name, argvalues=argvalues, ids=ids,
499 scope=scope, hook=hook, debug=debug)
501 # Create the corresponding alternative
502 # note: as opposed to SingleParamAlternative, no need to move the custom id/marks to the ParamAlternative
503 # since they are set on the created parametrized fixture above
504 return MultiParamAlternative(union_name=param_union_name, alternative_name=p_fix_name, argnames=argnames,
505 param_index_from=from_i, param_index_to=to_i, decorated=test_func)
508class FixtureParamAlternative(SingleParamAlternative):
509 """alternative class for a single parameter containing a fixture ref"""
511 def __init__(self,
512 union_name, # type: str
513 fixture_ref, # type: fixture_ref
514 argnames, # type: Sequence[str]
515 param_index, # type: int
516 id, # type: Optional[str]
517 decorated # type: Callable
518 ):
519 """
520 :param union_name: the name of the union fixture created by @parametrize to switch between param alternatives
521 :param param_index: the index of this parameter in the list of argvalues passed to @parametrize
522 :param argnames: the list of parameter names in @parametrize
523 :param fixture_ref: the fixture reference used in this alternative
524 """
525 # set alternative_name using the fixture name in fixture_ref
526 super(FixtureParamAlternative, self).__init__(union_name=union_name,
527 alternative_name=fixture_ref.fixture,
528 argnames=argnames, param_index=param_index,
529 argval=fixture_ref, id=id, decorated=decorated)
531 def get_alternative_idx(self):
532 return "P%sF" % self.alternative_index
534 def get_alternative_id(self):
535 if self.id is not None:
536 # custom id from `@parametrize(ids=<callable_or_list>)`
537 return self.id
538 else:
539 # ask the fixture_ref for an id: it can be the fixture name or a custom id
540 return self.argval.get_name_for_id()
543class ProductParamAlternative(SingleParamAlternative):
544 """alternative class for a single product parameter containing fixture refs"""
546 def get_alternative_idx(self):
547 return "P%sF" % self.alternative_index
549 def get_alternative_id(self):
550 """Similar to SingleParamAlternative: create an id representing this tuple, since the fixture won't be
551 parametrized"""
552 if self.id is not None: 552 ↛ 554line 552 didn't jump to line 554, because the condition on line 552 was never true
553 # custom id from `@parametrize(ids=<callable_or_list>)`
554 return self.id
555 else:
556 argval = tuple(t if not robust_isinstance(t, fixture_ref) else t.get_name_for_id() for t in self.argval)
557 return mini_idvalset(self.argnames, argval, idx=self.alternative_index)
560# if PYTEST54_OR_GREATER:
561# # an empty string will be taken into account but NOT filtered out in CallSpec2.id.
562# # so instead we create a dedicated unique string and return it.
563# # Ugly but the only viable alternative seems worse: it would be to return an empty string
564# # and in `remove_empty_ids` to always remove all empty strings (not necessary the ones set by us).
565# # That is too much of a change.
567EMPTY_ID = "<pytest_cases_empty_id>"
570if has_pytest_param: 570 ↛ 575line 570 didn't jump to line 575, because the condition on line 570 was never false
571 def remove_empty_ids(callspec):
572 # used by plugin.py to remove the EMPTY_ID from the callspecs
573 replace_list_contents(callspec._idlist, [c for c in callspec._idlist if not c.startswith(EMPTY_ID)])
574else:
575 def remove_empty_ids(callspec):
576 # used by plugin.py to remove the EMPTY_ID from the callspecs
577 replace_list_contents(callspec._idlist, [c for c in callspec._idlist if not c.endswith(EMPTY_ID)])
580# elif PYTEST421_OR_GREATER:
581# # an empty string will be taken into account and filtered out in CallSpec2.id.
582# # but.... if this empty string appears several times in the tests it is appended with a number to become unique :(
583# EMPTY_ID = ""
584#
585# else:
586# # an empty string will only be taken into account if its truth value is True
587# # but.... if this empty string appears several times in the tests it is appended with a number to become unique :(
588# # it will be filtered out in CallSpec2.id
589# class EmptyId(str):
590# def __new__(cls):
591# return str.__new__(cls, "")
592#
593# def __nonzero__(self):
594# # python 2
595# return True
596#
597# def __bool__(self):
598# # python 3
599# return True
600#
601# EMPTY_ID = EmptyId()
604class ParamIdMakers(UnionIdMakers):
605 """ 'Enum' of id styles for param ids
607 It extends UnionIdMakers to adapt to the special fixture alternatives `ParamAlternative` we create
608 in @parametrize
609 """
610 @classmethod
611 def nostyle(cls,
612 param # type: ParamAlternative
613 ):
614 if isinstance(param, MultiParamAlternative):
615 # make an empty minimal id since the parameter themselves will appear as ids separately
616 # note if the final id is empty it will be dropped by the filter in CallSpec2.id
617 return EMPTY_ID
618 else:
619 return UnionIdMakers.nostyle(param)
621 # @classmethod
622 # def explicit(cls,
623 # param # type: ParamAlternative
624 # ):
625 # """Same than parent but display the argnames as prefix instead of the fixture union name generated by
626 # @parametrize, because the latter is too complex (for unicity reasons)"""
627 # return "%s/%s" % (, param.get_id(prepend_index=True))
630_IDGEN = object()
633def parametrize(argnames=None, # type: Union[str, Tuple[str], List[str]]
634 argvalues=None, # type: Iterable[Any]
635 indirect=False, # type: bool
636 ids=None, # type: Union[Callable, Iterable[str]]
637 idstyle=None, # type: Union[str, Callable]
638 idgen=_IDGEN, # type: Union[str, Callable]
639 auto_refs=True, # type: bool
640 scope=None, # type: str
641 hook=None, # type: Callable[[Callable], Callable]
642 debug=False, # type: bool
643 **args):
644 # type: (...) -> Callable[[T], T]
645 """
646 Equivalent to `@pytest.mark.parametrize` but also supports
648 (1) new alternate style for argnames/argvalues. One can also use `**args` to pass additional `{argnames: argvalues}`
649 in the same parametrization call. This can be handy in combination with `idgen` to master the whole id template
650 associated with several parameters. Note that you can pass coma-separated argnames too, by de-referencing a dict:
651 e.g. `**{'a,b': [(0, True), (1, False)], 'c': [-1, 2]}`.
653 (2) new alternate style for ids. One can use `idgen` instead of `ids`. `idgen` can be a callable receiving all
654 parameters at once (`**args`) and returning an id ; or it can be a string template using the new-style string
655 formatting where the argnames can be used as variables (e.g. `idgen=lambda **args: "a={a}".format(**args)` or
656 `idgen="my_id where a={a}"`). The special `idgen=AUTO` symbol can be used to generate a default string template
657 equivalent to `lambda **args: "-".join("%s=%s" % (n, v) for n, v in args.items())`. This is enabled by default
658 if you use the alternate style for argnames/argvalues (e.g. if `len(args) > 0`), and if there are no `fixture_ref`s
659 in your argvalues.
661 (3) new possibilities in argvalues:
663 - one can include references to fixtures with `fixture_ref(<fixture>)` where <fixture> can be the fixture name or
664 fixture function. When such a fixture reference is detected in the argvalues, a new function-scope "union"
665 fixture will be created with a unique name, and the test function will be wrapped so as to be injected with the
666 correct parameters from this fixture. Special test ids will be created to illustrate the switching between the
667 various normal parameters and fixtures. You can see debug print messages about all fixtures created using
668 `debug=True`
670 - one can include lazy argvalues with `lazy_value(<valuegetter>, [id=..., marks=...])`. A `lazy_value` is the same
671 thing than a function-scoped fixture, except that the value getter function is not a fixture and therefore can
672 neither be parametrized nor depend on fixtures. It should have no mandatory argument.
674 Both `fixture_ref` and `lazy_value` can be used to represent a single argvalue, or a whole tuple of argvalues when
675 there are several argnames. Several of them can be used in a tuple.
677 Finally, `pytest.param` is supported even when there are `fixture_ref` and `lazy_value`.
679 An optional `hook` can be passed, to apply on each fixture function that is created during this call. The hook
680 function will be called every time a fixture is about to be created. It will receive a single argument (the
681 function implementing the fixture) and should return the function to use. For example you can use `saved_fixture`
682 from `pytest-harvest` as a hook in order to save all such created fixtures in the fixture store.
684 :param argnames: same as in pytest.mark.parametrize
685 :param argvalues: same as in pytest.mark.parametrize except that `fixture_ref` and `lazy_value` are supported
686 :param indirect: same as in pytest.mark.parametrize. Note that it is not recommended and is not guaranteed to work
687 in complex parametrization scenarii.
688 :param ids: same as in pytest.mark.parametrize. Note that an alternative way to create ids exists with `idgen`. Only
689 one non-None `ids` or `idgen should be provided.
690 :param idgen: an id formatter. Either a string representing a template, or a callable receiving all argvalues
691 at once (as opposed to the behaviour in pytest ids). This alternative way to generate ids can only be used when
692 `ids` is not provided (None). You can use the special `AUTO` formatter to generate an automatic id with
693 template <name>=<value>-<name2>=<value2>-etc. `AUTO` is enabled by default if you use the alternate style for
694 argnames/argvalues (e.g. if `len(args) > 0`), and if there are no `fixture_ref`s in your argvalues.
695 :param auto_refs: a boolean. If this is `True` (default), argvalues containing fixture symbols will automatically
696 be wrapped into a `fixture_ref`, for convenience.
697 :param idstyle: This is mostly for debug. Style of ids to be used in the "union" fixtures generated by
698 `@parametrize` if at least one `fixture_ref` is found in the argvalues. `idstyle` possible values are
699 'compact', 'explicit' or None/'nostyle' (default), or a callable. `idstyle` has no effect if no `fixture_ref`
700 are present in the argvalues. As opposed to `ids`, a callable provided here will receive a `ParamAlternative`
701 object indicating which generated fixture should be used. See `ParamIdMakers`.
702 :param scope: The scope of the union fixture to create if `fixture_ref`s are found in the argvalues. Otherwise same
703 as in pytest.mark.parametrize.
704 :param hook: an optional hook to apply to each fixture function that is created during this call. The hook function
705 will be called every time a fixture is about to be created. It will receive a single argument (the function
706 implementing the fixture) and should return the function to use. For example you can use `saved_fixture` from
707 `pytest-harvest` as a hook in order to save all such created fixtures in the fixture store.
708 :param debug: print debug messages on stdout to analyze fixture creation (use pytest -s to see them)
709 :param args: additional {argnames: argvalues} definition
710 :return:
711 """
712 _decorate, needs_inject = _parametrize_plus(argnames, argvalues, indirect=indirect, ids=ids, idgen=idgen,
713 auto_refs=auto_refs, idstyle=idstyle, scope=scope,
714 hook=hook, debug=debug, **args)
715 if needs_inject:
716 @inject_host
717 def _apply_parametrize_plus(f, host_class_or_module):
718 return _decorate(f, host_class_or_module)
719 return _apply_parametrize_plus
720 else:
721 return _decorate
724class InvalidIdTemplateException(Exception):
725 """
726 Raised when a string template provided in an `idgen` raises an error
727 """
728 def __init__(self, idgen, params, caught):
729 self.idgen = idgen
730 self.params = params
731 self.caught = caught
732 super(InvalidIdTemplateException, self).__init__()
734 def __str__(self):
735 return repr(self)
737 def __repr__(self):
738 return "Error generating test id using name template '%s' with parameter values " \
739 "%r. Please check the name template. Caught: %s - %s" \
740 % (self.idgen, self.params, self.caught.__class__, self.caught)
743def _parametrize_plus(argnames=None, # type: Union[str, Tuple[str], List[str]]
744 argvalues=None, # type: Iterable[Any]
745 indirect=False, # type: bool
746 ids=None, # type: Union[Callable, Iterable[str]]
747 idstyle=None, # type: Optional[Union[str, Callable]]
748 idgen=_IDGEN, # type: Union[str, Callable]
749 auto_refs=True, # type: bool
750 scope=None, # type: str
751 hook=None, # type: Callable[[Callable], Callable]
752 debug=False, # type: bool
753 **args):
754 # type: (...) -> Tuple[Callable[[T], T], bool]
755 """
757 :return: a tuple (decorator, needs_inject) where needs_inject is True if decorator has signature (f, host)
758 and False if decorator has signature (f)
759 """
760 # first handle argnames / argvalues (new modes of input)
761 argnames, argvalues = _get_argnames_argvalues(argnames, argvalues, **args)
763 # argnames related
764 initial_argnames = ','.join(argnames)
765 nb_params = len(argnames)
767 # extract all marks and custom ids.
768 # Do not check consistency of sizes argname/argvalue as a fixture_ref can stand for several argvalues.
769 marked_argvalues = argvalues
770 has_cust_ids = (idgen is not _IDGEN or len(args) > 0) or (ids is not None)
771 p_ids, p_marks, argvalues, fixture_indices, mod_lvid_indices = \
772 _process_argvalues(argnames, marked_argvalues, nb_params, has_cust_ids, auto_refs=auto_refs)
774 # idgen default
775 if idgen is _IDGEN:
776 # default: use the new id style only when some keyword **args are provided and there are no fixture refs
777 idgen = AUTO if (len(args) > 0 and len(fixture_indices) == 0 and ids is None) else None
779 if idgen is AUTO:
780 # note: we use a "trick" here with mini_idval to get the appropriate result (argname='', idx=v)
781 def _make_ids(**args):
782 for n, v in args.items():
783 yield "%s=%s" % (n, mini_idval(val=v, argname='', idx=v))
785 idgen = lambda **args: "-".join(_make_ids(**args)) # noqa
787 # generate id
788 if idgen is not None:
789 if ids is not None: 789 ↛ 790line 789 didn't jump to line 790, because the condition on line 789 was never true
790 raise ValueError("Only one of `ids` and `idgen` should be provided")
791 ids = _gen_ids(argnames, argvalues, idgen)
793 if len(fixture_indices) == 0:
794 # No fixture reference: fallback to a standard pytest.mark.parametrize
795 if debug: 795 ↛ 796line 795 didn't jump to line 796, because the condition on line 795 was never true
796 print("No fixture reference found. Calling @pytest.mark.parametrize...")
797 print(" - argnames: %s" % initial_argnames)
798 print(" - argvalues: %s" % marked_argvalues)
799 print(" - ids: %s" % ids)
801 # handle infinite iterables like latest pytest, for convenience
802 ids = resolve_ids(ids, marked_argvalues, full_resolve=False)
804 # no fixture reference: shortcut, do as usual (note that the hook won't be called since no fixture is created)
805 _decorator = pytest.mark.parametrize(initial_argnames, marked_argvalues, indirect=indirect,
806 ids=ids, scope=scope)
807 if indirect:
808 return _decorator, False
809 else:
810 # wrap the decorator to check if the test function has the parameters as arguments
811 def _apply(test_func):
812 # type: (...) -> Callable[[T], T]
813 if not safe_isclass(test_func):
814 # a Function: raise a proper error message if improper use
815 s = signature(test_func)
816 for p in argnames:
817 if p not in s.parameters:
818 raise ValueError("parameter '%s' not found in test function signature '%s%s'"
819 "" % (p, test_func.__name__, s))
820 else:
821 # a Class: we cannot really perform any check.
822 pass
823 return _decorator(test_func)
825 return _apply, False
827 else:
828 # there are fixture references: we will create a specific decorator replacing the params with a "union" fixture
829 if indirect: 829 ↛ 830line 829 didn't jump to line 830, because the condition on line 829 was never true
830 warn("Using `indirect=True` at the same time as fixture references in `@parametrize` is not guaranteed to "
831 "work and is strongly discouraged for readability reasons. See "
832 "https://github.com/smarie/python-pytest-cases/issues/150")
834 # First unset the pytest.param id we have set earlier in _process_argvalues: indeed it is only needed in
835 # the case above where we were defaulting to legacy @pytest.mark.parametrize .
836 # Here we have fixture refs so we will create a fixture union with several ParamAlternative, and their id will
837 # anyway be generated with `mini_idvalset` which tackles the case of lazy_value used for a tuple of args
838 for i in mod_lvid_indices:
839 p_ids[i] = None
840 if p_marks[i]:
841 marked_argvalues[i] = ParameterSet(values=marked_argvalues[i].values, id=None, marks=p_marks[i])
842 else:
843 marked_argvalues[i] = argvalues[i] # we can even remove the pytest.param wrapper
845 if indirect: 845 ↛ 846line 845 didn't jump to line 846, because the condition on line 845 was never true
846 raise ValueError("Setting `indirect=True` is not yet supported when at least a `fixure_ref` is present in "
847 "the `argvalues`.")
849 if debug:
850 print("Fixture references found. Creating references and fixtures...")
852 param_names_str = '_'.join(argnames).replace(' ', '')
854 # Are there explicit ids provided ?
855 explicit_ids_to_use = False
856 ids = resolve_ids(ids, argvalues, full_resolve=False)
857 if isinstance(ids, list):
858 explicit_ids_to_use = True
860 # First define a few functions that will help us create the various fixtures to use in the final "union"
862 def _create_params_alt(fh, test_func, union_name, from_i, to_i, hook): # noqa
863 """ Routine that will be used to create a parameter fixture for argvalues between prev_i and i"""
865 # is this about a single value or several values ?
866 if to_i == from_i + 1:
867 i = from_i
868 del from_i
870 # If an explicit list of ids was provided, slice it. Otherwise use the provided callable
871 if ids is not None:
872 _id = ids[i] if explicit_ids_to_use else ids(argvalues[i])
873 else:
874 _id = None
876 return SingleParamAlternative.create(new_fixture_host=fh, test_func=test_func,
877 param_union_name=union_name, argnames=argnames, i=i,
878 argvalue=marked_argvalues[i], id=_id,
879 scope="function" if scope is None else scope,
880 hook=hook, debug=debug)
881 else:
882 # If an explicit list of ids was provided, slice it. Otherwise the provided callable will be used later
883 _ids = ids[from_i:to_i] if explicit_ids_to_use else ids
885 return MultiParamAlternative.create(new_fixture_host=fh, test_func=test_func,
886 param_union_name=union_name, argnames=argnames, from_i=from_i,
887 to_i=to_i, argvalues=marked_argvalues[from_i:to_i], ids=_ids,
888 scope="function" if scope is None else scope,
889 hook=hook, debug=debug)
892 def _create_fixture_ref_alt(union_name, test_func, i): # noqa
894 # If an explicit list of ids was provided, slice it. Otherwise use the provided callable
895 if ids is not None:
896 _id = ids[i] if explicit_ids_to_use else ids(argvalues[i])
897 else:
898 _id = None
900 # Get the referenced fixture name
901 f_fix_name = argvalues[i].fixture
903 if debug:
904 print(" - Creating reference to existing fixture %r" % (f_fix_name,))
906 # Create the alternative
907 f_fix_alt = FixtureParamAlternative(union_name=union_name, fixture_ref=argvalues[i],
908 decorated=test_func, argnames=argnames, param_index=i, id=_id)
909 # Finally copy the custom id/marks on the FixtureParamAlternative if any
910 if is_marked_parameter_value(marked_argvalues[i]):
911 f_fix_alt = ParameterSet(values=(f_fix_alt,),
912 id=get_marked_parameter_id(marked_argvalues[i]),
913 marks=get_marked_parameter_marks(marked_argvalues[i]))
915 return f_fix_alt
917 def _create_fixture_ref_product(fh, union_name, i, fixture_ref_positions, test_func, hook): # noqa
919 # If an explicit list of ids was provided, slice it. Otherwise the provided callable will be used
920 _id = ids[i] if explicit_ids_to_use else ids
922 # values to use:
923 param_values = argvalues[i]
925 # Create a unique fixture name
926 p_fix_name = "%s_%s_P%s" % (test_func.__name__, param_names_str, i)
927 p_fix_name = check_name_available(fh, p_fix_name, if_name_exists=CHANGE, caller=parametrize)
929 if debug:
930 print(" - Creating new fixture %r to handle parameter %s that is a cross-product" % (p_fix_name, i))
932 # Create the fixture
933 _make_fixture_product(fh, name=p_fix_name, hook=hook, caller=parametrize,
934 fixtures_or_values=param_values, fixture_positions=fixture_ref_positions)
936 # Create the corresponding alternative
937 p_fix_alt = ProductParamAlternative(union_name=union_name, alternative_name=p_fix_name, decorated=test_func,
938 argval=argvalues[i], argnames=argnames, param_index=i, id=_id)
939 # copy the custom id/marks to the ParamAlternative if any
940 if is_marked_parameter_value(marked_argvalues[i]):
941 p_fix_alt = ParameterSet(values=(p_fix_alt,),
942 id=get_marked_parameter_id(marked_argvalues[i]),
943 marks=get_marked_parameter_marks(marked_argvalues[i]))
944 return p_fix_alt
946 # Then create the decorator per se
947 def parametrize_plus_decorate(test_func, fixtures_dest):
948 # type: (...) -> Callable[[T], T]
949 """
950 A decorator that wraps the test function so that instead of receiving the parameter names, it receives the
951 new fixture. All other decorations are unchanged.
953 :param test_func:
954 :return:
955 """
956 test_func_name = test_func.__name__
958 # first check if the test function has the parameters as arguments
959 if safe_isclass(test_func):
960 # a test class: not supported yet
961 raise NotImplementedError("@parametrize can not be used to decorate a Test class when the argvalues "
962 "contain at least one reference to a fixture.")
964 old_sig = signature(test_func)
965 for p in argnames:
966 if p not in old_sig.parameters: 966 ↛ 967line 966 didn't jump to line 967, because the condition on line 966 was never true
967 raise ValueError("parameter '%s' not found in test function signature '%s%s'"
968 "" % (p, test_func_name, old_sig))
970 # The name for the final "union" fixture
971 # style_template = "%s_param__%s"
972 main_fixture_style_template = "%s_%s"
973 fixture_union_name = main_fixture_style_template % (test_func_name, param_names_str)
974 fixture_union_name = check_name_available(fixtures_dest, fixture_union_name, if_name_exists=CHANGE,
975 caller=parametrize)
977 # Retrieve (if ref) or create (for normal argvalues) the fixtures that we will union
978 fixture_alternatives = []
979 prev_i = -1
980 for i, j_list in fixture_indices: # noqa
981 # A/ Is there any non-empty group of 'normal' parameters before the fixture_ref at <i> ? If so, handle.
982 if i > prev_i + 1:
983 # create a new "param" fixture parametrized with all of that consecutive group.
984 # Important note: we could either wish to create one fixture for parameter value or to create
985 # one for each consecutive group as shown below. This should not lead to different results but perf
986 # might differ. Maybe add a parameter in the signature so that users can test it ?
987 # this would make the ids more readable by removing the "P2toP3"-like ids
988 p_fix_alt = _create_params_alt(fixtures_dest, test_func=test_func, hook=hook,
989 union_name=fixture_union_name, from_i=prev_i + 1, to_i=i)
990 fixture_alternatives.append(p_fix_alt)
992 # B/ Now handle the fixture ref at position <i>
993 if j_list is None:
994 # argvalues[i] contains a single argvalue that is a fixture_ref : add the referenced fixture
995 f_fix_alt = _create_fixture_ref_alt(union_name=fixture_union_name, test_func=test_func, i=i)
996 fixture_alternatives.append(f_fix_alt)
997 else:
998 # argvalues[i] is a tuple, some of them being fixture_ref. create a fixture referring to all of them
999 prod_fix_alt = _create_fixture_ref_product(fixtures_dest, union_name=fixture_union_name, i=i,
1000 fixture_ref_positions=j_list,
1001 test_func=test_func, hook=hook)
1002 fixture_alternatives.append(prod_fix_alt)
1004 prev_i = i
1006 # C/ handle last consecutive group of normal parameters, if any
1007 i = len(argvalues) # noqa
1008 if i > prev_i + 1:
1009 p_fix_alt = _create_params_alt(fixtures_dest, test_func=test_func, hook=hook,
1010 union_name=fixture_union_name, from_i=prev_i + 1, to_i=i)
1011 fixture_alternatives.append(p_fix_alt)
1013 # if fixtures_to_union has length 1, simplify ? >> No, we leave such "optimization" to the end user
1015 # Handle the list of alternative names. Duplicates should be removed here
1016 fix_alt_names = []
1017 for alt in fixture_alternatives:
1018 if is_marked_parameter_value(alt):
1019 # wrapped by a pytest.param
1020 alt = get_marked_parameter_values(alt, nbargs=1)
1021 assert len(alt) == 1, "Error with alternative please report"
1022 alt = alt[0]
1023 if alt.alternative_name not in fix_alt_names:
1024 fix_alt_names.append(alt.alternative_name)
1025 else:
1026 # non-unique alt fixture names should only happen when the alternative is a fixture reference
1027 assert isinstance(alt, FixtureParamAlternative), "Created fixture names not unique, please report"
1029 # Finally create a "main" fixture with a unique name for this test function
1030 if debug:
1031 print("Creating final union fixture %r with alternatives %r"
1032 % (fixture_union_name, UnionFixtureAlternative.to_list_of_fixture_names(fixture_alternatives)))
1034 # use the custom subclass of idstyle that was created for ParamAlternatives
1035 if idstyle is None or isinstance(idstyle, string_types):
1036 _idstyle = ParamIdMakers.get(idstyle)
1037 else:
1038 _idstyle = idstyle
1040 # note: the function automatically registers it in the module
1041 _make_fixture_union(fixtures_dest, name=fixture_union_name, hook=hook, caller=parametrize,
1042 fix_alternatives=fixture_alternatives, unique_fix_alt_names=fix_alt_names,
1043 idstyle=_idstyle, scope=scope)
1045 # --create the new test function's signature that we want to expose to pytest
1046 # it is the same than existing, except that we want to replace all parameters with the new fixture
1047 # first check where we should insert the new parameters (where is the first param we remove)
1048 _first_idx = -1
1049 for _first_idx, _n in enumerate(old_sig.parameters): 1049 ↛ 1053line 1049 didn't jump to line 1053, because the loop on line 1049 didn't complete
1050 if _n in argnames:
1051 break
1052 # then remove all parameters that will be replaced by the new fixture
1053 new_sig = remove_signature_parameters(old_sig, *argnames)
1054 # finally insert the new fixture in that position. Indeed we can not insert first or last, because
1055 # 'self' arg (case of test class methods) should stay first and exec order should be preserved when possible
1056 new_sig = add_signature_parameters(new_sig, custom_idx=_first_idx,
1057 custom=Parameter(fixture_union_name,
1058 kind=Parameter.POSITIONAL_OR_KEYWORD))
1060 if debug:
1061 print("Creating final test function wrapper with signature %s%s" % (test_func_name, new_sig))
1063 # --Finally create the fixture function, a wrapper of user-provided fixture with the new signature
1064 def replace_paramfixture_with_values(kwargs): # noqa
1065 # remove the created fixture value
1066 encompassing_fixture = kwargs.pop(fixture_union_name)
1067 # and add instead the parameter values
1068 if nb_params > 1:
1069 for i, p in enumerate(argnames): # noqa
1070 try:
1071 kwargs[p] = encompassing_fixture[i]
1072 except TypeError:
1073 raise Exception("Unable to unpack parameter value to a tuple: %r" % encompassing_fixture)
1074 else:
1075 kwargs[argnames[0]] = encompassing_fixture
1076 # return
1077 return kwargs
1080 if isasyncgenfunction(test_func)and sys.version_info >= (3, 6): 1080 ↛ 1081line 1080 didn't jump to line 1081, because the condition on line 1080 was never true
1081 from .pep525 import _parametrize_plus_decorate_asyncgen_pep525
1082 wrapped_test_func = _parametrize_plus_decorate_asyncgen_pep525(test_func, new_sig, fixture_union_name,
1083 replace_paramfixture_with_values)
1084 elif iscoroutinefunction(test_func) and sys.version_info >= (3, 5):
1085 from .pep492 import _parametrize_plus_decorate_coroutine_pep492
1086 wrapped_test_func = _parametrize_plus_decorate_coroutine_pep492(test_func, new_sig, fixture_union_name,
1087 replace_paramfixture_with_values)
1088 elif isgeneratorfunction(test_func):
1089 # generator function (with a yield statement)
1090 if sys.version_info >= (3, 3): 1090 ↛ 1096line 1090 didn't jump to line 1096, because the condition on line 1090 was never false
1091 from .pep380 import _parametrize_plus_decorate_generator_pep380
1092 wrapped_test_func = _parametrize_plus_decorate_generator_pep380(test_func, new_sig,
1093 fixture_union_name,
1094 replace_paramfixture_with_values)
1095 else:
1096 @wraps(test_func, new_sig=new_sig)
1097 def wrapped_test_func(*args, **kwargs): # noqa
1098 if kwargs.get(fixture_union_name, None) is NOT_USED:
1099 # TODO why this ? it is probably useless: this fixture
1100 # is private and will never end up in another union
1101 yield NOT_USED
1102 else:
1103 replace_paramfixture_with_values(kwargs)
1104 for res in test_func(*args, **kwargs):
1105 yield res
1106 else:
1107 # normal function with return statement
1108 @wraps(test_func, new_sig=new_sig)
1109 def wrapped_test_func(*args, **kwargs): # noqa
1110 if kwargs.get(fixture_union_name, None) is NOT_USED: 1110 ↛ 1113line 1110 didn't jump to line 1113, because the condition on line 1110 was never true
1111 # TODO why this ? it is probably useless: this fixture
1112 # is private and will never end up in another union
1113 return NOT_USED
1114 else:
1115 replace_paramfixture_with_values(kwargs)
1116 return test_func(*args, **kwargs)
1118 # move all pytest marks from the test function to the wrapper
1119 # not needed because the __dict__ is automatically copied when we use @wraps
1120 # move_all_pytest_marks(test_func, wrapped_test_func)
1122 # With this hack we will be ordered correctly by pytest https://github.com/pytest-dev/pytest/issues/4429
1123 try:
1124 # propagate existing attribute if any
1125 wrapped_test_func.place_as = test_func.place_as
1126 except: # noqa
1127 # position the test at the original function's position
1128 wrapped_test_func.place_as = test_func
1130 # return the new test function
1131 return wrapped_test_func
1133 return parametrize_plus_decorate, True
1136def _get_argnames_argvalues(
1137 argnames=None, # type: Union[str, Tuple[str], List[str]]
1138 argvalues=None, # type: Iterable[Any]
1139 **args
1140):
1141 """
1143 :param argnames:
1144 :param argvalues:
1145 :param args:
1146 :return: argnames, argvalues - both guaranteed to be lists
1147 """
1148 # handle **args - a dict of {argnames: argvalues}
1149 if len(args) > 0:
1150 kw_argnames, kw_argvalues = cart_product_pytest(tuple(args.keys()), tuple(args.values()))
1151 else:
1152 kw_argnames, kw_argvalues = (), ()
1154 if argnames is None:
1155 # (1) all {argnames: argvalues} pairs are provided in **args
1156 if argvalues is not None or len(args) == 0: 1156 ↛ 1157line 1156 didn't jump to line 1157, because the condition on line 1156 was never true
1157 raise ValueError("No parameters provided")
1159 argnames = kw_argnames
1160 argvalues = kw_argvalues
1161 # simplify if needed to comply with pytest.mark.parametrize
1162 if len(argnames) == 1:
1163 argvalues = [_l[0] if not is_marked_parameter_value(_l) else _l for _l in argvalues]
1164 return argnames, argvalues
1166 if isinstance(argnames, string_types):
1167 # (2) argnames + argvalues, as usual. However **args can also be passed and should be added
1168 argnames = get_param_argnames_as_list(argnames)
1170 if not isinstance(argnames, (list, tuple)):
1171 raise TypeError("argnames should be a string, list or a tuple")
1173 if any([not isinstance(argname, str) for argname in argnames]):
1174 raise TypeError("all argnames should be strings")
1176 if argvalues is None: 1176 ↛ 1177line 1176 didn't jump to line 1177, because the condition on line 1176 was never true
1177 raise ValueError("No argvalues provided while argnames are provided")
1179 # transform argvalues to a list (it can be a generator)
1180 try:
1181 argvalues = list(argvalues)
1182 except TypeError:
1183 raise InvalidParamsList(argvalues)
1185 # append **args
1186 if len(kw_argnames) > 0:
1187 argnames, argvalues = cart_product_pytest((argnames, kw_argnames),
1188 (argvalues, kw_argvalues))
1190 return argnames, argvalues
1193def _gen_ids(argnames, argvalues, idgen):
1194 """
1195 Generates an explicit test ids list from a non-none `idgen`.
1197 `idgen` should be either a callable of a string template.
1199 :param argnames:
1200 :param argvalues:
1201 :param idgen:
1202 :return:
1203 """
1204 if not callable(idgen):
1205 # idgen is a new-style string formatting template
1206 if not isinstance(idgen, string_types): 1206 ↛ 1207line 1206 didn't jump to line 1207, because the condition on line 1206 was never true
1207 raise TypeError("idgen should be a callable or a string, found: %r" % idgen)
1209 _formatter = idgen
1211 def gen_id_using_str_formatter(**params):
1212 try:
1213 # format using the idgen template
1214 return _formatter.format(**params)
1215 except Exception as e:
1216 raise InvalidIdTemplateException(_formatter, params, e)
1218 idgen = gen_id_using_str_formatter
1220 if len(argnames) > 1:
1221 ids = [idgen(**{n: v for n, v in zip(argnames, _argvals)}) for _argvals in argvalues]
1222 else:
1223 _only_name = argnames[0]
1224 ids = [idgen(**{_only_name: v}) for v in argvalues]
1226 return ids
1229def _process_argvalues(argnames, marked_argvalues, nb_params, has_custom_ids, auto_refs):
1230 """Internal method to use in _pytest_parametrize_plus
1232 Processes the provided marked_argvalues (possibly marked with pytest.param) and returns
1233 p_ids, p_marks, argvalues (not marked with pytest.param), fixture_indices
1235 Note: `marked_argvalues` is modified in the process if a `lazy_value` is found with a custom id or marks.
1237 :param argnames:
1238 :param marked_argvalues:
1239 :param nb_params:
1240 :param has_custom_ids: a boolean indicating if custom ids are provided separately in `ids` or `idgen` (see
1241 @parametrize)
1242 :param auto_refs: if True, a `fixture_ref` will be created around fixture symbols used as argvalues automatically
1243 :return:
1244 """
1245 p_ids, p_marks, argvalues = extract_parameterset_info(argnames, marked_argvalues, check_nb=False)
1247 # find if there are fixture references or lazy values in the values provided
1248 fixture_indices = []
1249 mod_lvid_indices = [] # indices of lazy_values for which we created a wrapper pytest.param with an id
1250 if nb_params == 1:
1251 for i, v in enumerate(argvalues):
1252 if is_lazy_value(v):
1253 # --- A lazy value is used for several parameters at the same time ---
1254 # Users can declare custom marks in the lazy value API, we have to take these into account
1255 # (1) if there was a pytest.param around it, we have to merge the marks from the lazy value into it
1256 # (2) if there was no pytest.param around it and there are marks, we have to create the pytest.param
1257 # Note: a custom id in lazy value does not require such processing as it does not need to take
1258 # precedence over `ids` or `idgen`
1260 # are there any marks ? (either added with lazy_value(marks=), or on the function itself)
1261 _mks = v.get_marks(as_decorators=True)
1262 if len(_mks) > 0:
1263 # update/create the pytest.param marks on this value
1264 p_marks[i] = (p_marks[i] + _mks) if p_marks[i] is not None else _mks
1266 # update the original marked_argvalues. Note that argvalues[i] = v
1267 marked_argvalues[i] = ParameterSet(values=(argvalues[i],), id=p_ids[i], marks=p_marks[i])
1268 else:
1269 if auto_refs and is_fixture(v):
1270 # auto create wrapper fixture_refs
1271 argvalues[i] = v = fixture_ref(v)
1272 if p_ids[i] is None and p_marks[i] is None:
1273 marked_argvalues[i] = v
1274 else:
1275 marked_argvalues[i] = ParameterSet(values=(v,), id=p_ids[i], marks=p_marks[i])
1277 if isinstance(v, fixture_ref):
1278 # Fix the referenced fixture length
1279 v.theoretical_size = nb_params
1280 fixture_indices.append((i, None))
1282 elif nb_params > 1: 1282 ↛ 1417line 1282 didn't jump to line 1417, because the condition on line 1282 was never false
1283 for i, v in enumerate(argvalues):
1285 # A/ First analyze what is the case at hand
1286 _lazyvalue_used_as_tuple = False
1287 _fixtureref_used_as_tuple = False
1288 if is_lazy_value(v):
1289 _lazyvalue_used_as_tuple = True
1290 else:
1291 if auto_refs and is_fixture(v):
1292 # auto create wrapper fixture_refs
1293 argvalues[i] = v = fixture_ref(v)
1294 if p_ids[i] is None and p_marks[i] is None: 1294 ↛ 1297line 1294 didn't jump to line 1297, because the condition on line 1294 was never false
1295 marked_argvalues[i] = v
1296 else:
1297 marked_argvalues[i] = ParameterSet(values=(v,), id=p_ids[i], marks=p_marks[i])
1299 if isinstance(v, fixture_ref):
1300 # Fix the referenced fixture length
1301 v.theoretical_size = nb_params
1302 _fixtureref_used_as_tuple = True
1303 elif len(v) == 1:
1304 # same than above but it was in a pytest.param
1305 if is_lazy_value(v[0]):
1306 argvalues[i] = v = v[0]
1307 _lazyvalue_used_as_tuple = True
1308 else:
1309 if auto_refs and is_fixture(v[0]):
1310 # auto create wrapper fixture_refs
1311 v = (fixture_ref(v[0]),)
1313 if isinstance(v[0], fixture_ref): 1313 ↛ 1324line 1313 didn't jump to line 1324, because the condition on line 1313 was never false
1314 _fixtureref_used_as_tuple = True
1315 argvalues[i] = v = v[0]
1316 if p_ids[i] is None and p_marks[i] is None: 1316 ↛ 1317line 1316 didn't jump to line 1317, because the condition on line 1316 was never true
1317 marked_argvalues[i] = v
1318 else:
1319 marked_argvalues[i] = ParameterSet(values=(v,), id=p_ids[i], marks=p_marks[i])
1320 # Fix the referenced fixture length
1321 v.theoretical_size = nb_params
1323 # B/ Now process it
1324 if _lazyvalue_used_as_tuple:
1325 # --- A lazy value is used for several parameters at the same time ---
1327 # Since users have the possibility in the lazy value API to declare a custom id or custom marks,
1328 # we have to take these into account.
1329 # MARKS:
1330 # (1) if there was a pytest.param around it, we have to merge the marks from the lazy value into it
1331 # (2) if there was no pytest.param around it and there are marks, we have to create the pytest.param
1332 # IDS:
1333 # As opposed to the case of nb_params=1, we can not let pytest generate the id as it would create a
1334 # tuple of LazyTupleItem ids (e.g. <id>[0]-<id>[1]-...). So
1335 # (1) if there is a custom id list or generator, do not care about this.
1336 # (2) if there is a pytest.param with a custom id, do not care about this
1337 # (3) if there is nothing OR if there is a pytest.param with no id, we should create a pytest.param with
1338 # the id.
1340 # in this particular case we have to modify the initial list
1341 argvalues[i] = v.as_lazy_tuple(nb_params)
1343 # TUPLE usage: if the id is not provided elsewhere we HAVE to set an id to avoid <id>[0]-<id>[1]...
1344 if p_ids[i] is None and not has_custom_ids:
1345 if not has_pytest_param: 1345 ↛ 1346line 1345 didn't jump to line 1346, because the condition on line 1345 was never true
1346 if v._id is not None:
1347 # (on pytest 2 we cannot do it since pytest.param does not exist)
1348 warn("The custom id %r in `lazy_value` will be ignored as this version of pytest is too old"
1349 " to support `pytest.param`." % v._id)
1350 else:
1351 pass # no warning, but no p_id update
1352 else:
1353 # update/create the pytest.param id on this value
1354 p_ids[i] = v.get_id()
1355 mod_lvid_indices.append(i)
1357 # handle marks
1358 _mks = v.get_marks(as_decorators=True)
1359 if len(_mks) > 0:
1360 # update/create the pytest.param marks on this value
1361 p_marks[i] = (p_marks[i] + _mks) if p_marks[i] is not None else _mks
1363 # update the marked_argvalues
1364 # - at least with the unpacked lazytuple if no pytest.param is there or needs to be created
1365 # - with a pytest.param if one is needed
1366 if p_ids[i] is None and p_marks[i] is None:
1367 marked_argvalues[i] = argvalues[i]
1368 else:
1369 # note that here argvalues[i] IS a tuple-like so we do not create a tuple around it
1370 marked_argvalues[i] = ParameterSet(values=argvalues[i], id=p_ids[i], marks=p_marks[i] or ())
1372 elif _fixtureref_used_as_tuple:
1373 # a fixture ref is used for several parameters at the same time
1374 fixture_indices.append((i, None))
1375 else:
1376 # Tuple: check nb params for consistency
1377 if len(v) != len(argnames): 1377 ↛ 1378line 1377 didn't jump to line 1378, because the condition on line 1377 was never true
1378 raise ValueError("Inconsistent number of values in pytest parametrize: %s items found while the "
1379 "number of parameters is %s: %s." % (len(v), len(argnames), v))
1381 # let's dig into the tuple to check if there are fixture_refs or lazy_values
1382 lv_pos_list = [j for j, _pval in enumerate(v) if is_lazy_value(_pval)]
1383 if len(lv_pos_list) > 0:
1384 _mks = [mk for _lv in lv_pos_list for mk in v[_lv].get_marks(as_decorators=True)]
1385 if len(_mks) > 0:
1386 # update/create the pytest.param marks on this value (global). (id not taken into account)
1387 p_marks[i] = (list(p_marks[i]) + _mks) if p_marks[i] is not None else list(_mks)
1388 marked_argvalues[i] = ParameterSet(values=argvalues[i], id=p_ids[i], marks=p_marks[i] or ())
1390 # auto create fixtures
1391 if auto_refs:
1392 autofix_pos_list = [j for j, _pval in enumerate(v) if is_fixture(_pval)]
1393 if len(autofix_pos_list) > 0:
1394 # there is at least one fixture without wrapping ref inside the tuple
1395 autov = list(v)
1396 for _k in autofix_pos_list:
1397 autov[_k] = fixture_ref(autov[_k])
1398 argvalues[i] = v = tuple(autov)
1399 if p_ids[i] is None and p_marks[i] is None:
1400 marked_argvalues[i] = argvalues[i]
1401 else:
1402 # note that here argvalues[i] IS a tuple-like so we do not create a tuple around it
1403 marked_argvalues[i] = ParameterSet(values=argvalues[i], id=p_ids[i], marks=p_marks[i] or ())
1405 fix_pos_list = [j for j, _pval in enumerate(v) if isinstance(_pval, fixture_ref)]
1406 if len(fix_pos_list) > 0:
1407 # there is at least one fixture ref inside the tuple
1408 fixture_indices.append((i, fix_pos_list))
1410 # let's dig into the tuple
1411 # has_val_ref = any(isinstance(_pval, lazy_value) for _pval in v)
1412 # val_pos_list = [j for j, _pval in enumerate(v) if isinstance(_pval, lazy_value)]
1413 # if len(val_pos_list) > 0:
1414 # # there is at least one value ref inside the tuple
1415 # argvalues[i] = tuple_with_value_refs(v, theoreticalsize=nb_params, positions=val_pos_list)
1417 return p_ids, p_marks, argvalues, fixture_indices, mod_lvid_indices