4040
4141from pytest_cases .common import yield_fixture , get_pytest_parametrize_marks , get_test_ids_from_param_values , \
4242 make_marked_parameter_value , extract_parameterset_info , get_fixture_name , get_param_argnames_as_list , \
43- get_fixture_scope
43+ get_fixture_scope , remove_duplicates
4444from pytest_cases .main_params import cases_data
4545
4646
@@ -886,6 +886,76 @@ def _new_fixture(request, **all_fixtures):
886886 return fix
887887
888888
889+ def _fixture_product (caller_module , name , fixtures_or_values , fixture_positions ,
890+ scope = "function" , ids = fixture_alternative_to_str ,
891+ unpack_into = None , autouse = False , ** kwargs ):
892+ """
893+ Internal implementation for fixture products created by pytest parametrize plus.
894+
895+ :param caller_module:
896+ :param name:
897+ :param fixtures_or_values:
898+ :param fixture_positions:
899+ :param idstyle:
900+ :param scope:
901+ :param ids:
902+ :param unpack_into:
903+ :param autouse:
904+ :param kwargs:
905+ :return:
906+ """
907+ # test the `fixtures` argument to avoid common mistakes
908+ if not isinstance (fixtures_or_values , (tuple , set , list )):
909+ raise TypeError ("fixture_product: the `fixtures_or_values` argument should be a tuple, set or list" )
910+
911+ _tuple_size = len (fixtures_or_values )
912+
913+ # first get all required fixture names
914+ f_names = [None ] * _tuple_size
915+ for f_pos in fixture_positions :
916+ # possibly get the fixture name if the fixture symbol was provided
917+ f = fixtures_or_values [f_pos ]
918+ # and remember the position in the tuple
919+ f_names [f_pos ] = get_fixture_name (f ) if not isinstance (f , str ) else f
920+
921+ # remove duplicates by making it an ordered set
922+ all_names = remove_duplicates ((n for n in f_names if n is not None ))
923+ if len (all_names ) < 1 :
924+ raise ValueError ("Empty fixture products are not permitted" )
925+
926+ def _tuple_generator (all_fixtures ):
927+ for i in range (_tuple_size ):
928+ fix_at_pos_i = f_names [i ]
929+ if fix_at_pos_i is None :
930+ # fixed value
931+ yield fixtures_or_values [i ]
932+ else :
933+ # fixture value
934+ yield all_fixtures [fix_at_pos_i ]
935+
936+ # then generate the body of our product fixture. It will require all of its dependent fixtures
937+ @with_signature ("(%s)" % ', ' .join (all_names ))
938+ def _new_fixture (** all_fixtures ):
939+ return tuple (_tuple_generator (all_fixtures ))
940+
941+ _new_fixture .__name__ = name
942+
943+ # finally create the fixture per se.
944+ # WARNING we do not use pytest.fixture but pytest_fixture_plus so that NOT_USED is discarded
945+ f_decorator = pytest_fixture_plus (scope = scope , autouse = autouse , ids = ids , ** kwargs )
946+ fix = f_decorator (_new_fixture )
947+
948+ # Dynamically add fixture to caller's module as explained in https://github.com/pytest-dev/pytest/issues/2424
949+ check_name_available (caller_module , name , if_name_exists = WARN , caller = param_fixture )
950+ setattr (caller_module , name , fix )
951+
952+ # if unpacking is requested, do it here
953+ if unpack_into is not None :
954+ _unpack_fixture (caller_module , argnames = unpack_into , fixture = name )
955+
956+ return fix
957+
958+
889959class fixture_ref :
890960 """
891961 A reference to a fixture, to be used in `pytest_parametrize_plus`.
@@ -920,21 +990,46 @@ def pytest_parametrize_plus(argnames, argvalues, indirect=False, ids=None, scope
920990 except TypeError :
921991 raise InvalidParamsList (argvalues )
922992
993+ # get the param names
994+ all_param_names = get_param_argnames_as_list (argnames )
995+ nb_params = len (all_param_names )
996+
923997 # find if there are fixture references in the values provided
924998 fixture_indices = []
925- for i , v in enumerate (argvalues ):
926- if isinstance (v , fixture_ref ):
927- fixture_indices .append (i )
999+ if nb_params == 1 :
1000+ for i , v in enumerate (argvalues ):
1001+ if isinstance (v , fixture_ref ):
1002+ fixture_indices .append ((i , None ))
1003+ elif nb_params > 1 :
1004+ for i , v in enumerate (argvalues ):
1005+ try :
1006+ j = 0
1007+ fix_pos = []
1008+ for j , _pval in enumerate (v ):
1009+ if isinstance (_pval , fixture_ref ):
1010+ fix_pos .append (j )
1011+ if len (fix_pos ) > 0 :
1012+ fixture_indices .append ((i , fix_pos ))
1013+ if j + 1 != nb_params :
1014+ raise ValueError ("Invalid parameter values containing %s items while the number of parameters is %s: "
1015+ "%s." % (j + 1 , nb_params , v ))
1016+ except TypeError :
1017+ # a fixture ref is
1018+ if isinstance (v , fixture_ref ):
1019+ fixture_indices .append ((i , None ))
1020+ else :
1021+ raise ValueError (
1022+ "Invalid parameter values containing %s items while the number of parameters is %s: "
1023+ "%s." % (1 , nb_params , v ))
9281024
9291025 if len (fixture_indices ) == 0 :
9301026 # no fixture reference: do as usual
9311027 return pytest .mark .parametrize (argnames , argvalues , indirect = indirect , ids = ids , scope = scope , ** kwargs )
9321028 else :
9331029 # there are fixture references: we have to create a specific decorator
9341030 caller_module = get_caller_module ()
935- all_param_names = get_param_argnames_as_list (argnames )
9361031
937- def create_param_fixture (from_i , to_i , p_fix_name ):
1032+ def _create_param_fixture (from_i , to_i , p_fix_name ):
9381033 """ Routine that will be used to create a parameter fixture for argvalues between prev_i and i"""
9391034 selected_argvalues = argvalues [from_i :to_i ]
9401035 try :
@@ -944,14 +1039,32 @@ def create_param_fixture(from_i, to_i, p_fix_name):
9441039 # a callable to create the ids
9451040 selected_ids = ids
9461041
1042+ # default behaviour is not the same betwee pytest params and pytest fixtures
1043+ if selected_ids is None :
1044+ # selected_ids = ['-'.join([str(_v) for _v in v]) for v in selected_argvalues]
1045+ selected_ids = get_test_ids_from_param_values (all_param_names , selected_argvalues )
1046+
9471047 if to_i == from_i + 1 :
9481048 p_fix_name = "%s_is_%s" % (p_fix_name , from_i )
9491049 else :
9501050 p_fix_name = "%s_is_%sto%s" % (p_fix_name , from_i , to_i - 1 )
951- p_fix_name = check_name_available (caller_module , p_fix_name , if_name_exists = CHANGE , caller = pytest_parametrize_plus )
952- param_fix = _param_fixture (caller_module , p_fix_name , selected_argvalues , selected_ids )
1051+ p_fix_name = check_name_available (caller_module , p_fix_name , if_name_exists = CHANGE ,
1052+ caller = pytest_parametrize_plus )
1053+ param_fix = _param_fixture (caller_module , argname = p_fix_name , argvalues = selected_argvalues ,
1054+ ids = selected_ids )
9531055 return param_fix
9541056
1057+ def _create_fixture_product (argvalue_i , fixture_ref_positions , base_name ):
1058+ # do not use base name - we dont care if there is another in the same module, it will still be more readable
1059+ p_fix_name = "fixtureproduct__%s" % (argvalue_i , )
1060+ p_fix_name = check_name_available (caller_module , p_fix_name , if_name_exists = CHANGE ,
1061+ caller = pytest_parametrize_plus )
1062+ # unpack the fixture references
1063+ _vtuple = argvalues [argvalue_i ]
1064+ fixtures_or_values = tuple (v .fixture if i in fixture_ref_positions else v for i , v in enumerate (_vtuple ))
1065+ product_fix = _fixture_product (caller_module , p_fix_name , fixtures_or_values , fixture_ref_positions )
1066+ return product_fix
1067+
9551068 # then create the decorator
9561069 def parametrize_plus_decorate (test_func ):
9571070 """
@@ -981,21 +1094,32 @@ def parametrize_plus_decorate(test_func):
9811094 fixtures_to_union = []
9821095 fixtures_to_union_names_for_ids = []
9831096 prev_i = - 1
984- for i in fixture_indices :
1097+ for i , j_list in fixture_indices :
9851098 if i > prev_i + 1 :
986- param_fix = create_param_fixture (prev_i + 1 , i , base_name )
1099+ # there was a non-empty group of 'normal' parameters before this fixture_ref.
1100+ # create a new fixture parametrized with all of that consecutive group.
1101+ param_fix = _create_param_fixture (prev_i + 1 , i , base_name )
9871102 fixtures_to_union .append (param_fix )
9881103 fixtures_to_union_names_for_ids .append (get_fixture_name (param_fix ))
9891104
990- fixtures_to_union .append (argvalues [i ].fixture )
991- id_for_fixture = apply_id_style (get_fixture_name (argvalues [i ].fixture ), base_name , IdStyle .explicit )
992- fixtures_to_union_names_for_ids .append (id_for_fixture )
1105+ if j_list is None :
1106+ # add the fixture referenced with `fixture_ref`
1107+ referenced_fixture = argvalues [i ].fixture
1108+ fixtures_to_union .append (referenced_fixture )
1109+ id_for_fixture = apply_id_style (get_fixture_name (referenced_fixture ), base_name , IdStyle .explicit )
1110+ fixtures_to_union_names_for_ids .append (id_for_fixture )
1111+ else :
1112+ # create a fixture refering to all the fixtures required in the tuple
1113+ prod_fix = _create_fixture_product (i , j_list , base_name )
1114+ fixtures_to_union .append (prod_fix )
1115+ id_for_fixture = apply_id_style (get_fixture_name (prod_fix ), base_name , IdStyle .explicit )
1116+ fixtures_to_union_names_for_ids .append (id_for_fixture )
9931117 prev_i = i
9941118
995- # last bit if any
1119+ # handle last consecutive group of normal parameters, if any
9961120 i = len (argvalues )
9971121 if i > prev_i + 1 :
998- param_fix = create_param_fixture (prev_i + 1 , i , base_name )
1122+ param_fix = _create_param_fixture (prev_i + 1 , i , base_name )
9991123 fixtures_to_union .append (param_fix )
10001124 fixtures_to_union_names_for_ids .append (get_fixture_name (param_fix ))
10011125
@@ -1016,7 +1140,7 @@ def replace_paramfixture_with_values(kwargs):
10161140 # remove the created fixture value
10171141 encompassing_fixture = kwargs .pop (base_name )
10181142 # and add instead the parameter values
1019- if len ( all_param_names ) > 1 :
1143+ if nb_params > 1 :
10201144 for i , p in enumerate (all_param_names ):
10211145 kwargs [p ] = encompassing_fixture [i ]
10221146 else :
0 commit comments