⬅ pytest_cases/fixture_parametrize_plus.py source

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>
5 from inspect import isgeneratorfunction
6 from warnings import warn
7  
8  
9 try: # python 3.3+
10 from inspect import signature, Parameter
11 except ImportError:
12 from funcsigs import signature, Parameter # noqa
13  
  • E261 At least two spaces before inline comment
14 try: # native coroutines, python 3.5+
15 from inspect import iscoroutinefunction
16 except ImportError:
17 def iscoroutinefunction(obj):
18 return False
19  
  • E261 At least two spaces before inline comment
20 try: # native async generators, python 3.6+
21 from inspect import isasyncgenfunction
22 except ImportError:
23 def isasyncgenfunction(obj):
24 return False
25  
26 try:
27 from collections.abc import Iterable
28 except ImportError: # noqa
29 from collections import Iterable
30  
31 try:
32 from typing import Union, Callable, List, Any, Sequence, Optional, Type, Tuple, TypeVar # noqa
33 from types import ModuleType # noqa
34  
35 T = TypeVar('T', bound=Union[Type, Callable])
36 except ImportError:
37 pass
38  
39 import pytest
40 import sys
41 from makefun import with_signature, remove_signature_parameters, add_signature_parameters, wraps
42  
43 from .common_mini_six import string_types
44 from .common_others import AUTO, robust_isinstance, replace_list_contents
45 from .common_pytest_marks import has_pytest_param, get_param_argnames_as_list
46 from .common_pytest_lazy_values import is_lazy_value, get_lazy_args
47 from .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
51  
52 from .fixture__creation import check_name_available, CHANGE, WARN
53 from .fixture_core1_unions import InvalidParamsList, NOT_USED, UnionFixtureAlternative, _make_fixture_union, \
54 _make_unpack_fixture, UnionIdMakers
55 from .fixture_core2 import _create_param_fixture, fixture
56  
57  
58 def _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.
70  
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)):
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)
88  
89 _tuple_size = len(fixtures_or_values)
90  
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):
97 f = f.fixture
98 # and remember the position in the tuple
99 f_names[f_pos] = get_fixture_name(f)
100  
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:
104 raise ValueError("Empty fixture products are not permitted")
105  
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]
116  
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))
121  
122 _new_fixture.__name__ = name
123  
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)
128  
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)
132  
133 # if unpacking is requested, do it here
134 if unpack_into is not None:
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)
138  
139 return fix
140  
141  
142 _make_fixture_product = _fixture_product
143 """A readable alias for callers not using the returned symbol"""
144  
145  
146 class 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'
152  
153 def __init__(self,
154 fixture, # type: Union[str, Callable]
155 id=None, # type: str # noqa
156 ):
157 """
158  
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
165  
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
169  
170 def __str__(self):
171 # used in mini_idval for example
172 return self.get_name_for_id()
173  
174 def __repr__(self):
175 if self._id is not None:
176 return "fixture_ref<%s, id=%s>" % (self.fixture, self._id)
177 else:
178 return "fixture_ref<%s>" % self.fixture
179  
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:
188 raise TypeError("This fixture_ref does not represent a tuple of arguments, it is not iterable")
189  
190 def __len__(self):
191 self._check_iterable()
192 return self.theoretical_size
193  
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)
202  
203  
204 class FixtureRefItem(object):
205 """An item in a fixture_ref when this fixture_ref is used as a tuple."""
206 __slots__ = 'host', 'item'
207  
208 def __init__(self,
209 host, # type: fixture_ref
210 item # type: int
211 ):
212 self.host = host
213 self.item = item
214  
215 def __repr__(self):
216 return "%r[%s]" % (self.host, self.item)
217  
218  
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)
225 def 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)
230  
231  
232 parametrize_plus = pytest_parametrize_plus
233  
234  
235 class ParamAlternative(UnionFixtureAlternative):
236 """Defines an "alternative", used to parametrize a fixture union in the context of parametrize
237  
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')
244  
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 """
253  
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
264  
265 def get_union_id(self):
266 return ("(%s)" % ",".join(self.argnames)) if len(self.argnames) > 1 else self.argnames[0]
267  
268 def get_alternative_idx(self):
269 return "P%s" % self.alternative_index
270  
271 def get_alternative_id(self):
272 """Subclasses should return the smallest id representing this parametrize fixture union alternative"""
273 raise NotImplementedError()
274  
275  
276 class SingleParamAlternative(ParamAlternative):
277 """alternative class for single parameter value"""
278 __slots__ = 'argval', 'id'
279  
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
300  
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)
309  
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.
327  
328 This alternative will refer to a newly created fixture in `new_fixture_host`, that will return `argvalue`.
329  
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(' ', '')
342  
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)
346  
347 if debug:
  • T001 Print found.
348 print(" - Creating new fixture %r to handle parameter %s" % (p_fix_name, i))
349  
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.
354  
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]
363  
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)
367  
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)
373  
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
377  
378 return p_fix_alt
379  
380  
381 class MultiParamAlternative(ParamAlternative):
382 """alternative class for multiple parameter values"""
383 __slots__ = 'param_index_from', 'param_index_to'
384  
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 """
394  
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
409  
410 def __str__(self):
411 return "%s/%s/" % (self.get_union_id(), self.get_alternative_idx())
412  
413 def get_alternative_idx(self):
414 return "P%s:%s" % (self.param_index_from, self.param_index_to)
415  
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()
419  
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
438  
439 This alternative will refer to a newly created fixture in `new_fixture_host`, that will be parametrized to
440 return each of `argvalues`.
441  
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(' ', '')
455  
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)
459  
460 if debug:
  • T001 Print found.
