⬅ pytest_cases/fixture_core1_unions.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 __future__ import division
6  
7 from inspect import isgeneratorfunction
8 from warnings import warn
9  
10 from makefun import with_signature, add_signature_parameters, wraps
11  
12 import pytest
13 import sys
14  
15 try: # python 3.3+
16 from inspect import signature, Parameter
17 except ImportError:
18 from funcsigs import signature, Parameter # noqa
19  
  • E261 At least two spaces before inline comment
20 try: # native coroutines, python 3.5+
21 from inspect import iscoroutinefunction
22 except ImportError:
23 def iscoroutinefunction(obj):
24 return False
25  
  • E261 At least two spaces before inline comment
26 try: # native async generators, python 3.6+
27 from inspect import isasyncgenfunction
28 except ImportError:
29 def isasyncgenfunction(obj):
30 return False
31  
32  
33 try: # type hints, python 3+
34 from typing import Callable, Union, Optional, Any, List, Iterable, Sequence # noqa
35 from types import ModuleType # noqa
36 except ImportError:
37 pass
38  
39 from .common_mini_six import string_types
40 from .common_pytest import get_fixture_name, is_marked_parameter_value, get_marked_parameter_values, pytest_fixture, \
41 extract_parameterset_info, get_param_argnames_as_list, get_fixture_scope, resolve_ids
42 from .fixture__creation import get_caller_module, check_name_available, WARN
43  
44  
45 class _NotUsed:
46 def __repr__(self):
47 return "pytest_cases.NOT_USED"
48  
49  
50 class _Used:
51 def __repr__(self):
52 return "pytest_cases.USED"
53  
54  
55 NOT_USED = _NotUsed()
56 """Object representing a fixture value when the fixture is not used"""
57  
58  
59 USED = _Used()
60 """Object representing a fixture value when the fixture is used"""
61  
62  
63 class UnionIdMakers(object):
64 """
65 The enum defining all possible id styles for union fixture parameters ("alternatives")
66 """
67 @classmethod
68 def nostyle(cls,
69 param # type: UnionFixtureAlternative
70 ):
71 """ ids are <fixture_name> """
72 return param.get_alternative_id()
73  
74 @classmethod
75 def compact(cls,
76 param # type: UnionFixtureAlternative
77 ):
78 """ ids are /<fixture_name> """
79 return "/%s" % (param.get_alternative_id(),)
80  
81 @classmethod
82 def explicit(cls,
83 param # type: UnionFixtureAlternative
84 ):
85 """ ids are <union_name>/<fixture_name> """
86 return "%s/%s" % (param.get_union_id(), param.get_alternative_id())
87  
88 @classmethod
89 def get(cls, style # type: Union[str, Callable]
90 ):
91 # type: (...) -> Callable[[UnionFixtureAlternative], str]
92 """
93 Returns a function that one can use as the `ids` argument in parametrize, applying the given id style.
94 See https://github.com/smarie/python-pytest-cases/issues/41
95  
96 :param style:
97 :return:
98 """
99 if style is None or isinstance(style, string_types):
100 # return one of the styles from the class
101 style = style or 'nostyle'
102 try:
103 return getattr(cls, style)
104 except AttributeError:
105 raise ValueError("Unknown style: %r" % style)
106 else:
107 # assume a callable: return it directly
108 return style
109  
110  
111 class UnionFixtureAlternative(object):
112 """Defines an "alternative", used to parametrize a fixture union"""
113 __slots__ = 'union_name', 'alternative_name', 'alternative_index'
114  
115 def __init__(self,
116 union_name, # type: str
117 alternative_name, # type: str
118 alternative_index # type: int
119 ):
120 """
121  
122 :param union_name: the name of the union fixture
123 :param alternative_name: the name of the fixture that will be used by the union fixture when this alternative
124 is active
125 :param alternative_index: the index of the alternative, used for ids generation
126 """
127 self.union_name = union_name
128 self.alternative_name = alternative_name
129 self.alternative_index = alternative_index
130  
131 def get_union_id(self):
132 """Used by the id makers"""
133 return self.union_name
134  
135 def get_alternative_idx(self):
136 """Used by the id makers"""
137 return self.alternative_index
138  
139 def get_alternative_id(self):
140 """Used by the id makers to get the minimal (no style) id. Defaults to the alternative name"""
141 return self.alternative_name
142  
143 def __str__(self):
144 # This string representation can be used as an id if you pass `ids=str` to fixture_union for example
145 return "%s/%s/%s" % (self.get_union_id(), self.get_alternative_idx(), self.get_alternative_id())
146  
147 def __repr__(self):
148 return "%s(union_name=%s, alternative_index=%s, alternative_name=%s)" \
149 % (self.__class__.__name__, self.union_name, self.alternative_index, self.alternative_name)
150  
151 @staticmethod
152 def to_list_of_fixture_names(alternatives_lst # type: List[UnionFixtureAlternative]
153 ):
154 res = []
155 for f in alternatives_lst:
156 if is_marked_parameter_value(f):
157 f = get_marked_parameter_values(f, nbargs=1)[0]
158 res.append(f.alternative_name)
159 return res
160  
161  
162 class InvalidParamsList(Exception):
163 """
164 Exception raised when users attempt to provide a non-iterable `argvalues` in pytest parametrize.
165 See https://docs.pytest.org/en/latest/reference.html#pytest-mark-parametrize-ref
166 """
167 __slots__ = 'params',
168  
169 def __init__(self, params):
170 self.params = params
171  
172 def __str__(self):
173 return "Invalid parameters list (`argvalues`) in pytest parametrize. `list(argvalues)` returned an error. " \
174 "Please make sure that `argvalues` is a list, tuple or iterable : %r" % self.params
175  
176  
177 def is_fixture_union_params(params):
178 """
179 Internal helper to quickly check if a bunch of parameters correspond to a union fixture.
180  
181 Note: unfortunately `pytest` transform all params to a list when a @pytest.fixture is created,
182 so we can not pass a subclass of list to do the trick, we really have to work on the list elements.
183 :param params:
184 :return:
185 """
186 try:
187 if len(params) < 1:
188 return False
189 else:
190 if getattr(params, '__module__', '').startswith('pytest_cases'):
191 # a value_ref_tuple or another proxy object created somewhere in our code, not a list
192 return False
193 p0 = params[0]
194 if is_marked_parameter_value(p0):
195 p0 = get_marked_parameter_values(p0, nbargs=1)[0]
196 return isinstance(p0, UnionFixtureAlternative)
197 except: # noqa
198 # be conservative
199 # an iterable or the like - we do not use such things when we cope with fixture_refs and unions
200 return False
201  
202  
203 def is_used_request(request):
204 """
205 Internal helper to check if a given request for fixture is active or not.
206 Inactive fixtures happen when a fixture is not used in the current branch of a UNION fixture.
207  
208 All fixtures that need to be union-compliant have to be decorated with `@ignore_unused`
209  
210 :param request:
211 :return:
212 """
213 return getattr(request, 'param', None) is not NOT_USED
214  
215  
216 def ignore_unused(fixture_func):
217 """
218 A decorator for fixture functions so that they are compliant with fixture unions.
219 It
220  
221 - adds the `request` fixture dependency to their signature if needed
222 - filters the calls based on presence of the `NOT_USED` token in the request params.
223  
224 IMPORTANT: even if 'params' is not in kwargs, the fixture can be used in a fixture union and therefore a param
225 *will* be received on some calls (and the fixture will be called several times - only once for real) - we have to
226 handle the NOT_USED.
227  
228 :param fixture_func:
229 :return:
230 """
231 old_sig = signature(fixture_func)
232  
233 # add request if needed
234 func_needs_request = 'request' in old_sig.parameters
235 if not func_needs_request:
236 # Add it last so that `self` argument in class functions remains the first
237 new_sig = add_signature_parameters(old_sig, last=Parameter('request', kind=Parameter.POSITIONAL_OR_KEYWORD))
238 else:
239 new_sig = old_sig
240  
241 if isasyncgenfunction(fixture_func) and sys.version_info >= (3, 6):
242 from .pep525 import _ignore_unused_asyncgen_pep525
243 wrapped_fixture_func = _ignore_unused_asyncgen_pep525(fixture_func, new_sig, func_needs_request)
244 elif iscoroutinefunction(fixture_func) and sys.version_info >= (3, 5):
245 from .pep492 import _ignore_unused_coroutine_pep492
246 wrapped_fixture_func = _ignore_unused_coroutine_pep492(fixture_func, new_sig, func_needs_request)
247 elif isgeneratorfunction(fixture_func):
248 if sys.version_info >= (3, 3):
249 from .pep380 import _ignore_unused_generator_pep380
250 wrapped_fixture_func = _ignore_unused_generator_pep380(fixture_func, new_sig, func_needs_request)
251 else:
  • E115 Expected an indented block (comment)
