22from __future__ import division
33
44from distutils .version import LooseVersion
5+ from enum import Enum
56from inspect import isgeneratorfunction , getmodule , currentframe
67from itertools import product
78from warnings import warn
1011from makefun import with_signature , add_signature_parameters , remove_signature_parameters , wraps
1112
1213import pytest
13- from wrapt import ObjectProxy
1414
1515try : # python 3.3+
1616 from inspect import signature , Parameter
@@ -543,17 +543,56 @@ def __repr__(self):
543543"""Object representing a fixture value when the fixture is not used"""
544544
545545
546- class UnionFixtureAlternative (ObjectProxy ):
546+ class UnionFixtureAlternative (object ):
547547 """A special class that should be used to wrap a fixture name"""
548548
549- def __init__ (self , fixture_name ):
550- super (UnionFixtureAlternative , self ).__init__ (fixture_name )
549+ def __init__ (self ,
550+ fixture_name ,
551+ idstyle # type: IdStyle
552+ ):
553+ self .fixture_name = fixture_name
554+ self .idstyle = idstyle
551555
552- def __str__ (self ):
553- return str (self .__wrapped__ )
556+ # def __str__(self):
557+ # that is maybe too dangerous...
558+ # return self.fixture_name
554559
555560 def __repr__ (self ):
556- return "UnionFixtureAlternative<%s>" % str (self )
561+ return "UnionAlternative<%s, idstyle=%s>" % (self .fixture_name , self .idstyle )
562+
563+ @staticmethod
564+ def to_list_of_fixture_names (alternatives_lst # type: List[UnionFixtureAlternative]
565+ ):
566+ return [f .fixture_name for f in alternatives_lst ]
567+
568+
569+ class IdStyle (Enum ):
570+ """
571+ The enum defining all possible id styles.
572+ """
573+ none = None
574+ explicit = 'explicit'
575+ compact = 'compact'
576+
577+
578+ def apply_id_style (id , union_fixture_name , idstyle ):
579+ """
580+ Applies the id style defined in `idstyle` to the given id.
581+ See https://github.com/smarie/python-pytest-cases/issues/41
582+
583+ :param id:
584+ :param union_fixture_name:
585+ :param idstyle:
586+ :return:
587+ """
588+ if idstyle is IdStyle .none :
589+ return id
590+ elif idstyle is IdStyle .explicit :
591+ return "%s_is_%s" % (union_fixture_name , id )
592+ elif idstyle is IdStyle .compact :
593+ return "U%s" % id
594+ else :
595+ raise ValueError ("Invalid id style" )
557596
558597
559598def is_fixture_union_params (params ):
@@ -578,33 +617,65 @@ def is_used_request(request):
578617 return getattr (request , 'param' , None ) is not NOT_USED
579618
580619
581- def fixture_union (name , fixtures , scope = "function" , ids = None , autouse = False , ** kwargs ):
620+ def fixture_alternative_to_str (fixture_alternative , # type: UnionFixtureAlternative
621+ ):
622+ return fixture_alternative .fixture_name
623+
624+
625+ def fixture_union (name , fixtures , scope = "function" , idstyle = 'explicit' ,
626+ ids = fixture_alternative_to_str , autouse = False , ** kwargs ):
582627 """
583- Creates a fixture that will take all values of the provided fixtures in order.
628+ Creates a fixture that will take all values of the provided fixtures in order. That fixture is automatically
629+ registered into the callers' module, but you may wish to assign it to a variable for convenience. In that case
630+ make sure that you use the same name, e.g. `a = fixture_union('a', ['b', 'c'])`
584631
585- :param name:
632+ The style of test ids corresponding to the union alternatives can be changed with `idstyle`. Three values are
633+ allowed:
634+
635+ - `'explicit'` (default) favors readability,
636+ - `'compact'` adds a small mark so that at least one sees which parameters are union parameters and which others
637+ are normal parameters,
638+ - `None` does not change the ids.
639+
640+ :param name: the name of the fixture to create
586641 :param fixtures: an array-like containing fixture names and/or fixture symbols
587642 :param scope: the scope of the union. Since the union depends on the sub-fixtures, it should be smaller than the
588- smallest scope of fictures referenced.
589- :return:
643+ smallest scope of fixtures referenced.
644+ :param idstyle: The style of test ids corresponding to the union alternatives. One of `'explicit'` (default),
645+ `'compact'`, or `None`.
646+ :param ids: as in pytest. The default value returns the correct fixture
647+ :param autouse: as in pytest
648+ :param kwargs: other pytest fixture options. They might not be supported correctly.
649+ :return: the new fixture. Note: you do not need to capture that output in a symbol, since the fixture is
650+ automatically registered in your module. However if you decide to do so make sure that you use the same name.
590651 """
591652 caller_module = get_caller_module ()
592- return _fixture_union (caller_module , name , fixtures , scope = scope , ids = ids , autouse = autouse , ** kwargs )
653+ return _fixture_union (caller_module , name , fixtures , scope = scope , idstyle = idstyle , ids = ids , autouse = autouse ,
654+ ** kwargs )
593655
594656
595- def _fixture_union (caller_module , name , fixtures , scope = "function" , ids = None , autouse = False , ** kwargs ):
657+ def _fixture_union (caller_module , name , fixtures , idstyle , scope = "function" , ids = fixture_alternative_to_str ,
658+ autouse = False , ** kwargs ):
596659 """
597660 Internal implementation for fixture_union
598661
599662 :param caller_module:
600663 :param name:
601664 :param fixtures:
665+ :param idstyle:
602666 :param scope:
603667 :param ids:
604668 :param autouse:
605669 :param kwargs:
606670 :return:
607671 """
672+ # test the `fixtures` argument to avoid common mistakes
673+ if not isinstance (fixtures , (tuple , set , list )):
674+ raise TypeError ("fixture_union: the `fixtures` argument should be a tuple, set or list" )
675+
676+ # validate the idstyle
677+ idstyle = IdStyle (idstyle )
678+
608679 # first get all required fixture names
609680 f_names = []
610681 for f in fixtures :
@@ -621,14 +692,20 @@ def _new_fixture(request, **all_fixtures):
621692 if not is_used_request (request ):
622693 return NOT_USED
623694 else :
624- fixture_to_use = request .param
625- return all_fixtures [fixture_to_use ]
695+ alternative = request .param
696+ if isinstance (alternative , UnionFixtureAlternative ):
697+ fixture_to_use = alternative .fixture_name
698+ return all_fixtures [fixture_to_use ]
699+ else :
700+ raise TypeError ("Union Fixture %s received invalid parameter type: %s. Please report this issue."
701+ "" % (name , alternative .__class__ ))
626702
627703 _new_fixture .__name__ = name
628704
629705 # finally create the fixture per se.
630706 # WARNING we do not use pytest.fixture but pytest_fixture_plus so that NOT_USED is discarded
631- f_decorator = pytest_fixture_plus (scope = scope , params = [UnionFixtureAlternative (_name ) for _name in f_names ],
707+ f_decorator = pytest_fixture_plus (scope = scope ,
708+ params = [UnionFixtureAlternative (_name , idstyle ) for _name in f_names ],
632709 autouse = autouse , ids = ids , ** kwargs )
633710 fix = f_decorator (_new_fixture )
634711
@@ -656,7 +733,8 @@ def pytest_parametrize_plus(argnames, argvalues, indirect=False, ids=None, scope
656733 fixtures with `fixture_ref(<fixture>)` where <fixture> can be the fixture name or fixture function.
657734
658735 When such a fixture reference is detected in the argvalues, a new function-scope fixture will be created with a
659- unique name, and the test function will be wrapped so as to be injected .
736+ unique name, and the test function will be wrapped so as to be injected with the correct parameters. Special test
737+ ids will be created to illustrate the switching between normal parameters and fixtures.
660738
661739 :param argnames:
662740 :param argvalues:
@@ -694,9 +772,9 @@ def create_param_fixture(from_i, to_i, p_fix_name):
694772 selected_ids = ids
695773
696774 if to_i == from_i + 1 :
697- p_fix_name = "%s__ %s" % (p_fix_name , from_i )
775+ p_fix_name = "%s_is_ %s" % (p_fix_name , from_i )
698776 else :
699- p_fix_name = "%s__%s_to_ %s" % (p_fix_name , from_i , to_i - 1 )
777+ p_fix_name = "%s_is_%sto %s" % (p_fix_name , from_i , to_i - 1 )
700778 p_fix_name = check_name_available (caller_module , p_fix_name , if_name_exists = CHANGE , caller = pytest_parametrize_plus )
701779 param_fix = _param_fixture (caller_module , p_fix_name , selected_argvalues , selected_ids )
702780 return param_fix
@@ -718,31 +796,41 @@ def parametrize_plus_decorate(test_func):
718796 "" % (p , test_func .__name__ , old_sig ))
719797
720798 # The base name for all fixtures that will be created below
721- base_name = test_func .__name__ + '_param__' + argnames .replace (' ' , '' ).replace (',' , '_' )
799+ # style_template = "%s_param__%s"
800+ style_template = "%s_%s"
801+ base_name = style_template % (test_func .__name__ , argnames .replace (' ' , '' ).replace (',' , '_' ))
722802 base_name = check_name_available (caller_module , base_name , if_name_exists = CHANGE , caller = pytest_parametrize_plus )
723803
724804 # Retrieve (if ref) or create (for normal argvalues) the fixtures that we will union
725805 # TODO important note: we could either wish to create one fixture for parameter value or to create one for
726806 # each consecutive group as shown below. This should not lead to different results but perf might differ.
727807 # maybe add a parameter in the signature so that users can test it ?
728808 fixtures_to_union = []
809+ fixtures_to_union_names_for_ids = []
729810 prev_i = - 1
730811 for i in fixture_indices :
731812 if i > prev_i + 1 :
732813 param_fix = create_param_fixture (prev_i + 1 , i , base_name )
733814 fixtures_to_union .append (param_fix )
815+ fixtures_to_union_names_for_ids .append (get_fixture_name (param_fix ))
816+
734817 fixtures_to_union .append (argvalues [i ].fixture )
818+ id_for_fixture = apply_id_style (get_fixture_name (argvalues [i ].fixture ), base_name , IdStyle .explicit )
819+ fixtures_to_union_names_for_ids .append (id_for_fixture )
735820 prev_i = i
736821
737822 # last bit if any
738823 i = len (argvalues )
739824 if i > prev_i + 1 :
740825 param_fix = create_param_fixture (prev_i + 1 , i , base_name )
741826 fixtures_to_union .append (param_fix )
827+ fixtures_to_union_names_for_ids .append (get_fixture_name (param_fix ))
742828
743829 # Finally create a "main" fixture with a unique name for this test function
744830 # note: the function automatically registers it in the module
745- big_param_fixture = _fixture_union (caller_module , base_name , fixtures_to_union )
831+ # note 2: idstyle is set to None because we provide an explicit enough list of ids
832+ big_param_fixture = _fixture_union (caller_module , base_name , fixtures_to_union , idstyle = None ,
833+ ids = fixtures_to_union_names_for_ids )
746834
747835 # --create the new test function's signature that we want to expose to pytest
748836 # it is the same than existing, except that we want to replace all parameters with the new fixture
0 commit comments