461 print(" - Creating new fixture %r to handle parameters %s to %s" % (p_fix_name, from_i, to_i - 1))
462  
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
468  
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:
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
488  
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:
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)]
497  
498 _create_param_fixture(new_fixture_host, argname=p_fix_name, argvalues=argvalues, ids=ids,
499 scope=scope, hook=hook, debug=debug)
500  
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)
506  
507  
508 class FixtureParamAlternative(SingleParamAlternative):
509 """alternative class for a single parameter containing a fixture ref"""
510  
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)
530  
531 def get_alternative_idx(self):
532 return "P%sF" % self.alternative_index
533  
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()
541  
542  
543 class ProductParamAlternative(SingleParamAlternative):
544 """alternative class for a single product parameter containing fixture refs"""
545  
546 def get_alternative_idx(self):
547 return "P%sF" % self.alternative_index
548  
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:
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)
558  
559  
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.
566  
567 EMPTY_ID = "<pytest_cases_empty_id>"
568  
569  
570 if has_pytest_param:
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)])
574 else:
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)])
578  
579  
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()
602  
603  
604 class ParamIdMakers(UnionIdMakers):
605 """ 'Enum' of id styles for param ids
606  
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)
620  
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))
628  
629  
630 _IDGEN = object()
631  
632  
633 def 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
647  
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]}`.
652  
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.
660  
661 (3) new possibilities in argvalues:
662  
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`
669  
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.
673  
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.
676  
677 Finally, `pytest.param` is supported even when there are `fixture_ref` and `lazy_value`.
678  
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.
683  
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
722  
723  
724 class 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__()
733  
734 def __str__(self):
735 return repr(self)
736  
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)
741  
742  
743 def _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 """
756  
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)
762  
763 # argnames related
764 initial_argnames = ','.join(argnames)
765 nb_params = len(argnames)
766  
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)
773  
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
778  
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))
784  
785 idgen = lambda **args: "-".join(_make_ids(**args)) # noqa
786  
787 # generate id
788 if idgen is not None:
789 if ids is not None:
790 raise ValueError("Only one of `ids` and `idgen` should be provided")
791 ids = _gen_ids(argnames, argvalues, idgen)
792  
793 if len(fixture_indices) == 0:
794 # No fixture reference: fallback to a standard pytest.mark.parametrize
795 if debug:
  • T001 Print found.
796 print("No fixture reference found. Calling @pytest.mark.parametrize...")
  • T001 Print found.
797 print(" - argnames: %s" % initial_argnames)
  • T001 Print found.
798 print(" - argvalues: %s" % marked_argvalues)
  • T001 Print found.
799 print(" - ids: %s" % ids)
800  
801 # handle infinite iterables like latest pytest, for convenience
802 ids = resolve_ids(ids, marked_argvalues, full_resolve=False)
803  
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)
824  
825 return _apply, False
826  
827 else:
828 # there are fixture references: we will create a specific decorator replacing the params with a "union" fixture
829 if indirect:
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")
833  
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
844  
845 if indirect:
846 raise ValueError("Setting `indirect=True` is not yet supported when at least a `fixure_ref` is present in "
847 "the `argvalues`.")
848  
849 if debug:
  • T001 Print found.
850 print("Fixture references found. Creating references and fixtures...")
851  
852 param_names_str = '_'.join(argnames).replace(' ', '')
853  
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
859  
860 # First define a few functions that will help us create the various fixtures to use in the final "union"
861  
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"""
864  
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
869  
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
875  
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
884  
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)
890  
891  
892 def _create_fixture_ref_alt(union_name, test_func, i): # noqa
893  
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
899  
900 # Get the referenced fixture name
901 f_fix_name = argvalues[i].fixture
902  
903 if debug:
  • T001 Print found.
904 print(" - Creating reference to existing fixture %r" % (f_fix_name,))
905  
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]))
914  
915 return f_fix_alt
916  
917 def _create_fixture_ref_product(fh, union_name, i, fixture_ref_positions, test_func, hook): # noqa
918  
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
921  
922 # values to use:
923 param_values = argvalues[i]
924  
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)
928  
929 if debug:
  • T001 Print found.
930 print(" - Creating new fixture %r to handle parameter %s that is a cross-product" % (p_fix_name, i))
931  
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)
935  
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
945  
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.
952  
953 :param test_func:
954 :return:
955 """
956 test_func_name = test_func.__name__
957  
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.")
963  
964 old_sig = signature(test_func)
965 for p in argnames:
966 if p not in old_sig.parameters:
967 raise ValueError("parameter '%s' not found in test function signature '%s%s'"
968 "" % (p, test_func_name, old_sig))
969  
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)
976  
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)
991  
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)
1003  
1004 prev_i = i
1005  
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)
1012  
1013 # if fixtures_to_union has length 1, simplify ? >> No, we leave such "optimization" to the end user
1014  
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)
  • S101 Use of assert detected. The enclosed code will be removed when compiling to optimised byte code.
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
  • S101 Use of assert detected. The enclosed code will be removed when compiling to optimised byte code.