252 # generator function (with a yield statement)
253 @wraps(fixture_func, new_sig=new_sig)
254 def wrapped_fixture_func(*args, **kwargs):
255 request = kwargs['request'] if func_needs_request else kwargs.pop('request')
256 if is_used_request(request):
257 for res in fixture_func(*args, **kwargs):
258 yield res
259 else:
260 yield NOT_USED
261 else:
262 # normal function with return statement
263 @wraps(fixture_func, new_sig=new_sig)
264 def wrapped_fixture_func(*args, **kwargs):
265 request = kwargs['request'] if func_needs_request else kwargs.pop('request')
266 if is_used_request(request):
267 return fixture_func(*args, **kwargs)
268 else:
269 return NOT_USED
270  
271 return wrapped_fixture_func
272  
273  
274 def fixture_union(name, # type: str
275 fixtures, # type: Iterable[Union[str, Callable]]
276 scope="function", # type: str
277 idstyle='compact', # type: Optional[Union[str, Callable]]
278 ids=None, # type: Union[Callable, Iterable[str]]
279 unpack_into=None, # type: Iterable[str]
280 autouse=False, # type: bool
281 hook=None, # type: Callable[[Callable], Callable]
282 **kwargs):
283 """
284 Creates a fixture that will take all values of the provided fixtures in order. That fixture is automatically
285 registered into the callers' module, but you may wish to assign it to a variable for convenience. In that case
286 make sure that you use the same name, e.g. `a = fixture_union('a', ['b', 'c'])`
287  
288 The style of test ids corresponding to the union alternatives can be changed with `idstyle`. Three values are
289 allowed:
290  
291 - `'explicit'` favors readability with names as `<union>/<alternative>`,
292 - `'compact'` (default) adds a small mark so that at least one sees which parameters are union alternatives and
293 which others are normal parameters: `/<alternative>`
294 - `None` or `'nostyle'` provides minimalistic ids : `<alternative>`
295  
296 See `UnionIdMakers` class for details.
297  
298 You can also pass a callable `idstyle` that will receive instances of `UnionFixtureAlternative`. For example `str`
299 leads to very explicit ids: `<union>/<idx>/<alternative>`. See `UnionFixtureAlternative` class for details.
300  
301 :param name: the name of the fixture to create
302 :param fixtures: an array-like containing fixture names and/or fixture symbols
303 :param scope: the scope of the union. Since the union depends on the sub-fixtures, it should be smaller than the
304 smallest scope of fixtures referenced.
305 :param idstyle: The style of test ids corresponding to the union alternatives. One of `'explicit'`, `'compact'`,
306 `'nostyle'`/`None`, or a callable (e.g. `str`) that will receive instances of `UnionFixtureAlternative`.
307 :param ids: as in pytest. The default value returns the correct fixture
308 :param unpack_into: an optional iterable of names, or string containing coma-separated names, for additional
309 fixtures to create to represent parts of this fixture. See `unpack_fixture` for details.
310 :param autouse: as in pytest
311 :param hook: an optional hook to apply to each fixture function that is created during this call. The hook function
312 will be called every time a fixture is about to be created. It will receive a single argument (the function
313 implementing the fixture) and should return the function to use. For example you can use `saved_fixture` from
314 `pytest-harvest` as a hook in order to save all such created fixtures in the fixture store.
315 :param kwargs: other pytest fixture options. They might not be supported correctly.
316 :return: the new fixture. Note: you do not need to capture that output in a symbol, since the fixture is
317 automatically registered in your module. However if you decide to do so make sure that you use the same name.
318 """
319 # grab the caller module, so that we can later create the fixture directly inside it
320 caller_module = get_caller_module()
321  
322 # test the `fixtures` argument to avoid common mistakes
323 if not isinstance(fixtures, (tuple, set, list)):
324 raise TypeError("fixture_union: the `fixtures` argument should be a tuple, set or list")
325  
326 # unpack the pytest.param marks
327 custom_pids, p_marks, fixtures = extract_parameterset_info((name, ), fixtures)
328  
329 # get all required fixture names
330 f_names = [get_fixture_name(f) for f in fixtures]
331  
332 # create all alternatives and reapply the marks on them
333 fix_alternatives = []
334 f_names_args = []
335 for _idx, (_name, _id, _mark) in enumerate(zip(f_names, custom_pids, p_marks)):
336 # create the alternative object
337 alternative = UnionFixtureAlternative(union_name=name, alternative_name=_name, alternative_index=_idx)
338  
339 # remove duplicates in the fixture arguments: each is required only once by the union fixture to create
340 if _name in f_names_args:
341 warn("Creating a fixture union %r where two alternatives are the same fixture %r." % (name, _name))
342 else:
343 f_names_args.append(_name)
344  
345 # reapply the marks
346 if _id is not None or (_mark or ()) != ():
347 alternative = pytest.param(alternative, id=_id, marks=_mark or ())
348 fix_alternatives.append(alternative)
349  
350 union_fix = _fixture_union(caller_module, name,
351 fix_alternatives=fix_alternatives, unique_fix_alt_names=f_names_args,
352 scope=scope, idstyle=idstyle, ids=ids, autouse=autouse, hook=hook, **kwargs)
353  
354 # if unpacking is requested, do it here
355 if unpack_into is not None:
356 # Note: we can't expose the `in_cls` argument as we would not be able to output both the union and the
357 # unpacked fixtures. However there is a simple workaround for this scenario of unpacking a union inside a class:
358 # call unpack_fixture separately.
359 _make_unpack_fixture(caller_module, argnames=unpack_into, fixture=name, hook=hook, in_cls=False)
360  
361 return union_fix
362  
363  
364 def _fixture_union(fixtures_dest,
365 name, # type: str
366 fix_alternatives, # type: Sequence[UnionFixtureAlternative]
367 unique_fix_alt_names, # type: List[str]
368 scope="function", # type: str
369 idstyle="compact", # type: Optional[Union[str, Callable]]
370 ids=None, # type: Union[Callable, Iterable[str]]
371 autouse=False, # type: bool
372 hook=None, # type: Callable[[Callable], Callable]
373 caller=fixture_union, # type: Callable
374 **kwargs):
375 """
376 Internal implementation for fixture_union.
377 The "alternatives" have to be created beforehand, by the caller. This allows `fixture_union` and `parametrize`
378 to use the same implementation while `parametrize` uses customized "alternatives" containing more information.
379  
380 :param fixtures_dest:
381 :param name:
382 :param fix_alternatives:
383 :param unique_fix_alt_names:
384 :param idstyle:
385 :param scope:
386 :param ids:
387 :param unpack_into:
388 :param autouse:
389 :param hook: an optional hook to apply to each fixture function that is created during this call. The hook function
390 will be called every time a fixture is about to be created. It will receive a single argument (the function
391 implementing the fixture) and should return the function to use. For example you can use `saved_fixture` from
392 `pytest-harvest` as a hook in order to save all such created fixtures in the fixture store.
393 :param caller: a function to reference for error messages
394 :param kwargs:
395 :return:
396 """
397 if len(fix_alternatives) < 1:
398 raise ValueError("Empty fixture unions are not permitted")
399  
400 # then generate the body of our union fixture. It will require all of its dependent fixtures and receive as
401 # a parameter the name of the fixture to use
402 @with_signature("%s(%s, request)" % (name, ', '.join(unique_fix_alt_names)))
403 def _new_fixture(request, **all_fixtures):
404 # ignore the "not used" marks, like in @ignore_unused
405 if not is_used_request(request):
406 return NOT_USED
407 else:
408 _alternative = request.param
409 if isinstance(_alternative, UnionFixtureAlternative):
410 fixture_to_use = _alternative.alternative_name
411 return all_fixtures[fixture_to_use]
412 else:
413 raise TypeError("Union Fixture %s received invalid parameter type: %s. Please report this issue."
414 "" % (name, _alternative.__class__))
415  
416 if ids is None:
417 ids = UnionIdMakers.get(idstyle)
418 else:
419 # resolve possibly infinite generators of ids here
420 ids = resolve_ids(ids, fix_alternatives, full_resolve=False)
421  
422 # finally create the fixture per se.
423 _make_fix = pytest_fixture(scope=scope or "function", params=fix_alternatives, autouse=autouse,
424 ids=ids, hook=hook, **kwargs)
425 new_union_fix = _make_fix(_new_fixture)
426  
427 # Dynamically add fixture to caller's module as explained in https://github.com/pytest-dev/pytest/issues/2424
428 check_name_available(fixtures_dest, name, if_name_exists=WARN, caller=caller)
429 setattr(fixtures_dest, name, new_union_fix)
430  
431 return new_union_fix
432  
433  
434 _make_fixture_union = _fixture_union
435 """A readable alias for callers not using the returned symbol"""
436  
437  
438 def unpack_fixture(argnames, # type: str
439 fixture, # type: Union[str, Callable]
440 in_cls=False, # type: bool
441 hook=None # type: Callable[[Callable], Callable]
442 ):
443 """
444 Creates several fixtures with names `argnames` from the source `fixture`. Created fixtures will correspond to
445 elements unpacked from `fixture` in order. For example if `fixture` is a tuple of length 2, `argnames="a,b"` will
446 create two fixtures containing the first and second element respectively.
447  
448 The created fixtures are automatically registered into the callers' module, but you may wish to assign them to
449 variables for convenience. In that case make sure that you use the same names,
450 e.g. `a, b = unpack_fixture('a,b', 'c')`.
451  
452 ```python
453 import pytest
454 from pytest_cases import unpack_fixture, fixture
455  
456 @fixture
457 @pytest.mark.parametrize("o", ['hello', 'world'])
458 def c(o):
459 return o, o[0]
460  
461 a, b = unpack_fixture("a,b", c)
462  
463 def test_function(a, b):
464 assert a[0] == b
465 ```
466  
467 You can also use this function inside a class with `in_cls=True`. In that case you MUST assign the output of the
468 function to variables, as the created fixtures won't be registered with the encompassing module.
469  
470 ```python
471 import pytest
472 from pytest_cases import unpack_fixture, fixture
473  
474 @fixture
475 @pytest.mark.parametrize("o", ['hello', 'world'])
476 def c(o):
477 return o, o[0]
478  
479 class TestClass:
480 a, b = unpack_fixture("a,b", c, in_cls=True)
481  
482 def test_function(self, a, b):
483 assert a[0] == b
484 ```
485  
486 :param argnames: same as `@pytest.mark.parametrize` `argnames`.
487 :param fixture: a fixture name string or a fixture symbol. If a fixture symbol is provided, the created fixtures
488 will have the same scope. If a name is provided, they will have scope='function'. Note that in practice the
489 performance loss resulting from using `function` rather than a higher scope is negligible since the created
490 fixtures' body is a one-liner.
491 :param in_cls: a boolean (default False). You may wish to turn this to `True` to use this function inside a class.
492 If you do so, you **MUST** assign the output to variables in the class.
493 :param hook: an optional hook to apply to each fixture function that is created during this call. The hook function
494 will be called every time a fixture is about to be created. It will receive a single argument (the function
495 implementing the fixture) and should return the function to use. For example you can use `saved_fixture` from
496 `pytest-harvest` as a hook in order to save all such created fixtures in the fixture store.
497 :return: the created fixtures.
498 """
499 if in_cls:
500 # the user needs to capture the outputs of the function in symbols in the class
501 caller_module = None
502 else:
503 # get the caller module to create the symbols in it. Assigning outputs is optional
504 caller_module = get_caller_module()
505 return _unpack_fixture(caller_module, argnames, fixture, hook=hook, in_cls=in_cls)
506  
507  
508 def _unpack_fixture(fixtures_dest, # type: ModuleType
509 argnames, # type: Union[str, Iterable[str]]
510 fixture, # type: Union[str, Callable]
511 in_cls, # type: bool
512 hook # type: Callable[[Callable], Callable]
513 ):
514 """
515  
516 :param fixtures_dest: if this is `None` the fixtures won't be registered anywhere (just returned)
517 :param argnames:
518 :param fixture:
519 :param in_cls: a boolean indicating if the `self` argument should be prepended.
520 :param hook: an optional hook to apply to each fixture function that is created during this call. The hook function
521 will be called every time a fixture is about to be created. It will receive a single argument (the function
522 implementing the fixture) and should return the function to use. For example you can use `saved_fixture` from
523 `pytest-harvest` as a hook in order to save all such created fixtures in the fixture store.
524 :return:
525 """
526 # unpack fixture names to create if needed
527 argnames_lst = get_param_argnames_as_list(argnames)
528  
529 # possibly get the source fixture name if the fixture symbol was provided
530 source_f_name = get_fixture_name(fixture)
531 if not isinstance(fixture, string_types):
532 scope = get_fixture_scope(fixture)
533 else:
534 # we dont have a clue about the real scope, so lets use function scope
535 scope = 'function'
536  
537 # finally create the sub-fixtures
538 created_fixtures = []
539  
540 # we'll need to create their signature
541 if in_cls:
542 _sig = "(self, %s, request)" % source_f_name
543 else:
544 _sig = "(%s, request)" % source_f_name
545  
546 for value_idx, argname in enumerate(argnames_lst):
547 # create the fixture
548 # To fix late binding issue with `value_idx` we add an extra layer of scope: a factory function
549 # See https://stackoverflow.com/questions/3431676/creating-functions-in-a-loop
550 def _create_fixture(_value_idx):
551 # no need to autouse=True: this fixture does not bring any added value in terms of setup.
552 @pytest_fixture(name=argname, scope=scope, autouse=False, hook=hook)
553 @with_signature(argname + _sig)
554 def _param_fixture(request, **kwargs):
555 # ignore the "not used" marks, like in @ignore_unused
556 if not is_used_request(request):
557 return NOT_USED
558 # get the required fixture's value (the tuple to unpack)
559 source_fixture_value = kwargs.pop(source_f_name)
560 # unpack: get the item at the right position.
561 return source_fixture_value[_value_idx]
562  
563 return _param_fixture
564  
565 # create it
566 fix = _create_fixture(value_idx)
567  
568 if fixtures_dest is not None:
569 # add to module
570 check_name_available(fixtures_dest, argname, if_name_exists=WARN, caller=unpack_fixture)
571 setattr(fixtures_dest, argname, fix)
572  
573 # collect to return the whole list eventually
574 created_fixtures.append(fix)
575  
576 return created_fixtures
577  
578  
579 _make_unpack_fixture = _unpack_fixture
580 """A readable alias for callers not using the returned symbol"""