1027 assert isinstance(alt, FixtureParamAlternative), "Created fixture names not unique, please report"
1028  
1029 # Finally create a "main" fixture with a unique name for this test function
1030 if debug:
  • T001 Print found.
1031 print("Creating final union fixture %r with alternatives %r"
1032 % (fixture_union_name, UnionFixtureAlternative.to_list_of_fixture_names(fixture_alternatives)))
1033  
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
1039  
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)
1044  
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):
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))
1059  
1060 if debug:
  • T001 Print found.
1061 print("Creating final test function wrapper with signature %s%s" % (test_func_name, new_sig))
1062  
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
1078  
1079  
  • E303 Too many blank lines (2)
  • E225 Missing whitespace around operator
1080 if isasyncgenfunction(test_func)and sys.version_info >= (3, 6):
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,
  • E128 Continuation line under-indented for visual indent
1087 replace_paramfixture_with_values)
1088 elif isgeneratorfunction(test_func):
1089 # generator function (with a yield statement)
1090 if sys.version_info >= (3, 3):
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:
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)
1117  
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)
1121  
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
1129  
1130 # return the new test function
1131 return wrapped_test_func
1132  
1133 return parametrize_plus_decorate, True
1134  
1135  
1136 def _get_argnames_argvalues(
1137 argnames=None, # type: Union[str, Tuple[str], List[str]]
1138 argvalues=None, # type: Iterable[Any]
1139 **args
1140 ):
1141 """
1142  
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 = (), ()
1153  
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:
1157 raise ValueError("No parameters provided")
1158  
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
1165  
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)
1169  
1170 if not isinstance(argnames, (list, tuple)):
1171 raise TypeError("argnames should be a string, list or a tuple")
1172  
1173 if any([not isinstance(argname, str) for argname in argnames]):
1174 raise TypeError("all argnames should be strings")
1175  
1176 if argvalues is None:
1177 raise ValueError("No argvalues provided while argnames are provided")
1178  
1179 # transform argvalues to a list (it can be a generator)
1180 try:
1181 argvalues = list(argvalues)
1182 except TypeError:
1183 raise InvalidParamsList(argvalues)
1184  
1185 # append **args
1186 if len(kw_argnames) > 0:
1187 argnames, argvalues = cart_product_pytest((argnames, kw_argnames),
1188 (argvalues, kw_argvalues))
1189  
1190 return argnames, argvalues
1191  
1192  
1193 def _gen_ids(argnames, argvalues, idgen):
1194 """
1195 Generates an explicit test ids list from a non-none `idgen`.
1196  
1197 `idgen` should be either a callable of a string template.
1198  
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):
1207 raise TypeError("idgen should be a callable or a string, found: %r" % idgen)
1208  
1209 _formatter = idgen
1210  
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)
1217  
1218 idgen = gen_id_using_str_formatter
1219  
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]
1225  
1226 return ids
1227  
1228  
1229 def _process_argvalues(argnames, marked_argvalues, nb_params, has_custom_ids, auto_refs):
1230 """Internal method to use in _pytest_parametrize_plus
1231  
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
1234  
1235 Note: `marked_argvalues` is modified in the process if a `lazy_value` is found with a custom id or marks.
1236  
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)
1246  
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`
1259  
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
1265  
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])
1276  
1277 if isinstance(v, fixture_ref):
1278 # Fix the referenced fixture length
1279 v.theoretical_size = nb_params
1280 fixture_indices.append((i, None))
1281  
1282 elif nb_params > 1:
1283 for i, v in enumerate(argvalues):
1284  
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:
1295 marked_argvalues[i] = v
1296 else:
1297 marked_argvalues[i] = ParameterSet(values=(v,), id=p_ids[i], marks=p_marks[i])
1298  
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]),)
1312  
1313 if isinstance(v[0], fixture_ref):
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:
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
1322  
1323 # B/ Now process it
1324 if _lazyvalue_used_as_tuple:
1325 # --- A lazy value is used for several parameters at the same time ---
1326  
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.
1339  
1340 # in this particular case we have to modify the initial list
1341 argvalues[i] = v.as_lazy_tuple(nb_params)
1342  
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:
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)
1356  
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
1362  
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 ())
1371  
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):
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))
1380  
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 ())
1389  
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 ())
1404  
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))
1409  
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)
1416  
1417 return p_ids, p_marks, argvalues, fixture_indices, mod_lvid_indices