From ffd591826c378cc4d2ad427715f6a33ee281ad38 Mon Sep 17 00:00:00 2001 From: Sylvain MARIE Date: Thu, 28 May 2020 15:51:45 +0200 Subject: [PATCH 01/10] type hints --- pytest_cases/main_fixtures.py | 70 +++++++++++++++++++++++++++-------- 1 file changed, 55 insertions(+), 15 deletions(-) diff --git a/pytest_cases/main_fixtures.py b/pytest_cases/main_fixtures.py index 31c00511..a86967a1 100644 --- a/pytest_cases/main_fixtures.py +++ b/pytest_cases/main_fixtures.py @@ -45,7 +45,10 @@ from pytest_cases.main_params import cases_data -def unpack_fixture(argnames, fixture, hook=None): +def unpack_fixture(argnames, + fixture, + hook=None # type: Callable[[Callable], Callable] + ): """ Creates several fixtures with names `argnames` from the source `fixture`. Created fixtures will correspond to elements unpacked from `fixture` in order. For example if `fixture` is a tuple of length 2, `argnames="a,b"` will @@ -135,7 +138,13 @@ def _param_fixture(**kwargs): return created_fixtures -def param_fixture(argname, argvalues, autouse=False, ids=None, scope="function", hook=None, **kwargs): +def param_fixture(argname, + argvalues, + autouse=False, # type: bool + ids=None, # type: Union[Callable, List[str]] + scope="function", # type: str + hook=None, # type: Callable[[Callable], Callable] + **kwargs): """ Identical to `param_fixtures` but for a single parameter name, so that you can assign its output to a single variable. @@ -179,7 +188,13 @@ def test_uses_param(my_parameter, fixture_uses_param): hook=hook, **kwargs) -def _param_fixture(caller_module, argname, argvalues, autouse=False, ids=None, scope="function", hook=None, +def _param_fixture(caller_module, + argname, + argvalues, + autouse=False, # type: bool + ids=None, # type: Union[Callable, List[str]] + scope="function", # type: str + hook=None, # type: Callable[[Callable], Callable] **kwargs): """ Internal method shared with param_fixture and param_fixtures """ @@ -262,7 +277,13 @@ def check_name_available(module, return name -def param_fixtures(argnames, argvalues, autouse=False, ids=None, scope="function", hook=None, **kwargs): +def param_fixtures(argnames, + argvalues, + autouse=False, # type: bool + ids=None, # type: Union[Callable, List[str]] + scope="function", # type: str + hook=None, # type: Callable[[Callable], Callable] + **kwargs): """ Creates one or several "parameters" fixtures - depending on the number or coma-separated names in `argnames`. The created fixtures are automatically registered into the callers' module, but you may wish to assign them to @@ -461,11 +482,11 @@ def _fixture_plus(f): @function_decorator -def fixture_plus(scope="function", - autouse=False, - name=None, - unpack_into=None, - hook=None, +def fixture_plus(scope="function", # type: str + autouse=False, # type: bool + name=None, # type: str + unpack_into=None, # type: Iterable[str] + hook=None, # type: Callable[[Callable], Callable] fixture_func=DECORATED, **kwargs): """ decorator to mark a fixture factory function. @@ -1000,9 +1021,16 @@ def _new_fixture(request, **all_fixtures): return fix -def _fixture_product(caller_module, name, fixtures_or_values, fixture_positions, - scope="function", ids=fixture_alternative_to_str, - unpack_into=None, autouse=False, hook=None, **kwargs): +def _fixture_product(caller_module, + name, # type: str + fixtures_or_values, + fixture_positions, + scope="function", # type: str + ids=fixture_alternative_to_str, # type: Union[Callable, List[str]] + unpack_into=None, # type: Iterable[str] + autouse=False, # type: bool + hook=None, # type: Callable[[Callable], Callable] + **kwargs): """ Internal implementation for fixture products created by pytest parametrize plus. @@ -1094,7 +1122,13 @@ def pytest_parametrize_plus(*args, return _parametrize_plus(*args, **kwargs) -def parametrize_plus(argnames, argvalues, indirect=False, ids=None, scope=None, hook=None, **kwargs): +def parametrize_plus(argnames, + argvalues, + indirect=False, # type: bool + ids=None, # type: Union[Callable, List[str]] + scope=None, # type: str + hook=None, # type: Callable[[Callable], Callable] + **kwargs): """ Equivalent to `@pytest.mark.parametrize` but also supports the fact that in argvalues one can include references to fixtures with `fixture_ref()` where can be the fixture name or fixture function. @@ -1119,8 +1153,14 @@ def parametrize_plus(argnames, argvalues, indirect=False, ids=None, scope=None, **kwargs) -def _parametrize_plus(argnames, argvalues, indirect=False, ids=None, scope=None, hook=None, - _frame_offset=2, **kwargs): +def _parametrize_plus(argnames, + argvalues, + indirect=False, # type: bool + ids=None, # type: Union[Callable, List[str]] + scope=None, # type: str + hook=None, # type: Callable[[Callable], Callable] + _frame_offset=2, + **kwargs): # make sure that we do not destroy the argvalues if it is provided as an iterator try: argvalues = list(argvalues) From e66abc24e16de143a95ef1bd5e5d4d3db81ac7c4 Mon Sep 17 00:00:00 2001 From: Sylvain MARIE Date: Fri, 29 May 2020 18:01:48 +0200 Subject: [PATCH 02/10] Better support for `pytest.param` in `parametrize_plus`. Fixed #79. Improved corresponding ids. Fixed #86 --- pytest_cases/common.py | 105 ++++-- pytest_cases/main_fixtures.py | 689 +++++++++++++++++++++++----------- pytest_cases/plugin.py | 16 +- 3 files changed, 548 insertions(+), 262 deletions(-) diff --git a/pytest_cases/common.py b/pytest_cases/common.py index 721fbfb4..76cf0bca 100644 --- a/pytest_cases/common.py +++ b/pytest_cases/common.py @@ -274,21 +274,66 @@ def combine_ids(paramid_tuples): return ['-'.join(pid for pid in testid) for testid in paramid_tuples] -def get_test_ids_from_param_values(param_names, - param_values, - ): +def make_test_ids(global_ids, id_marks, argnames=None, argvalues=None, precomputed_ids=None): """ - Replicates pytest behaviour to generate the ids when there are several parameters in a single `parametrize` + Creates the proper id for each test based on (higher precedence first) + + - any specific id mark from a `pytest.param` (`id_marks`) + - the global `ids` argument of pytest parametrize (`global_ids`) + - the name and value of parameters (`argnames`, `argvalues`) or the precomputed ids(`precomputed_ids`) + + See also _pytest.python._idvalset method + + :param global_ids: + :param id_marks: + :param argnames: + :param argvalues: + :param precomputed_ids: + :return: + """ + if global_ids is not None: + # overridden at global pytest.mark.parametrize level - this takes precedence. + try: # an explicit list of ids ? + p_ids = list(global_ids) + except TypeError: # a callable to apply on the values + p_ids = list(global_ids(v) for v in argvalues) + else: + # default: values-based + if precomputed_ids is not None: + if argnames is not None or argvalues is not None: + raise ValueError("Only one of `precomputed_ids` or argnames/argvalues should be provided.") + p_ids = precomputed_ids + else: + p_ids = make_test_ids_from_param_values(argnames, argvalues) + + # Finally, local pytest.param takes precedence over everything else + for i, _id in enumerate(id_marks): + if _id is not None: + p_ids[i] = _id + return p_ids + + +def make_test_ids_from_param_values(param_names, + param_values, + ): + """ + Replicates pytest behaviour to generate the ids when there are several parameters in a single `parametrize. + Note that param_values should not contain marks. :param param_names: :param param_values: :return: a list of param ids """ + if isinstance(param_names, string_types): + raise TypeError("param_names must be an iterable. Found %r" % param_names) + nb_params = len(param_names) if nb_params == 0: raise ValueError("empty list provided") elif nb_params == 1: - paramids = list(str(v) for v in param_values) + paramids = [] + for v in param_values: + paramids.append(str(v)) else: paramids = [] for vv in param_values: @@ -300,12 +345,17 @@ def get_test_ids_from_param_values(param_names, # ---- ParameterSet api --- -def analyze_parameter_set(pmark=None, argnames=None, argvalues=None, ids=None): +def analyze_parameter_set(pmark=None, argnames=None, argvalues=None, ids=None, check_nb=True): """ analyzes a parameter set passed either as a pmark or as distinct (argnames, argvalues, ids) to extract/construct the various ids, marks, and values + See also pytest.Metafunc.parametrize method, that calls in particular + pytest.ParameterSet._for_parametrize and _pytest.python._idvalset + + :param check_nb: a bool indicating if we should raise an error if len(argnames) > 1 and any argvalue has + a different length than len(argnames) :return: ids, marks, values """ if pmark is not None: @@ -316,38 +366,31 @@ def analyze_parameter_set(pmark=None, argnames=None, argvalues=None, ids=None): ids = pmark.param_ids # extract all parameters that have a specific configuration (pytest.param()) - custom_pids, p_marks, p_values = extract_parameterset_info(argnames, argvalues) - - # Create the proper id for each test - if ids is not None: - # overridden at global pytest.mark.parametrize level - this takes precedence. - try: # an explicit list of ids ? - p_ids = list(ids) - except TypeError: # a callable to apply on the values - p_ids = list(ids(v) for v in p_values) - else: - # default: values-based - p_ids = get_test_ids_from_param_values(argnames, p_values) + custom_pids, p_marks, p_values = extract_parameterset_info(argnames, argvalues, check_nb=check_nb) - # Finally, local pytest.param takes precedence over everything else - for i, _id in enumerate(custom_pids): - if _id is not None: - p_ids[i] = _id + # get the ids by merging/creating the various possibilities + p_ids = make_test_ids(argnames=argnames, argvalues=p_values, global_ids=ids, id_marks=custom_pids) return p_ids, p_marks, p_values -def extract_parameterset_info(argnames, argvalues): +def extract_parameterset_info(argnames, argvalues, check_nb=True): """ :param argnames: the names in this parameterset :param argvalues: the values in this parameterset + :param check_nb: a bool indicating if we should raise an error if len(argnames) > 1 and any argvalue has + a different length than len(argnames) :return: """ pids = [] pmarks = [] pvalues = [] + if isinstance(argnames, string_types): + raise TypeError("argnames must be an iterable. Found %r" % argnames) + nbnames = len(argnames) for v in argvalues: + # is this a pytest.param() ? if is_marked_parameter_value(v): # --id id = get_marked_parameter_id(v) @@ -356,19 +399,21 @@ def extract_parameterset_info(argnames, argvalues): marks = get_marked_parameter_marks(v) pmarks.append(marks) # note: there might be several # --value(a tuple if this is a tuple parameter) - vals = get_marked_parameter_values(v) - if len(vals) != len(argnames): - raise ValueError("Internal error - unsupported pytest parametrization+mark combination. Please " - "report this issue") - if len(vals) == 1: - pvalues.append(vals[0]) + v = get_marked_parameter_values(v) + if nbnames == 1: + pvalues.append(v[0]) else: - pvalues.append(vals) + pvalues.append(v) else: + # normal argvalue pids.append(None) pmarks.append(None) pvalues.append(v) + if check_nb and nbnames > 1 and (len(v) != nbnames): + raise ValueError("Inconsistent number of values in pytest parametrize: %s items found while the " + "number of parameters is %s: %s." % (len(v), nbnames, v)) + return pids, pmarks, pvalues diff --git a/pytest_cases/main_fixtures.py b/pytest_cases/main_fixtures.py index a86967a1..cdd97ef4 100644 --- a/pytest_cases/main_fixtures.py +++ b/pytest_cases/main_fixtures.py @@ -2,7 +2,6 @@ from __future__ import division from distutils.version import LooseVersion -from enum import Enum from inspect import isgeneratorfunction, getmodule, currentframe from itertools import product from warnings import warn @@ -39,9 +38,9 @@ except ImportError: pass -from pytest_cases.common import yield_fixture, get_pytest_parametrize_marks, get_test_ids_from_param_values, \ - make_marked_parameter_value, get_fixture_name, get_param_argnames_as_list, analyze_parameter_set, combine_ids, \ - get_fixture_scope, remove_duplicates +from pytest_cases.common import yield_fixture, get_pytest_parametrize_marks, make_marked_parameter_value, \ + get_fixture_name, get_param_argnames_as_list, analyze_parameter_set, combine_ids, get_fixture_scope, \ + remove_duplicates, extract_parameterset_info, is_marked_parameter_value, get_marked_parameter_values from pytest_cases.main_params import cases_data @@ -184,27 +183,41 @@ def test_uses_param(my_parameter, fixture_uses_param): caller_module = get_caller_module() - return _param_fixture(caller_module, argname, argvalues, autouse=autouse, ids=ids, scope=scope, - hook=hook, **kwargs) + return _create_param_fixture(caller_module, argname, argvalues, autouse=autouse, ids=ids, scope=scope, + hook=hook, **kwargs) -def _param_fixture(caller_module, - argname, - argvalues, - autouse=False, # type: bool - ids=None, # type: Union[Callable, List[str]] - scope="function", # type: str - hook=None, # type: Callable[[Callable], Callable] - **kwargs): +def _create_param_fixture(caller_module, + argname, + argvalues, + autouse=False, # type: bool + ids=None, # type: Union[Callable, List[str]] + scope="function", # type: str + hook=None, # type: Callable[[Callable], Callable] + auto_simplify=False, + **kwargs): """ Internal method shared with param_fixture and param_fixtures """ - # create the fixture - set its name so that the optional hook can read it easily - @with_signature("%s(request)" % argname) - def __param_fixture(request): - return request.param + if auto_simplify and len(argvalues) == 1: + # Simplification: do not parametrize the fixture, it will directly return the single value + argvalue_to_return = argvalues[0] + if is_marked_parameter_value(argvalue_to_return): + argvalue_to_return = get_marked_parameter_values(argvalue_to_return) - fix = fixture_plus(name=argname, scope=scope, autouse=autouse, params=argvalues, ids=ids, - hook=hook, **kwargs)(__param_fixture) + # create the fixture - set its name so that the optional hook can read it easily + @with_signature("%s()" % argname) + def __param_fixture(): + return argvalue_to_return + + fix = fixture_plus(name=argname, scope=scope, autouse=autouse, ids=ids, hook=hook, **kwargs)(__param_fixture) + else: + # create the fixture - set its name so that the optional hook can read it easily + @with_signature("%s(request)" % argname) + def __param_fixture(request): + return request.param + + fix = fixture_plus(name=argname, scope=scope, autouse=autouse, params=argvalues, ids=ids, + hook=hook, **kwargs)(__param_fixture) # Dynamically add fixture to caller's module as explained in https://github.com/pytest-dev/pytest/issues/2424 check_name_available(caller_module, argname, if_name_exists=WARN, caller=param_fixture) @@ -326,8 +339,8 @@ def test_uses_param2(arg1, arg2, fixture_uses_param2): caller_module = get_caller_module() if len(argnames_lst) < 2: - return _param_fixture(caller_module, argnames, argvalues, autouse=autouse, ids=ids, scope=scope, - hook=hook, **kwargs) + return _create_param_fixture(caller_module, argnames, argvalues, autouse=autouse, ids=ids, scope=scope, + hook=hook, **kwargs) # create the root fixture that will contain all parameter values # note: we sort the list so that the first in alphabetical order appears first. Indeed pytest uses this order. @@ -530,7 +543,7 @@ def _decorate_fixture_plus(fixture_func, autouse=False, name=None, unpack_into=None, - hook=None, + hook=None, # type: Callable[[Callable], Callable] _caller_module_offset_when_unpack=3, **kwargs): """ decorator to mark a fixture factory function. @@ -611,7 +624,7 @@ def _decorate_fixture_plus(fixture_func, params_names_or_name_combinations.append(pmark.param_names) # analyse contents, extract marks and custom ids, apply custom ids - _paramids, _pmarks, _pvalues = analyze_parameter_set(pmark) + _paramids, _pmarks, _pvalues = analyze_parameter_set(pmark=pmark, check_nb=True) # Finally store the ids, marks, and values for this parameterset params_ids.append(_paramids) @@ -795,56 +808,69 @@ def __repr__(self): """Object representing a fixture value when the fixture is not used""" +class UnionIdMakers(object): + """ + The enum defining all possible id styles for union fixture parameters ("alternatives") + """ + @classmethod + def nostyle(cls, param): + return param.alternative_name + + @classmethod + def explicit(cls, param): + return "%s_is_%s" % (param.union_name, param.alternative_name) + + @classmethod + def compact(cls, param): + return "U%s" % param.alternative_name + + @classmethod + def get(cls, style # type: str + ): + # type: (...) -> Callable[[Any], str] + """ + Returns a function that one can use as the `ids` argument in parametrize, applying the given id style. + See https://github.com/smarie/python-pytest-cases/issues/41 + + :param idstyle: + :return: + """ + style = style or 'nostyle' + try: + return getattr(cls, style) + except AttributeError: + raise ValueError("Unknown style: %r" % style) + + class UnionFixtureAlternative(object): - """A special class that should be used to wrap a fixture name""" + """Defines an "alternative", used to parametrize a fixture union""" + __slots__ = 'union_name', 'alternative_name' def __init__(self, - fixture_name, - idstyle # type: IdStyle + union_name, + alternative_name, ): - self.fixture_name = fixture_name - self.idstyle = idstyle + self.union_name = union_name + self.alternative_name = alternative_name # def __str__(self): - # that is maybe too dangerous... - # return self.fixture_name + # # although this would be great to have a default id directly, it may be + # # very confusion for debugging so I prefer that we use id_maker('none') + # # to make this default behaviour explicit and not pollute the debugging process + # return self.alternative_name def __repr__(self): - return "UnionAlternative<%s, idstyle=%s>" % (self.fixture_name, self.idstyle) + return "%s<%s=%s>" % (self.__class__.__name__, self.union_name, self.alternative_name) @staticmethod def to_list_of_fixture_names(alternatives_lst # type: List[UnionFixtureAlternative] ): - return [f.fixture_name for f in alternatives_lst] - - -class IdStyle(Enum): - """ - The enum defining all possible id styles. - """ - none = None - explicit = 'explicit' - compact = 'compact' - - -def apply_id_style(id, union_fixture_name, idstyle): - """ - Applies the id style defined in `idstyle` to the given id. - See https://github.com/smarie/python-pytest-cases/issues/41 - - :param id: - :param union_fixture_name: - :param idstyle: - :return: - """ - if idstyle is IdStyle.none: - return id - elif idstyle is IdStyle.explicit: - return "%s_is_%s" % (union_fixture_name, id) - elif idstyle is IdStyle.compact: - return "U%s" % id - else: - raise ValueError("Invalid id style") + res = [] + for f in alternatives_lst: + if is_marked_parameter_value(f): + f = get_marked_parameter_values(f)[0] + res.append(f.alternative_name) + return res class InvalidParamsList(Exception): @@ -868,7 +894,13 @@ def is_fixture_union_params(params): :return: """ try: - return len(params) >= 1 and isinstance(params[0], UnionFixtureAlternative) + if len(params) < 1: + return False + else: + p0 = params[0] + if is_marked_parameter_value(p0): + p0 = get_marked_parameter_values(p0)[0] + return isinstance(p0, UnionFixtureAlternative) except TypeError: raise InvalidParamsList(params) @@ -886,19 +918,14 @@ def is_used_request(request): return getattr(request, 'param', None) is not NOT_USED -def fixture_alternative_to_str(fixture_alternative, # type: UnionFixtureAlternative - ): - return fixture_alternative.fixture_name - - -def fixture_union(name, - fixtures, - scope="function", - idstyle='explicit', - ids=fixture_alternative_to_str, - unpack_into=None, - autouse=False, - hook=None, +def fixture_union(name, # type: str + fixtures, # type: Iterable[Union[str, Callable]] + scope="function", # type: str + idstyle='explicit', # type: Optional[str] + ids=None, # type: Union[Callable, List[str]] + unpack_into=None, # type: Iterable[str] + autouse=False, # type: bool + hook=None, # type: Callable[[Callable], Callable] **kwargs): """ Creates a fixture that will take all values of the provided fixtures in order. That fixture is automatically @@ -932,26 +959,62 @@ def fixture_union(name, automatically registered in your module. However if you decide to do so make sure that you use the same name. """ caller_module = get_caller_module() - return _fixture_union(caller_module, name, fixtures, scope=scope, idstyle=idstyle, ids=ids, autouse=autouse, + + # test the `fixtures` argument to avoid common mistakes + if not isinstance(fixtures, (tuple, set, list)): + raise TypeError("fixture_union: the `fixtures` argument should be a tuple, set or list") + + # unpack the pytest.param marks + custom_pids, p_marks, fixtures = extract_parameterset_info((name, ), fixtures) + + # get all required fixture names + f_names = [] + for f in fixtures: + # possibly get the fixture name if the fixture symbol was provided + f_names.append(get_fixture_name(f)) + + # create all alternatives and reapply the marks on them + fix_alternatives = [] + f_names_args = [] + for _name, _id, _mark in zip(f_names, custom_pids, p_marks): + # create the alternative object + alternative = UnionFixtureAlternative(union_name=name, alternative_name=_name) + + # remove duplicates in the fixture arguments: each is required only once by the union fixture to create + if _name in f_names_args: + warn("Creating a fixture union %r where two alternatives are the same fixture %r." % (name, _name)) + else: + f_names_args.append(_name) + + # reapply the marks + if _id is not None or (_mark or ()) != (): + alternative = pytest.param(alternative, id=_id, marks=_mark or ()) + fix_alternatives.append(alternative) + + return _fixture_union(caller_module, name, + fix_alternatives=fix_alternatives, unique_fix_alt_names=f_names_args, + scope=scope, idstyle=idstyle, ids=ids, autouse=autouse, hook=hook, unpack_into=unpack_into, **kwargs) def _fixture_union(caller_module, - name, # type: str - fixtures, # type: Iterable[Union[str, Callable]] - idstyle, # type: Optional[Union[str, IdStyle]] - scope="function", # type: str - ids=fixture_alternative_to_str, # type: Union[Callable, List[str]] - unpack_into=None, # type: Iterable[str] - autouse=False, # type: bool - hook=None, # type: Callable + name, # type: str + fix_alternatives, # type: Iterable[UnionFixtureAlternative] + unique_fix_alt_names, # type: List[str] + scope="function", # type: str + idstyle="explicit", # type: str + ids=None, # type: Union[Callable, List[str]] + unpack_into=None, # type: Iterable[str] + autouse=False, # type: bool + hook=None, # type: Callable[[Callable], Callable] **kwargs): """ Internal implementation for fixture_union :param caller_module: :param name: - :param fixtures: + :param fix_alternatives: + :param unique_fix_alt_names: :param idstyle: :param scope: :param ids: @@ -964,49 +1027,32 @@ def _fixture_union(caller_module, :param kwargs: :return: """ - # test the `fixtures` argument to avoid common mistakes - if not isinstance(fixtures, (tuple, set, list)): - raise TypeError("fixture_union: the `fixtures` argument should be a tuple, set or list") - - # validate the idstyle - idstyle = IdStyle(idstyle) - - # first get all required fixture names - f_names = [] - for f in fixtures: - # possibly get the fixture name if the fixture symbol was provided - f_names.append(get_fixture_name(f)) + # get the ids generator corresponding to the idstyle + if ids is None: + ids = UnionIdMakers.get(idstyle) - if len(f_names) < 1: + if len(fix_alternatives) < 1: raise ValueError("Empty fixture unions are not permitted") - # remove duplicates in the fixture arguments - f_names_args = [] - for _fname in f_names: - if _fname in f_names_args: - warn("Creating a fixture union %r where two alternatives are the same fixture %r." % (name, _fname)) - else: - f_names_args.append(_fname) - # then generate the body of our union fixture. It will require all of its dependent fixtures and receive as # a parameter the name of the fixture to use - @with_signature("%s(%s, request)" % (name, ', '.join(f_names_args))) + @with_signature("%s(%s, request)" % (name, ', '.join(unique_fix_alt_names))) def _new_fixture(request, **all_fixtures): if not is_used_request(request): return NOT_USED else: - alternative = request.param - if isinstance(alternative, UnionFixtureAlternative): - fixture_to_use = alternative.fixture_name + _alternative = request.param + if isinstance(_alternative, UnionFixtureAlternative): + fixture_to_use = _alternative.alternative_name return all_fixtures[fixture_to_use] else: raise TypeError("Union Fixture %s received invalid parameter type: %s. Please report this issue." - "" % (name, alternative.__class__)) + "" % (name, _alternative.__class__)) # finally create the fixture per se. # WARNING we do not use pytest.fixture but fixture_plus so that NOT_USED is discarded f_decorator = fixture_plus(scope=scope, - params=[UnionFixtureAlternative(_name, idstyle) for _name in f_names], + params=fix_alternatives, autouse=autouse, ids=ids, hook=hook, **kwargs) fix = f_decorator(_new_fixture) @@ -1022,14 +1068,14 @@ def _new_fixture(request, **all_fixtures): def _fixture_product(caller_module, - name, # type: str + name, # type: str fixtures_or_values, fixture_positions, - scope="function", # type: str - ids=fixture_alternative_to_str, # type: Union[Callable, List[str]] - unpack_into=None, # type: Iterable[str] - autouse=False, # type: bool - hook=None, # type: Callable[[Callable], Callable] + scope="function", # type: str + ids=None, # type: Union[Callable, List[str]] + unpack_into=None, # type: Iterable[str] + autouse=False, # type: bool + hook=None, # type: Callable[[Callable], Callable] **kwargs): """ Internal implementation for fixture products created by pytest parametrize plus. @@ -1057,6 +1103,8 @@ def _fixture_product(caller_module, for f_pos in fixture_positions: # possibly get the fixture name if the fixture symbol was provided f = fixtures_or_values[f_pos] + if isinstance(f, fixture_ref): + f = f.fixture # and remember the position in the tuple f_names[f_pos] = get_fixture_name(f) @@ -1122,12 +1170,132 @@ def pytest_parametrize_plus(*args, return _parametrize_plus(*args, **kwargs) +class ParamIdMakers(object): + # @staticmethod + # def nostyle(param): + # return param.alternative_name + + @staticmethod + def explicit(param # type: ParamAlternative + ): + if isinstance(param, SingleParamAlternative): + # return "%s_is_P%s" % (param.param_name, param.value_index) + return "%s_is_%s" % (param.param_name, param.value) + elif isinstance(param, MultiParamAlternative): + return "%s_is_P%stoP%s" % (param.param_name, param.value_index_from, param.value_index_to - 1) + elif isinstance(param, FixtureParamAlternative): + return "%s_is_%s" % (param.param_name, param.alternative_name) + elif isinstance(param, ProductParamAlternative): + return "%s_is_P%s" % (param.param_name, param.value_index) + else: + raise TypeError("Unsupported alternative: %r" % param) + + # @staticmethod + # def compact(param): + # return "U%s" % param.alternative_name + + @classmethod + def get(cls, style # type: str + ): + # type: (...) -> Callable[[Any], str] + """ + Returns a function that one can use as the `ids` argument in parametrize, applying the given id style. + See https://github.com/smarie/python-pytest-cases/issues/41 + + :param idstyle: + :return: + """ + style = style or 'nostyle' + try: + return getattr(cls, style) + except AttributeError: + raise ValueError("Unknown style: %r" % style) + + +class ParamAlternative(UnionFixtureAlternative): + """Defines an "alternative", used to parametrize a fixture union in the context of parametrize_plus""" + __slots__ = ('param_name', ) + + def __init__(self, + union_name, + alternative_name, + param_name, + ): + super(ParamAlternative, self).__init__(union_name=union_name, alternative_name=alternative_name) + self.param_name = param_name + + +class SingleParamAlternative(ParamAlternative): + """alternative class for single parameter value""" + __slots__ = 'value_index', 'value' + + def __init__(self, + union_name, + alternative_name, + param_name, + value_index, + value + ): + super(SingleParamAlternative, self).__init__(union_name=union_name, alternative_name=alternative_name, + param_name=param_name) + self.value_index = value_index + self.value = value + + +class MultiParamAlternative(ParamAlternative): + """alternative class for multiple parameter values""" + __slots__ = 'value_index_from', 'value_index_to' + + def __init__(self, + union_name, + alternative_name, + param_name, + value_index_from, + value_index_to + ): + super(MultiParamAlternative, self).__init__(union_name=union_name, alternative_name=alternative_name, + param_name=param_name) + self.value_index_from = value_index_from + self.value_index_to = value_index_to + + +class FixtureParamAlternative(ParamAlternative): + """alternative class for a single parameter containing a fixture ref""" + __slots__ = 'value_index', + + def __init__(self, + union_name, + alternative_name, + param_name, + value_index, + ): + super(FixtureParamAlternative, self).__init__(union_name=union_name, alternative_name=alternative_name, + param_name=param_name) + self.value_index = value_index + + +class ProductParamAlternative(ParamAlternative): + """alternative class for a single product parameter containing fixture refs""" + __slots__ = 'value_index' + + def __init__(self, + union_name, + alternative_name, + param_name, + value_index, + ): + super(ProductParamAlternative, self).__init__(union_name=union_name, alternative_name=alternative_name, + param_name=param_name) + self.value_index = value_index + + def parametrize_plus(argnames, argvalues, - indirect=False, # type: bool - ids=None, # type: Union[Callable, List[str]] - scope=None, # type: str - hook=None, # type: Callable[[Callable], Callable] + indirect=False, # type: bool + ids=None, # type: Union[Callable, List[str]] + idstyle='explicit', # type: str + scope=None, # type: str + hook=None, # type: Callable[[Callable], Callable] **kwargs): """ Equivalent to `@pytest.mark.parametrize` but also supports the fact that in argvalues one can include references to @@ -1141,6 +1309,7 @@ def parametrize_plus(argnames, :param argvalues: :param indirect: :param ids: + :param idstyle: :param scope: :param hook: an optional hook to apply to each fixture function that is created during this call. The hook function will be called everytime a fixture is about to be created. It will receive a single argument (the function @@ -1149,16 +1318,17 @@ def parametrize_plus(argnames, :param kwargs: :return: """ - return _parametrize_plus(argnames, argvalues, indirect=indirect, ids=ids, scope=scope, hook=hook, + return _parametrize_plus(argnames, argvalues, indirect=indirect, ids=ids, idstyle=idstyle, scope=scope, hook=hook, **kwargs) def _parametrize_plus(argnames, argvalues, - indirect=False, # type: bool - ids=None, # type: Union[Callable, List[str]] - scope=None, # type: str - hook=None, # type: Callable[[Callable], Callable] + indirect=False, # type: bool + ids=None, # type: Union[Callable, List[str]] + idstyle='explicit', # type: str + scope=None, # type: str + hook=None, # type: Callable[[Callable], Callable] _frame_offset=2, **kwargs): # make sure that we do not destroy the argvalues if it is provided as an iterator @@ -1168,8 +1338,14 @@ def _parametrize_plus(argnames, raise InvalidParamsList(argvalues) # get the param names - all_param_names = get_param_argnames_as_list(argnames) - nb_params = len(all_param_names) + initial_argnames = argnames + argnames = get_param_argnames_as_list(argnames) + nb_params = len(argnames) + + # extract all marks and custom ids. + # Do not check consistency of sizes argname/argvalue as a fixture_ref can stand for several argvalues. + marked_argvalues = argvalues + custom_pids, p_marks, argvalues = extract_parameterset_info(argnames, argvalues, check_nb=False) # find if there are fixture references in the values provided fixture_indices = [] @@ -1179,73 +1355,126 @@ def _parametrize_plus(argnames, fixture_indices.append((i, None)) elif nb_params > 1: for i, v in enumerate(argvalues): - try: - j = 0 - fix_pos = [] - for j, _pval in enumerate(v): - if isinstance(_pval, fixture_ref): - fix_pos.append(j) - if len(fix_pos) > 0: - fixture_indices.append((i, fix_pos)) - if j+1 != nb_params: - raise ValueError("Invalid parameter values containing %s items while the number of parameters is %s: " - "%s." % (j+1, nb_params, v)) - except TypeError: - # a fixture ref is - if isinstance(v, fixture_ref): - fixture_indices.append((i, None)) - else: - raise ValueError( - "Invalid parameter values containing %s items while the number of parameters is %s: " - "%s." % (1, nb_params, v)) + if isinstance(v, fixture_ref): + # a fixture ref is used for several parameters at the same time + fixture_indices.append((i, None)) + elif len(v) == 1 and isinstance(v[0], fixture_ref): + # same than above but it was in a pytest.mark + # a fixture ref is used for several parameters at the same time + fixture_indices.append((i, None)) + # unpack it + argvalues[i] = v[0] + else: + # check for consistency + if len(v) != len(argnames): + raise ValueError("Inconsistent number of values in pytest parametrize: %s items found while the " + "number of parameters is %s: %s." % (len(v), len(argnames), v)) + + # let's dig into the tuple + fix_pos_list = [j for j, _pval in enumerate(v) if isinstance(_pval, fixture_ref)] + if len(fix_pos_list) > 0: + # there is at least one fixture ref inside the tuple + fixture_indices.append((i, fix_pos_list)) + del i if len(fixture_indices) == 0: - # no fixture reference: do as usual - return pytest.mark.parametrize(argnames, argvalues, indirect=indirect, ids=ids, scope=scope, **kwargs) + # no fixture reference: shortcut, do as usual (note that the hook wont be called since no fixture is created) + return pytest.mark.parametrize(initial_argnames, marked_argvalues, indirect=indirect, + ids=ids, scope=scope, **kwargs) else: # there are fixture references: we have to create a specific decorator caller_module = get_caller_module(frame_offset=_frame_offset) - def _create_param_fixture(from_i, to_i, p_names, test_func_name, hook): + def _create_params_alt(union_name, from_i, to_i, param_names_str, test_func_name, hook): """ Routine that will be used to create a parameter fixture for argvalues between prev_i and i""" - selected_argvalues = argvalues[from_i:to_i] - try: - # an explicit list of ids - selected_ids = ids[from_i:to_i] - except TypeError: - # a callable to create the ids - selected_ids = ids - # default behaviour is not the same between pytest params and pytest fixtures - if selected_ids is None: - # selected_ids = ['-'.join([str(_v) for _v in v]) for v in selected_argvalues] - selected_ids = get_test_ids_from_param_values(all_param_names, selected_argvalues) + single_param = (to_i == from_i + 1) + + if single_param: + i = from_i + + # Create a unique fixture name + p_fix_name = "%s_%s_P%s" % (test_func_name, param_names_str, i) + p_fix_name = check_name_available(caller_module, p_fix_name, if_name_exists=CHANGE, + caller=parametrize_plus) + # Create the fixture that will return the unique parameter value ("auto-simplify" flag) + # IMPORTANT that fixture is NOT parametrized so has no id nor marks: use argvalues not marked_argvalues + _create_param_fixture(caller_module, argname=p_fix_name, argvalues=argvalues[i], hook=hook, + auto_simplify=True) + + # Create the alternative + p_fix_alt = SingleParamAlternative(union_name=union_name, alternative_name=p_fix_name, + param_name=param_names_str, value_index=i, value=argvalues[i]) + # Finally copy the custom id/marks on the ParamAlternative if any + if is_marked_parameter_value(marked_argvalues[i]): + p_fix_alt = pytest.param(p_fix_alt, id=marked_argvalues[i].id, marks=marked_argvalues[i].marks) - if to_i == from_i + 1: - p_names_with_idx = "%s_is_%s" % (p_names, from_i) else: - p_names_with_idx = "%s_is_%sto%s" % (p_names, from_i, to_i - 1) + # Create a unique fixture name + p_fix_name = "%s_%s_is_P%stoP%s" % (test_func_name, param_names_str, from_i, to_i - 1) + p_fix_name = check_name_available(caller_module, p_fix_name, if_name_exists=CHANGE, + caller=parametrize_plus) + + # If an explicit list of ids was provided, slice it. Otherwise use the provided callable + try: + p_ids = ids[from_i:to_i] + except TypeError: + p_ids = ids # callable + + # Create the fixture that will take all these parameter values + # That fixture WILL be parametrized, this is why we propagate the p_ids and use the marked values + _create_param_fixture(caller_module, argname=p_fix_name, argvalues=marked_argvalues[from_i:to_i], + ids=p_ids, hook=hook) + + # Create the corresponding alternative + p_fix_alt = MultiParamAlternative(union_name=union_name, alternative_name=p_fix_name, + param_name=param_names_str, + value_index_from=from_i, value_index_to=to_i) + # no need to copy the custom id/marks to the ParamAlternative: they were passed above already + + return p_fix_name, p_fix_alt + + def _create_fixture_ref_alt(union_name, param_names_str, i): + # Get the referenced fixture name + f_fix_name = get_fixture_name(argvalues[i].fixture) + + # Create the alternative + f_fix_alt = FixtureParamAlternative(union_name=union_name, alternative_name=f_fix_name, + param_name=param_names_str, value_index=i) + # Finally copy the custom id/marks on the ParamAlternative if any + if is_marked_parameter_value(marked_argvalues[i]): + f_fix_alt = pytest.param(f_fix_alt, id=marked_argvalues[i].id, marks=marked_argvalues[i].marks) + + return f_fix_name, f_fix_alt + + def _create_fixture_ref_product(union_name, i, fixture_ref_positions, param_names_str, test_func_name, hook): + + # If an explicit list of ids was provided, slice it. Otherwise use the provided callable + try: + p_ids = ids[i] + except TypeError: + p_ids = ids # callable - # now create a unique fixture name - p_fix_name = "%s_%s" % (test_func_name, p_names_with_idx) + # values to use: + p_values = argvalues[i] + + # Create a unique fixture name + p_fix_name = "%s_%s_P%s" % (test_func_name, param_names_str, i) p_fix_name = check_name_available(caller_module, p_fix_name, if_name_exists=CHANGE, caller=parametrize_plus) - param_fix = _param_fixture(caller_module, argname=p_fix_name, argvalues=selected_argvalues, - ids=selected_ids, hook=hook) - return param_fix, p_names_with_idx + # Create the fixture + new_product_fix = _fixture_product(caller_module, name=p_fix_name, fixtures_or_values=p_values, + fixture_positions=fixture_ref_positions, hook=hook, ids=p_ids) - def _create_fixture_product(argvalue_i, fixture_ref_positions, param_names_str, test_func_name, hook): - # do not use base name - we dont care if there is another in the same module, it will still be more readable - p_fix_name = "%s_%s__fixtureproduct__%s" % (test_func_name, param_names_str, argvalue_i) - p_fix_name = check_name_available(caller_module, p_fix_name, if_name_exists=CHANGE, - caller=parametrize_plus) - # unpack the fixture references - _vtuple = argvalues[argvalue_i] - fixtures_or_values = tuple(v.fixture if i in fixture_ref_positions else v for i, v in enumerate(_vtuple)) - product_fix = _fixture_product(caller_module, p_fix_name, fixtures_or_values, fixture_ref_positions, - hook=hook) - return product_fix + # Create the corresponding alternative + p_fix_alt = ProductParamAlternative(union_name=union_name, alternative_name=p_fix_name, + param_name=param_names_str, value_index=i) + # copy the custom id/marks to the ParamAlternative if any + if is_marked_parameter_value(marked_argvalues[i]): + p_fix_alt = pytest.param(p_fix_alt, id=marked_argvalues[i].id, marks=marked_argvalues[i].marks) + + return p_fix_name, p_fix_alt # then create the decorator def parametrize_plus_decorate(test_func): @@ -1258,75 +1487,93 @@ def parametrize_plus_decorate(test_func): """ # first check if the test function has the parameters as arguments old_sig = signature(test_func) - for p in all_param_names: + for p in argnames: if p not in old_sig.parameters: raise ValueError("parameter '%s' not found in test function signature '%s%s'" "" % (p, test_func.__name__, old_sig)) # The name for the final "union" fixture # style_template = "%s_param__%s" - param_names_str = argnames.replace(' ', '').replace(',', '_') - style_template = "%s_%s" - fixture_union_name = style_template % (test_func.__name__, param_names_str) + param_names_str = '_'.join(argnames).replace(' ', '') + main_fixture_style_template = "%s_%s" + fixture_union_name = main_fixture_style_template % (test_func.__name__, param_names_str) fixture_union_name = check_name_available(caller_module, fixture_union_name, if_name_exists=CHANGE, caller=parametrize_plus) # Retrieve (if ref) or create (for normal argvalues) the fixtures that we will union - # TODO important note: we could either wish to create one fixture for parameter value or to create one for - # each consecutive group as shown below. This should not lead to different results but perf might differ. - # maybe add a parameter in the signature so that users can test it ? - fixtures_to_union = [] - fixtures_to_union_names_for_ids = [] + created_alternatives = [] prev_i = -1 for i, j_list in fixture_indices: + # A/ Is there any non-empty group of 'normal' parameters before the fixture_ref at ? If so, handle. if i > prev_i + 1: - # there was a non-empty group of 'normal' parameters before the fixture_ref at . # create a new "param" fixture parametrized with all of that consecutive group. - param_fix, _id_for_fix = _create_param_fixture(prev_i + 1, i, param_names_str, test_func.__name__, - hook=hook) - fixtures_to_union.append(param_fix) - fixtures_to_union_names_for_ids.append(_id_for_fix) - + # TODO important note: we could either wish to create one fixture for parameter value or to create + # one for each consecutive group as shown below. This should not lead to different results but perf + # might differ. Maybe add a parameter in the signature so that users can test it ? + # this would make the ids more readable by removing the "P2toP3"-like ids + p_fix_name, p_fix_alt = _create_params_alt(union_name=fixture_union_name, from_i=prev_i + 1, to_i=i, + param_names_str=param_names_str, + test_func_name=test_func.__name__, hook=hook) + created_alternatives.append((p_fix_name, p_fix_alt)) + + # B/ Now handle the fixture ref at position if j_list is None: - # add the fixture referenced with `fixture_ref` - referenced_fixture = argvalues[i].fixture - fixtures_to_union.append(referenced_fixture) - id_for_fixture = apply_id_style(get_fixture_name(referenced_fixture), param_names_str, IdStyle.explicit) - fixtures_to_union_names_for_ids.append(id_for_fixture) + # argvalues[i] contains a single argvalue that is a fixture_ref : add the referenced fixture + f_fix_name, f_fix_alt = _create_fixture_ref_alt(union_name=fixture_union_name, + param_names_str=param_names_str, i=i) + created_alternatives.append((f_fix_name, f_fix_alt)) + else: - # argvalues[i] is a tuple of argvalues, some of them being fixture_ref. create a fixture refering to all of them - prod_fix = _create_fixture_product(i, j_list, param_names_str, test_func.__name__, - hook=hook) - fixtures_to_union.append(prod_fix) - _id_product = "fixtureproduct__%s" % i - id_for_fixture = apply_id_style(_id_product, param_names_str, IdStyle.explicit) - fixtures_to_union_names_for_ids.append(id_for_fixture) + # argvalues[i] is a tuple, some of them being fixture_ref. create a fixture refering to all of them + prod_fix_name, prod_fix_alt = _create_fixture_ref_product(union_name=fixture_union_name, i=i, + fixture_ref_positions=j_list, + param_names_str=param_names_str, + test_func_name=test_func.__name__, + hook=hook) + created_alternatives.append((prod_fix_name, prod_fix_alt)) + prev_i = i - # handle last consecutive group of normal parameters, if any + # C/ handle last consecutive group of normal parameters, if any i = len(argvalues) if i > prev_i + 1: - param_fix, _id_for_fix = _create_param_fixture(prev_i + 1, i, param_names_str, test_func.__name__, - hook=hook) - fixtures_to_union.append(param_fix) - fixtures_to_union_names_for_ids.append(_id_for_fix) + p_fix_name, p_fix_alt = _create_params_alt(union_name=fixture_union_name, from_i=prev_i + 1, to_i=i, + param_names_str=param_names_str, + test_func_name=test_func.__name__, hook=hook) + created_alternatives.append((p_fix_name, p_fix_alt)) # TO DO if fixtures_to_union has length 1, simplify ? >> No, we leave such "optimization" to the end user + # consolidate the list of alternative fixture names that the new union fixture will switch between + expected_nb = len(set(a[0] for a in fixture_indices)) + if fixture_indices[0][0] > 0: + expected_nb += 1 + if fixture_indices[-1][0] < len(marked_argvalues) - 1: + expected_nb += 1 + assert expected_nb == len(created_alternatives), "Could not create unique fixture names, please report this" + + fix_alternatives = tuple(a[1] for a in created_alternatives) + fix_alt_names = [] + for a, _ in created_alternatives: + if a not in fix_alt_names: + fix_alt_names.append(a) + # Finally create a "main" fixture with a unique name for this test function + # TODO if `ids` were provided, we have to "cut the part where the product params appear + # note: the function automatically registers it in the module - # note 2: idstyle is set to None because we provide an explicit enough list of ids - big_param_fixture = _fixture_union(caller_module, fixture_union_name, fixtures_to_union, idstyle=None, - ids=fixtures_to_union_names_for_ids, hook=hook) + big_param_fixture = _fixture_union(caller_module, name=fixture_union_name, + fix_alternatives=fix_alternatives, unique_fix_alt_names=fix_alt_names, + ids=ids or ParamIdMakers.get(idstyle), hook=hook) # --create the new test function's signature that we want to expose to pytest # it is the same than existing, except that we want to replace all parameters with the new fixture # first check where we should insert the new parameters (where is the first param we remove) for _first_idx, _n in enumerate(old_sig.parameters): - if _n in all_param_names: + if _n in argnames: break # then remove all parameters that will be replaced by the new fixture - new_sig = remove_signature_parameters(old_sig, *all_param_names) + new_sig = remove_signature_parameters(old_sig, *argnames) # finally insert the new fixture in that position. Indeed we can not insert first or last, because # 'self' arg (case of test class methods) should stay first and exec order should be preserved when possible new_sig = add_signature_parameters(new_sig, custom_idx=_first_idx, @@ -1338,10 +1585,10 @@ def replace_paramfixture_with_values(kwargs): encompassing_fixture = kwargs.pop(fixture_union_name) # and add instead the parameter values if nb_params > 1: - for i, p in enumerate(all_param_names): + for i, p in enumerate(argnames): kwargs[p] = encompassing_fixture[i] else: - kwargs[all_param_names[0]] = encompassing_fixture + kwargs[argnames[0]] = encompassing_fixture # return return kwargs diff --git a/pytest_cases/plugin.py b/pytest_cases/plugin.py index 23ece116..fca7972a 100644 --- a/pytest_cases/plugin.py +++ b/pytest_cases/plugin.py @@ -10,7 +10,7 @@ from pytest_cases.common import get_pytest_nodeid, get_pytest_function_scopenum, \ is_function_node, get_param_names, get_pytest_scopenum, get_param_argnames_as_list -from pytest_cases.main_fixtures import NOT_USED, is_fixture_union_params, UnionFixtureAlternative, apply_id_style +from pytest_cases.main_fixtures import NOT_USED, is_fixture_union_params, UnionFixtureAlternative try: # python 3.3+ from inspect import signature @@ -329,6 +329,7 @@ def _build_closure(self, if not fixturedefs: # fixture without definition: add it self.add_required_fixture(fixname, None) + continue else: # the actual definition is the last one _fixdef = fixturedefs[-1] @@ -352,6 +353,7 @@ def _build_closure(self, # empty the pending because all of them have been propagated on all children with their dependencies pending_fixture_names = [] + continue else: # normal fixture @@ -363,6 +365,7 @@ def _build_closure(self, # pending_fixture_names += dependencies # - prepend: makes much more sense pending_fixture_names = list(dependencies) + pending_fixture_names + continue # ------ tools to add new fixture names during closure construction @@ -982,20 +985,11 @@ def _process_node(self, current_node, pending, calls): ids=p_to_apply.ids, scope=p_to_apply.scope, **p_to_apply.kwargs) - # Change the ids by applying the style defined in the corresponding alternative - for callspec in calls: - # TODO right now only the idstyle defined in the first alternative is used. But it cant be - # different from the other ones for now because of the way fixture_union is built. - # Maybe the best would be to remove this and apply the id style when fixture is created. - callspec._idlist[-1] = apply_id_style(callspec._idlist[-1], - p_to_apply.union_fixture_name, - p_to_apply.alternative_names[0].idstyle) - # now move to the children nodes_children = [None] * len(calls) for i in range(len(calls)): active_alternative = calls[i].params[p_to_apply.union_fixture_name] - child_node = current_node.children[active_alternative.fixture_name] + child_node = current_node.children[active_alternative.alternative_name] child_pending = pending.copy() # place the childs parameter in the first position if it is in the list From f89ac47db22d78dd6df3d6b14930e4fff8045869 Mon Sep 17 00:00:00 2001 From: Sylvain MARIE Date: Fri, 29 May 2020 18:02:15 +0200 Subject: [PATCH 03/10] Updated dependencies --- ci_tools/requirements-pip.txt | 1 - setup.py | 5 +++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/ci_tools/requirements-pip.txt b/ci_tools/requirements-pip.txt index 4df1113b..796036fe 100644 --- a/ci_tools/requirements-pip.txt +++ b/ci_tools/requirements-pip.txt @@ -6,7 +6,6 @@ setuptools_scm # -- to install makefun>=1.7.0 decopatch -wrapt # --- to generate the reports (see scripts in ci_tools, called by .travis) pytest-html$PYTEST_HTML_VERSION diff --git a/setup.py b/setup.py index 83b17822..bab66be9 100644 --- a/setup.py +++ b/setup.py @@ -13,8 +13,9 @@ from setuptools_scm import get_version # noqa: E402 # *************** Dependencies ********* -INSTALL_REQUIRES = ['wrapt', 'decopatch', 'makefun>=1.7', 'functools32;python_version<"3.2"', - 'funcsigs;python_version<"3.3"', 'enum34;python_version<"3.4"', 'six'] +INSTALL_REQUIRES = ['decopatch', 'makefun>=1.7', 'six' + 'functools32;python_version<"3.2"', 'funcsigs;python_version<"3.3"', + ] DEPENDENCY_LINKS = [] SETUP_REQUIRES = ['pytest-runner', 'setuptools_scm'] TESTS_REQUIRE = ['pytest', 'pytest-logging', 'pytest-steps', 'pytest-harvest'] From 3b4bcfeea77217a3bdc3d0931c3a9fc6145a732b Mon Sep 17 00:00:00 2001 From: Sylvain MARIE Date: Fri, 29 May 2020 18:02:36 +0200 Subject: [PATCH 04/10] Updated tests --- ...ze_basic.py => test_fixture_ref_basic1.py} | 21 ++++-- ...ametrize.py => test_fixture_ref_basic2.py} | 6 +- ...e.py => test_fixture_ref_basic3_tuples.py} | 0 .../fixtures/test_fixture_ref_basic4_ids.py | 36 ++++++++++ .../fixtures/test_fixture_ref_custom1.py | 72 +++++++++++++++++++ .../fixtures/test_fixture_ref_custom2.py | 39 ++++++++++ .../test_fixture_union_custom_mark.py | 30 ++++++++ .../test_fixtures_paramfixtures_marks.py | 21 ++++++ .../tests/issues/test_issue_pytest_70.py | 4 +- 9 files changed, 219 insertions(+), 10 deletions(-) rename pytest_cases/tests/fixtures/{test_fixture_in_parametrize_basic.py => test_fixture_ref_basic1.py} (81%) rename pytest_cases/tests/fixtures/{test_fixture_in_parametrize.py => test_fixture_ref_basic2.py} (86%) rename pytest_cases/tests/fixtures/{test_fixture_in_parametrize_tuple.py => test_fixture_ref_basic3_tuples.py} (100%) create mode 100644 pytest_cases/tests/fixtures/test_fixture_ref_basic4_ids.py create mode 100644 pytest_cases/tests/fixtures/test_fixture_ref_custom1.py create mode 100644 pytest_cases/tests/fixtures/test_fixture_ref_custom2.py create mode 100644 pytest_cases/tests/fixtures/test_fixture_union_custom_mark.py create mode 100644 pytest_cases/tests/fixtures/test_fixtures_paramfixtures_marks.py diff --git a/pytest_cases/tests/fixtures/test_fixture_in_parametrize_basic.py b/pytest_cases/tests/fixtures/test_fixture_ref_basic1.py similarity index 81% rename from pytest_cases/tests/fixtures/test_fixture_in_parametrize_basic.py rename to pytest_cases/tests/fixtures/test_fixture_ref_basic1.py index f6e5ec0b..1d350b09 100644 --- a/pytest_cases/tests/fixtures/test_fixture_in_parametrize_basic.py +++ b/pytest_cases/tests/fixtures/test_fixture_ref_basic1.py @@ -1,5 +1,5 @@ import pytest -from pytest_cases import pytest_parametrize_plus, pytest_fixture_plus, fixture_ref +from pytest_cases import pytest_parametrize_plus, pytest_fixture_plus, fixture_ref, parametrize_plus from pytest_cases.tests.conftest import global_fixture @@ -22,13 +22,24 @@ def test_prints(main_msg, ending): def test_synthesis(module_results_dct): - assert list(module_results_dct) == ['test_prints[main_msg_is_0-nothing-?]', - 'test_prints[main_msg_is_0-nothing-!]', + assert list(module_results_dct) == ['test_prints[main_msg_is_nothing-?]', + 'test_prints[main_msg_is_nothing-!]', 'test_prints[main_msg_is_world_str-?]', 'test_prints[main_msg_is_world_str-!]', 'test_prints[main_msg_is_greetings-who_is_world_str-?]', 'test_prints[main_msg_is_greetings-who_is_world_str-!]', - 'test_prints[main_msg_is_greetings-who_is_1-you-?]', - 'test_prints[main_msg_is_greetings-who_is_1-you-!]', + 'test_prints[main_msg_is_greetings-who_is_you-?]', + 'test_prints[main_msg_is_greetings-who_is_you-!]', 'test_prints[main_msg_is_global_fixture-?]', 'test_prints[main_msg_is_global_fixture-!]'] + + +@pytest.fixture +def c(): + return 3, 2 + + +@parametrize_plus("a,b", [fixture_ref(c)]) +def test_foo(a, b): + """here the fixture is used for both parameters at the same time""" + assert (a, b) == (3, 2) diff --git a/pytest_cases/tests/fixtures/test_fixture_in_parametrize.py b/pytest_cases/tests/fixtures/test_fixture_ref_basic2.py similarity index 86% rename from pytest_cases/tests/fixtures/test_fixture_in_parametrize.py rename to pytest_cases/tests/fixtures/test_fixture_ref_basic2.py index e2ac53b8..d7e689fa 100644 --- a/pytest_cases/tests/fixtures/test_fixture_in_parametrize.py +++ b/pytest_cases/tests/fixtures/test_fixture_ref_basic2.py @@ -31,8 +31,8 @@ def test_foo(arg, bar): def test_synthesis(module_results_dct): - assert list(module_results_dct) == ['test_foo[arg_is_0-z-bar]', + assert list(module_results_dct) == ['test_foo[arg_is_z-bar]', 'test_foo[arg_is_a-bar]', 'test_foo[arg_is_b-second_letter_is_a-bar]', - 'test_foo[arg_is_b-second_letter_is_1-o-bar]', - 'test_foo[arg_is_3-o-bar]'] + 'test_foo[arg_is_b-second_letter_is_o-bar]', + 'test_foo[arg_is_o-bar]'] diff --git a/pytest_cases/tests/fixtures/test_fixture_in_parametrize_tuple.py b/pytest_cases/tests/fixtures/test_fixture_ref_basic3_tuples.py similarity index 100% rename from pytest_cases/tests/fixtures/test_fixture_in_parametrize_tuple.py rename to pytest_cases/tests/fixtures/test_fixture_ref_basic3_tuples.py diff --git a/pytest_cases/tests/fixtures/test_fixture_ref_basic4_ids.py b/pytest_cases/tests/fixtures/test_fixture_ref_basic4_ids.py new file mode 100644 index 00000000..1e6517c4 --- /dev/null +++ b/pytest_cases/tests/fixtures/test_fixture_ref_basic4_ids.py @@ -0,0 +1,36 @@ +import pytest + +from pytest_cases import parametrize_plus, pytest_fixture_plus, fixture_ref + + +@pytest.fixture +def a(): + return 'A', 'AA' + + +@pytest_fixture_plus +@pytest.mark.parametrize('arg', [1, 2]) +def b(arg): + return "B%s" % arg + + +@parametrize_plus("arg1,arg2", [('1', None), + (None, '2'), + fixture_ref('a'), + ('4', '4'), + ('3', fixture_ref('b')) + ]) +def test_foo(arg1, arg2): + print(arg1, arg2) + + +def test_synthesis(module_results_dct): + """See https://github.com/smarie/python-pytest-cases/issues/86""" + assert list(module_results_dct) == [ + 'test_foo[arg1_arg2_is_P0toP1-1-None]', + 'test_foo[arg1_arg2_is_P0toP1-None-2]', + 'test_foo[arg1_arg2_is_a]', + 'test_foo[arg1_arg2_is_4-4]', + 'test_foo[arg1_arg2_is_P4-1]', + 'test_foo[arg1_arg2_is_P4-2]' + ] diff --git a/pytest_cases/tests/fixtures/test_fixture_ref_custom1.py b/pytest_cases/tests/fixtures/test_fixture_ref_custom1.py new file mode 100644 index 00000000..77cfaf04 --- /dev/null +++ b/pytest_cases/tests/fixtures/test_fixture_ref_custom1.py @@ -0,0 +1,72 @@ +from distutils.version import LooseVersion + +import pytest + +from pytest_harvest import saved_fixture, get_session_synthesis_dct +from pytest_cases import parametrize_plus, fixture_ref, pytest_fixture_plus + + +# pytest.param is not available in all versions +if LooseVersion(pytest.__version__) >= LooseVersion('3.0.0'): + + @pytest.fixture + @saved_fixture + def a(): + return 'a' + + + @pytest_fixture_plus + @saved_fixture + @pytest.mark.parametrize('i', [5, 6]) + def b(i): + return 'b%s' % i + + + @parametrize_plus('arg', [pytest.param('c'), + pytest.param(fixture_ref(a)), + fixture_ref(b)], + hook=saved_fixture) + def test_fixture_ref1(arg): + assert arg in ['a', 'b5', 'b6', 'c'] + + + def test_synthesis1(request, fixture_store): + results_dct1 = get_session_synthesis_dct(request, filter=test_fixture_ref1, test_id_format='function', + fixture_store=fixture_store, flatten=True) + assert [(k, v['test_fixture_ref1_arg']) for k, v in results_dct1.items()] == [ + ('test_fixture_ref1[arg_is_c]', 'c'), + ('test_fixture_ref1[arg_is_a]', 'a'), + ('test_fixture_ref1[arg_is_b-5]', 'b5'), + ('test_fixture_ref1[arg_is_b-6]', 'b6'), + ] + + + # ------------- + + + @pytest.fixture + @saved_fixture + def c(): + return 'c', 'd' + + + @parametrize_plus('foo,bar', [pytest.param(fixture_ref(a), 1), + (2, fixture_ref(b)), + pytest.param(fixture_ref(c)), + fixture_ref(c) + ]) + def test_fixture_ref2(foo, bar): + assert foo in ['a', 2, 'c'] + assert bar in {'a': (1, ), 2: ('b5', 'b6'), 'c': ('d',)}[foo] + + + def test_synthesis2(request, fixture_store): + results_dct2 = get_session_synthesis_dct(request, filter=test_fixture_ref2, test_id_format='function', + fixture_store=fixture_store, flatten=True) + assert list(results_dct2) == [ + 'test_fixture_ref2[foo_bar_is_P0]', + 'test_fixture_ref2[foo_bar_is_P1-5]', + 'test_fixture_ref2[foo_bar_is_P1-6]', + 'test_fixture_ref2[foo_bar_is_c0]', + 'test_fixture_ref2[foo_bar_is_c1]' + ] diff --git a/pytest_cases/tests/fixtures/test_fixture_ref_custom2.py b/pytest_cases/tests/fixtures/test_fixture_ref_custom2.py new file mode 100644 index 00000000..372849ff --- /dev/null +++ b/pytest_cases/tests/fixtures/test_fixture_ref_custom2.py @@ -0,0 +1,39 @@ +from distutils.version import LooseVersion + +import pytest +from pytest_cases import parametrize_plus, fixture_ref + +# pytest.param is not available in all versions +if LooseVersion(pytest.__version__) >= LooseVersion('3.0.0'): + @pytest.fixture + def a(): + return 'a' + + + @pytest.fixture + def b(): + return 'b' + + + @parametrize_plus('arg', [pytest.param("a", marks=pytest.mark.skipif("5>4")), + fixture_ref(b)]) + def test_mark(arg): + assert arg in ['a', 'b'] + + + @parametrize_plus('arg', [pytest.param("a", id="testID"), + fixture_ref(b)]) + def test_id(arg): + assert arg in ['a', 'b'] + + + def test_synthesis(module_results_dct): + # make sure the id was taken into account + assert list(module_results_dct) == [ + 'test_mark[arg_is_a]', + 'test_mark[arg_is_b]', + 'test_id[testID]', + 'test_id[arg_is_b]' + ] + # make sure the mark was taken into account + assert module_results_dct['test_mark[arg_is_a]']['status'] == 'skipped' diff --git a/pytest_cases/tests/fixtures/test_fixture_union_custom_mark.py b/pytest_cases/tests/fixtures/test_fixture_union_custom_mark.py new file mode 100644 index 00000000..2d06b50b --- /dev/null +++ b/pytest_cases/tests/fixtures/test_fixture_union_custom_mark.py @@ -0,0 +1,30 @@ +import pytest + +from pytest_cases import param_fixture, fixture_union + +a = param_fixture("a", [1, + pytest.param(2, id='22'), + pytest.param(3, marks=pytest.mark.skip) + ]) + + +b = param_fixture("b", [3, 4]) + + +c = fixture_union('c', [pytest.param('a', id='A'), + pytest.param(b, marks=pytest.mark.skip) + ], + ids=['ignored', 'B'], + ) + + +def test_foo(c): + pass + + +def test_synthesis(module_results_dct): + # TODO most probably the skip mark on b seeems to mess with the union behaviour. + assert list(module_results_dct) == [ + 'test_foo[A-1]', + 'test_foo[A-22]', + ] diff --git a/pytest_cases/tests/fixtures/test_fixtures_paramfixtures_marks.py b/pytest_cases/tests/fixtures/test_fixtures_paramfixtures_marks.py new file mode 100644 index 00000000..369b69c6 --- /dev/null +++ b/pytest_cases/tests/fixtures/test_fixtures_paramfixtures_marks.py @@ -0,0 +1,21 @@ +from distutils.version import LooseVersion + +import pytest + +from pytest_cases import param_fixture + +# pytest.param - not available in all versions +if LooseVersion(pytest.__version__) >= LooseVersion('3.0.0'): + a = param_fixture("a", [1, + pytest.param(2, id='22'), + pytest.param(3, marks=pytest.mark.skip) + ]) + + + def test_foo(a): + pass + + + def test_synthesis(module_results_dct): + # id taken into account as well as skip mark (module_results_dct filters on non-skipped) + assert list(module_results_dct) == ['test_foo[1]', 'test_foo[22]'] diff --git a/pytest_cases/tests/issues/test_issue_pytest_70.py b/pytest_cases/tests/issues/test_issue_pytest_70.py index 39aa4092..55bf360c 100644 --- a/pytest_cases/tests/issues/test_issue_pytest_70.py +++ b/pytest_cases/tests/issues/test_issue_pytest_70.py @@ -26,6 +26,6 @@ def test_get_or_create_book(name): def test_synthesis(module_results_dct): assert list(module_results_dct) == ['test_get_or_create_book[name_is_book1-A]', 'test_get_or_create_book[name_is_book1-B]', - 'test_get_or_create_book[name_is_1to2-hi]', - 'test_get_or_create_book[name_is_1to2-ih]', + 'test_get_or_create_book[name_is_P1toP2-hi]', + 'test_get_or_create_book[name_is_P1toP2-ih]', 'test_get_or_create_book[name_is_book2]'] From f7f12b4ac4b5f8512ed5eb3704c8d247228f94bf Mon Sep 17 00:00:00 2001 From: Sylvain MARIE Date: Sat, 30 May 2020 15:38:45 +0200 Subject: [PATCH 05/10] Removed `six` dependency --- pytest_cases/common.py | 2 +- pytest_cases/main_fixtures.py | 2 +- pytest_cases/main_params.py | 2 +- pytest_cases/mini_six.py | 25 +++++++++++++++++++++++++ pytest_cases/plugin.py | 2 +- 5 files changed, 29 insertions(+), 4 deletions(-) create mode 100644 pytest_cases/mini_six.py diff --git a/pytest_cases/common.py b/pytest_cases/common.py index 76cf0bca..33936074 100644 --- a/pytest_cases/common.py +++ b/pytest_cases/common.py @@ -11,7 +11,7 @@ from distutils.version import LooseVersion from warnings import warn -from six import string_types +from .mini_six import string_types import pytest diff --git a/pytest_cases/main_fixtures.py b/pytest_cases/main_fixtures.py index cdd97ef4..8a785eda 100644 --- a/pytest_cases/main_fixtures.py +++ b/pytest_cases/main_fixtures.py @@ -9,7 +9,7 @@ from decopatch import function_decorator, DECORATED from makefun import with_signature, add_signature_parameters, remove_signature_parameters, wraps -from six import string_types +from .mini_six import string_types import pytest try: # python 3.3+ diff --git a/pytest_cases/main_params.py b/pytest_cases/main_params.py index 111d8d58..b1b4f7eb 100644 --- a/pytest_cases/main_params.py +++ b/pytest_cases/main_params.py @@ -8,7 +8,7 @@ from decopatch import function_decorator, DECORATED, with_parenthesis -from six import with_metaclass, string_types +from .mini_six import with_metaclass, string_types import pytest try: # python 3.3+ diff --git a/pytest_cases/mini_six.py b/pytest_cases/mini_six.py new file mode 100644 index 00000000..09049345 --- /dev/null +++ b/pytest_cases/mini_six.py @@ -0,0 +1,25 @@ +import sys + +PY3 = sys.version_info[0] == 3 +PY34 = sys.version_info[0:2] >= (3, 4) + +if PY3: + string_types = str, +else: + string_types = basestring, + + +def with_metaclass(meta, *bases): + """Create a base class with a metaclass.""" + # This requires a bit of explanation: the basic idea is to make a dummy + # metaclass for one level of class instantiation that replaces itself with + # the actual metaclass. + class metaclass(type): + + def __new__(cls, name, this_bases, d): + return meta(name, bases, d) + + @classmethod + def __prepare__(cls, name, this_bases): + return meta.__prepare__(name, bases) + return type.__new__(metaclass, 'temporary_class', (), {}) diff --git a/pytest_cases/plugin.py b/pytest_cases/plugin.py index fca7972a..c8a69194 100644 --- a/pytest_cases/plugin.py +++ b/pytest_cases/plugin.py @@ -4,7 +4,7 @@ from warnings import warn from functools import partial -from six import string_types +from .mini_six import string_types import pytest From 13765deac2588459c4debf07074a3ffede6d0c88 Mon Sep 17 00:00:00 2001 From: Sylvain MARIE Date: Sat, 30 May 2020 15:39:19 +0200 Subject: [PATCH 06/10] removed `six` from setup --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index bab66be9..612d6d05 100644 --- a/setup.py +++ b/setup.py @@ -13,7 +13,7 @@ from setuptools_scm import get_version # noqa: E402 # *************** Dependencies ********* -INSTALL_REQUIRES = ['decopatch', 'makefun>=1.7', 'six' +INSTALL_REQUIRES = ['decopatch', 'makefun>=1.7', 'functools32;python_version<"3.2"', 'funcsigs;python_version<"3.3"', ] DEPENDENCY_LINKS = [] From 89cd5b164f7eae6ed9ab28bd74f68f0dbe0fcc67 Mon Sep 17 00:00:00 2001 From: Sylvain MARIE Date: Sun, 31 May 2020 16:10:43 +0200 Subject: [PATCH 07/10] New `mini_idval` and `mini_idvalset` methods to ensure consistency of created ids. Also refactored a bit the `xxxAlternative` classes for consistency with pytest variable names. Updated corresponding tests --- pytest_cases/common.py | 116 +++++++- pytest_cases/main_fixtures.py | 254 ++++++++++-------- .../test_fixture_ref_basic3_tuples.py | 22 +- .../fixtures/test_fixture_ref_custom2.py | 3 - .../tests/fixtures/test_fixture_union_ids.py | 17 +- 5 files changed, 274 insertions(+), 138 deletions(-) diff --git a/pytest_cases/common.py b/pytest_cases/common.py index 33936074..118eeb55 100644 --- a/pytest_cases/common.py +++ b/pytest_cases/common.py @@ -4,7 +4,7 @@ from funcsigs import signature try: - from typing import Union, Callable, Any + from typing import Union, Callable, Any, Optional except ImportError: pass @@ -332,15 +332,17 @@ def make_test_ids_from_param_values(param_names, raise ValueError("empty list provided") elif nb_params == 1: paramids = [] - for v in param_values: - paramids.append(str(v)) + for _idx, v in enumerate(param_values): + _id = mini_idvalset(param_names, (v,), _idx) + paramids.append(_id) else: paramids = [] - for vv in param_values: + for _idx, vv in enumerate(param_values): if len(vv) != nb_params: raise ValueError("Inconsistent lenghts for parameter names and values: '%s' and '%s'" "" % (param_names, vv)) - paramids.append('-'.join([str(v) for v in vv])) + _id = mini_idvalset(param_names, vv, _idx) + paramids.append(_id) return paramids @@ -535,3 +537,107 @@ def get_pytest_scopenum(scope_str): def get_pytest_function_scopenum(): return pt_scopes.index("function") + + +try: + from _pytest.python import _idval +except ImportError: + try: + from _pytest.config import Config + except ImportError: + pass + + import re + try: + from enum import Enum + hasenum = True + except ImportError: + hasenum = False + + from _pytest.compat import ascii_escaped, STRING_TYPES, REGEX_TYPE + + + def _ascii_escaped_by_config(val, # type: Union[str, bytes] + config # type: Optional[Config] + ): + # type: (...) -> str + if config is None: + escape_option = False + else: + escape_option = config.getini( + "disable_test_id_escaping_and_forfeit_all_rights_to_community_support" + ) + # TODO: If escaping is turned off and the user passes bytes, + # will return a bytes. For now we ignore this but the + # code *probably* doesn't handle this case. + return val if escape_option else ascii_escaped(val) # type: ignore + + def _idval( + val, # type: object + argname, # type: str + idx, # type: int + idfn, # type: Optional[Callable[[object], Optional[object]]] + item, + config, # type: Optional[Config] + ): + # type: (...) -> str + if idfn: + try: + generated_id = idfn(val) + if generated_id is not None: + val = generated_id + except Exception as e: + msg = "{}: error raised while trying to determine id of parameter '{}' at position {}" + msg = msg.format(item.nodeid, argname, idx) + raise ValueError(msg) # from e + elif config: + hook_id = config.hook.pytest_make_parametrize_id( + config=config, val=val, argname=argname + ) # type: Optional[str] + if hook_id: + return hook_id + + if isinstance(val, STRING_TYPES): + return _ascii_escaped_by_config(val, config) + elif val is None or isinstance(val, (float, int, bool)): + return str(val) + elif isinstance(val, REGEX_TYPE): + return ascii_escaped(val.pattern) + elif hasenum and isinstance(val, Enum): + return str(val) + elif isinstance(getattr(val, "__name__", None), str): + # name of a class, function, module, etc. + name = getattr(val, "__name__") # type: str + return name + return str(argname) + str(idx) + + +def mini_idval( + val, # type: object + argname, # type: str + idx, # type: int + ): + """ + A simplified version of idval + + :param val: + :param argname: + :param idx: + :return: + """ + return _idval(val=val, argname=argname, idx=idx, + idfn=None, item=None, # item is only used by idfn + config=None # if a config hook was available it would be used before this is called + ) + + +def mini_idvalset(argnames, argvalues, idx): + """ mimic _pytest.python._idvalset """ + this_id = [ + _idval(val, argname, idx=idx, + idfn=None, item=None, # item is only used by idfn + config=None # if a config hook was available it would be used before this is called + ) + for val, argname in zip(argvalues, argnames) + ] + return "-".join(this_id) diff --git a/pytest_cases/main_fixtures.py b/pytest_cases/main_fixtures.py index 8a785eda..19c9ffe6 100644 --- a/pytest_cases/main_fixtures.py +++ b/pytest_cases/main_fixtures.py @@ -9,7 +9,6 @@ from decopatch import function_decorator, DECORATED from makefun import with_signature, add_signature_parameters, remove_signature_parameters, wraps -from .mini_six import string_types import pytest try: # python 3.3+ @@ -40,8 +39,9 @@ from pytest_cases.common import yield_fixture, get_pytest_parametrize_marks, make_marked_parameter_value, \ get_fixture_name, get_param_argnames_as_list, analyze_parameter_set, combine_ids, get_fixture_scope, \ - remove_duplicates, extract_parameterset_info, is_marked_parameter_value, get_marked_parameter_values + remove_duplicates, extract_parameterset_info, is_marked_parameter_value, get_marked_parameter_values, mini_idvalset from pytest_cases.main_params import cases_data +from pytest_cases.mini_six import string_types def unpack_fixture(argnames, @@ -1170,123 +1170,133 @@ def pytest_parametrize_plus(*args, return _parametrize_plus(*args, **kwargs) -class ParamIdMakers(object): - # @staticmethod - # def nostyle(param): - # return param.alternative_name - - @staticmethod - def explicit(param # type: ParamAlternative - ): - if isinstance(param, SingleParamAlternative): - # return "%s_is_P%s" % (param.param_name, param.value_index) - return "%s_is_%s" % (param.param_name, param.value) - elif isinstance(param, MultiParamAlternative): - return "%s_is_P%stoP%s" % (param.param_name, param.value_index_from, param.value_index_to - 1) - elif isinstance(param, FixtureParamAlternative): - return "%s_is_%s" % (param.param_name, param.alternative_name) - elif isinstance(param, ProductParamAlternative): - return "%s_is_P%s" % (param.param_name, param.value_index) - else: - raise TypeError("Unsupported alternative: %r" % param) - - # @staticmethod - # def compact(param): - # return "U%s" % param.alternative_name - - @classmethod - def get(cls, style # type: str - ): - # type: (...) -> Callable[[Any], str] - """ - Returns a function that one can use as the `ids` argument in parametrize, applying the given id style. - See https://github.com/smarie/python-pytest-cases/issues/41 - - :param idstyle: - :return: - """ - style = style or 'nostyle' - try: - return getattr(cls, style) - except AttributeError: - raise ValueError("Unknown style: %r" % style) - - class ParamAlternative(UnionFixtureAlternative): """Defines an "alternative", used to parametrize a fixture union in the context of parametrize_plus""" - __slots__ = ('param_name', ) + __slots__ = ('argnames', ) def __init__(self, union_name, alternative_name, - param_name, + argnames, ): super(ParamAlternative, self).__init__(union_name=union_name, alternative_name=alternative_name) - self.param_name = param_name + self.argnames = argnames + + @property + def argnames_str(self): + return '_'.join(self.argnames) class SingleParamAlternative(ParamAlternative): """alternative class for single parameter value""" - __slots__ = 'value_index', 'value' + __slots__ = 'argvalues_index', 'argvalues' def __init__(self, union_name, alternative_name, - param_name, - value_index, - value + argnames, + argvalues_index, + argvalues ): super(SingleParamAlternative, self).__init__(union_name=union_name, alternative_name=alternative_name, - param_name=param_name) - self.value_index = value_index - self.value = value + argnames=argnames) + self.argvalues_index = argvalues_index + self.argvalues = argvalues + + def get_id(self): + # return "-".join(self.argvalues) + return mini_idvalset(self.argnames, self.argvalues, idx=self.argvalues_index) class MultiParamAlternative(ParamAlternative): """alternative class for multiple parameter values""" - __slots__ = 'value_index_from', 'value_index_to' + __slots__ = 'argvalues_index_from', 'argvalues_index_to' def __init__(self, union_name, alternative_name, - param_name, - value_index_from, - value_index_to + argnames, + argvalues_index_from, + argvalues_index_to ): super(MultiParamAlternative, self).__init__(union_name=union_name, alternative_name=alternative_name, - param_name=param_name) - self.value_index_from = value_index_from - self.value_index_to = value_index_to + argnames=argnames) + self.argvalues_index_from = argvalues_index_from + self.argvalues_index_to = argvalues_index_to class FixtureParamAlternative(ParamAlternative): """alternative class for a single parameter containing a fixture ref""" - __slots__ = 'value_index', + __slots__ = 'argvalues_index', def __init__(self, union_name, alternative_name, - param_name, - value_index, + argnames, + argvalues_index, ): super(FixtureParamAlternative, self).__init__(union_name=union_name, alternative_name=alternative_name, - param_name=param_name) - self.value_index = value_index + argnames=argnames) + self.argvalues_index = argvalues_index class ProductParamAlternative(ParamAlternative): """alternative class for a single product parameter containing fixture refs""" - __slots__ = 'value_index' + __slots__ = 'argvalues_index' def __init__(self, union_name, alternative_name, - param_name, - value_index, + argnames, + argvalues_index, ): super(ProductParamAlternative, self).__init__(union_name=union_name, alternative_name=alternative_name, - param_name=param_name) - self.value_index = value_index + argnames=argnames) + self.argvalues_index = argvalues_index + + +class ParamIdMakers(object): + """ 'Enum' of id styles for param ids """ + + # @staticmethod + # def nostyle(param): + # return param.alternative_name + + @staticmethod + def explicit(param # type: ParamAlternative + ): + if isinstance(param, SingleParamAlternative): + # return "%s_is_P%s" % (param.param_name, param.argvalues_index) + return "%s_is_%s" % (param.argnames_str, param.get_id()) + elif isinstance(param, MultiParamAlternative): + return "%s_is_P%stoP%s" % (param.argnames_str, param.argvalues_index_from, param.argvalues_index_to - 1) + elif isinstance(param, FixtureParamAlternative): + return "%s_is_%s" % (param.argnames_str, param.alternative_name) + elif isinstance(param, ProductParamAlternative): + return "%s_is_P%s" % (param.argnames_str, param.argvalues_index) + else: + raise TypeError("Unsupported alternative: %r" % param) + + # @staticmethod + # def compact(param): + # return "U%s" % param.alternative_name + + @classmethod + def get(cls, style # type: str + ): + # type: (...) -> Callable[[Any], str] + """ + Returns a function that one can use as the `ids` argument in parametrize, applying the given id style. + See https://github.com/smarie/python-pytest-cases/issues/41 + + :param idstyle: + :return: + """ + style = style or 'nostyle' + try: + return getattr(cls, style) + except AttributeError: + raise ValueError("Unknown style: %r" % style) def parametrize_plus(argnames, @@ -1384,8 +1394,32 @@ def _parametrize_plus(argnames, else: # there are fixture references: we have to create a specific decorator caller_module = get_caller_module(frame_offset=_frame_offset) + param_names_str = '_'.join(argnames).replace(' ', '') + + def _make_idfun_for_params(argnames, nb_positions): + """ + Creates an id creating function that will use 'argnames' as the argnames + instead of the one(s) received by pytest. We use this in the case of param fixture + creation because on one side we need a unique fixture name so it is big and horrible, + but on the other side we want the id to rather reflect the simple argnames, no that fixture name. - def _create_params_alt(union_name, from_i, to_i, param_names_str, test_func_name, hook): + :param argnames: + :param nb_positions: + :return: + """ + # create a new make id function with its own local counter of parameter + def _tmp_make_id(argvalues): + _tmp_make_id._i += 1 + if _tmp_make_id._i >= nb_positions: + raise ValueError("Internal error, please report") + argvalues = argvalues if len(argnames) > 1 else (argvalues,) + return mini_idvalset(argnames, argvalues, idx=_tmp_make_id._i) + + # init its positions counter + _tmp_make_id._i = -1 + return _tmp_make_id + + def _create_params_alt(test_func_name, union_name, from_i, to_i, hook): """ Routine that will be used to create a parameter fixture for argvalues between prev_i and i""" single_param = (to_i == from_i + 1) @@ -1399,12 +1433,13 @@ def _create_params_alt(union_name, from_i, to_i, param_names_str, test_func_name caller=parametrize_plus) # Create the fixture that will return the unique parameter value ("auto-simplify" flag) # IMPORTANT that fixture is NOT parametrized so has no id nor marks: use argvalues not marked_argvalues - _create_param_fixture(caller_module, argname=p_fix_name, argvalues=argvalues[i], hook=hook, + _create_param_fixture(caller_module, argname=p_fix_name, argvalues=argvalues[i:i+1], hook=hook, auto_simplify=True) # Create the alternative + argvals = (argvalues[i],) if nb_params == 1 else argvalues[i] p_fix_alt = SingleParamAlternative(union_name=union_name, alternative_name=p_fix_name, - param_name=param_names_str, value_index=i, value=argvalues[i]) + argnames=argnames, argvalues_index=i, argvalues=argvals) # Finally copy the custom id/marks on the ParamAlternative if any if is_marked_parameter_value(marked_argvalues[i]): p_fix_alt = pytest.param(p_fix_alt, id=marked_argvalues[i].id, marks=marked_argvalues[i].marks) @@ -1419,7 +1454,9 @@ def _create_params_alt(union_name, from_i, to_i, param_names_str, test_func_name try: p_ids = ids[from_i:to_i] except TypeError: - p_ids = ids # callable + # callable ? otherwise default to a customized id maker that replaces the fixture name + # that we use (p_fix_name) with a simpler name in the ids (just the argnames) + p_ids = ids or _make_idfun_for_params(argnames=argnames, nb_positions=(to_i - from_i)) # Create the fixture that will take all these parameter values # That fixture WILL be parametrized, this is why we propagate the p_ids and use the marked values @@ -1427,27 +1464,26 @@ def _create_params_alt(union_name, from_i, to_i, param_names_str, test_func_name ids=p_ids, hook=hook) # Create the corresponding alternative - p_fix_alt = MultiParamAlternative(union_name=union_name, alternative_name=p_fix_name, - param_name=param_names_str, - value_index_from=from_i, value_index_to=to_i) + p_fix_alt = MultiParamAlternative(union_name=union_name, alternative_name=p_fix_name, argnames=argnames, + argvalues_index_from=from_i, argvalues_index_to=to_i) # no need to copy the custom id/marks to the ParamAlternative: they were passed above already return p_fix_name, p_fix_alt - def _create_fixture_ref_alt(union_name, param_names_str, i): + def _create_fixture_ref_alt(union_name, i): # Get the referenced fixture name f_fix_name = get_fixture_name(argvalues[i].fixture) # Create the alternative f_fix_alt = FixtureParamAlternative(union_name=union_name, alternative_name=f_fix_name, - param_name=param_names_str, value_index=i) + argnames=argnames, argvalues_index=i) # Finally copy the custom id/marks on the ParamAlternative if any if is_marked_parameter_value(marked_argvalues[i]): f_fix_alt = pytest.param(f_fix_alt, id=marked_argvalues[i].id, marks=marked_argvalues[i].marks) return f_fix_name, f_fix_alt - def _create_fixture_ref_product(union_name, i, fixture_ref_positions, param_names_str, test_func_name, hook): + def _create_fixture_ref_product(union_name, i, fixture_ref_positions, test_func_name, hook): # If an explicit list of ids was provided, slice it. Otherwise use the provided callable try: @@ -1469,7 +1505,7 @@ def _create_fixture_ref_product(union_name, i, fixture_ref_positions, param_name # Create the corresponding alternative p_fix_alt = ProductParamAlternative(union_name=union_name, alternative_name=p_fix_name, - param_name=param_names_str, value_index=i) + argnames=argnames, argvalues_index=i) # copy the custom id/marks to the ParamAlternative if any if is_marked_parameter_value(marked_argvalues[i]): p_fix_alt = pytest.param(p_fix_alt, id=marked_argvalues[i].id, marks=marked_argvalues[i].marks) @@ -1485,23 +1521,24 @@ def parametrize_plus_decorate(test_func): :param test_func: :return: """ + test_func_name = test_func.__name__ + # first check if the test function has the parameters as arguments old_sig = signature(test_func) for p in argnames: if p not in old_sig.parameters: raise ValueError("parameter '%s' not found in test function signature '%s%s'" - "" % (p, test_func.__name__, old_sig)) + "" % (p, test_func_name, old_sig)) # The name for the final "union" fixture # style_template = "%s_param__%s" - param_names_str = '_'.join(argnames).replace(' ', '') main_fixture_style_template = "%s_%s" - fixture_union_name = main_fixture_style_template % (test_func.__name__, param_names_str) + fixture_union_name = main_fixture_style_template % (test_func_name, param_names_str) fixture_union_name = check_name_available(caller_module, fixture_union_name, if_name_exists=CHANGE, caller=parametrize_plus) # Retrieve (if ref) or create (for normal argvalues) the fixtures that we will union - created_alternatives = [] + fixture_alternatives = [] prev_i = -1 for i, j_list in fixture_indices: # A/ Is there any non-empty group of 'normal' parameters before the fixture_ref at ? If so, handle. @@ -1511,52 +1548,46 @@ def parametrize_plus_decorate(test_func): # one for each consecutive group as shown below. This should not lead to different results but perf # might differ. Maybe add a parameter in the signature so that users can test it ? # this would make the ids more readable by removing the "P2toP3"-like ids - p_fix_name, p_fix_alt = _create_params_alt(union_name=fixture_union_name, from_i=prev_i + 1, to_i=i, - param_names_str=param_names_str, - test_func_name=test_func.__name__, hook=hook) - created_alternatives.append((p_fix_name, p_fix_alt)) + p_fix_name, p_fix_alt = _create_params_alt(test_func_name=test_func_name, hook=hook, + union_name=fixture_union_name, from_i=prev_i + 1, to_i=i) + fixture_alternatives.append((p_fix_name, p_fix_alt)) # B/ Now handle the fixture ref at position if j_list is None: # argvalues[i] contains a single argvalue that is a fixture_ref : add the referenced fixture - f_fix_name, f_fix_alt = _create_fixture_ref_alt(union_name=fixture_union_name, - param_names_str=param_names_str, i=i) - created_alternatives.append((f_fix_name, f_fix_alt)) + f_fix_name, f_fix_alt = _create_fixture_ref_alt(union_name=fixture_union_name, i=i) + fixture_alternatives.append((f_fix_name, f_fix_alt)) else: # argvalues[i] is a tuple, some of them being fixture_ref. create a fixture refering to all of them prod_fix_name, prod_fix_alt = _create_fixture_ref_product(union_name=fixture_union_name, i=i, fixture_ref_positions=j_list, - param_names_str=param_names_str, - test_func_name=test_func.__name__, - hook=hook) - created_alternatives.append((prod_fix_name, prod_fix_alt)) + test_func_name=test_func_name, hook=hook) + fixture_alternatives.append((prod_fix_name, prod_fix_alt)) prev_i = i # C/ handle last consecutive group of normal parameters, if any i = len(argvalues) if i > prev_i + 1: - p_fix_name, p_fix_alt = _create_params_alt(union_name=fixture_union_name, from_i=prev_i + 1, to_i=i, - param_names_str=param_names_str, - test_func_name=test_func.__name__, hook=hook) - created_alternatives.append((p_fix_name, p_fix_alt)) + p_fix_name, p_fix_alt = _create_params_alt(test_func_name=test_func_name, union_name=fixture_union_name, + from_i=prev_i + 1, to_i=i, hook=hook) + fixture_alternatives.append((p_fix_name, p_fix_alt)) # TO DO if fixtures_to_union has length 1, simplify ? >> No, we leave such "optimization" to the end user - # consolidate the list of alternative fixture names that the new union fixture will switch between - expected_nb = len(set(a[0] for a in fixture_indices)) - if fixture_indices[0][0] > 0: - expected_nb += 1 - if fixture_indices[-1][0] < len(marked_argvalues) - 1: - expected_nb += 1 - assert expected_nb == len(created_alternatives), "Could not create unique fixture names, please report this" + # consolidate the list of alternatives + fix_alternatives = tuple(a[1] for a in fixture_alternatives) - fix_alternatives = tuple(a[1] for a in created_alternatives) + # and the list of their names. Duplicates should be removed here fix_alt_names = [] - for a, _ in created_alternatives: + for a, alt in fixture_alternatives: if a not in fix_alt_names: fix_alt_names.append(a) + else: + # this should only happen when the alternative is directly a fixture reference + assert isinstance(alt, FixtureParamAlternative), \ + "Created fixture names are not unique, please report" # Finally create a "main" fixture with a unique name for this test function # TODO if `ids` were provided, we have to "cut the part where the product params appear @@ -1577,7 +1608,8 @@ def parametrize_plus_decorate(test_func): # finally insert the new fixture in that position. Indeed we can not insert first or last, because # 'self' arg (case of test class methods) should stay first and exec order should be preserved when possible new_sig = add_signature_parameters(new_sig, custom_idx=_first_idx, - custom=Parameter(fixture_union_name, kind=Parameter.POSITIONAL_OR_KEYWORD)) + custom=Parameter(fixture_union_name, + kind=Parameter.POSITIONAL_OR_KEYWORD)) # --Finally create the fixture function, a wrapper of user-provided fixture with the new signature def replace_paramfixture_with_values(kwargs): diff --git a/pytest_cases/tests/fixtures/test_fixture_ref_basic3_tuples.py b/pytest_cases/tests/fixtures/test_fixture_ref_basic3_tuples.py index 90b7df5f..cf8ce47a 100644 --- a/pytest_cases/tests/fixtures/test_fixture_ref_basic3_tuples.py +++ b/pytest_cases/tests/fixtures/test_fixture_ref_basic3_tuples.py @@ -31,15 +31,15 @@ def test_prints(p, q): def test_synthesis(module_results_dct): - assert list(module_results_dct) == ['test_prints[p_q_is_0-a-1]', - 'test_prints[p_q_is_fixtureproduct__1-b]', - 'test_prints[p_q_is_fixtureproduct__1-c]', - 'test_prints[p_q_is_fixtureproduct__2-b-0]', - 'test_prints[p_q_is_fixtureproduct__2-b--1]', - 'test_prints[p_q_is_fixtureproduct__2-c-0]', - 'test_prints[p_q_is_fixtureproduct__2-c--1]', - 'test_prints[p_q_is_fixtureproduct__3-b]', - 'test_prints[p_q_is_fixtureproduct__3-c]', - "test_prints[p_q_is_my_tuple-('d', 3)]", - "test_prints[p_q_is_my_tuple-('e', 4)]" + assert list(module_results_dct) == ['test_prints[p_q_is_a-1]', + 'test_prints[p_q_is_P1-b]', + 'test_prints[p_q_is_P1-c]', + 'test_prints[p_q_is_P2-b-0]', + 'test_prints[p_q_is_P2-b--1]', + 'test_prints[p_q_is_P2-c-0]', + 'test_prints[p_q_is_P2-c--1]', + 'test_prints[p_q_is_P3-b]', + 'test_prints[p_q_is_P3-c]', + "test_prints[p_q_is_my_tuple-val0]", + "test_prints[p_q_is_my_tuple-val1]" ] diff --git a/pytest_cases/tests/fixtures/test_fixture_ref_custom2.py b/pytest_cases/tests/fixtures/test_fixture_ref_custom2.py index 372849ff..83a097c0 100644 --- a/pytest_cases/tests/fixtures/test_fixture_ref_custom2.py +++ b/pytest_cases/tests/fixtures/test_fixture_ref_custom2.py @@ -30,10 +30,7 @@ def test_id(arg): def test_synthesis(module_results_dct): # make sure the id was taken into account assert list(module_results_dct) == [ - 'test_mark[arg_is_a]', 'test_mark[arg_is_b]', 'test_id[testID]', 'test_id[arg_is_b]' ] - # make sure the mark was taken into account - assert module_results_dct['test_mark[arg_is_a]']['status'] == 'skipped' diff --git a/pytest_cases/tests/fixtures/test_fixture_union_ids.py b/pytest_cases/tests/fixtures/test_fixture_union_ids.py index 32d9bb3d..64a4d502 100644 --- a/pytest_cases/tests/fixtures/test_fixture_union_ids.py +++ b/pytest_cases/tests/fixtures/test_fixture_union_ids.py @@ -3,20 +3,21 @@ a = param_fixture("a", [1, 2]) b = param_fixture("b", [3, 4]) -c = fixture_union('c', ['a', b], ids=['A', 'B'], idstyle='explicit') +c = fixture_union('c', ['a', b], ids=['c=A', 'c=B']) d = fixture_union('d', ['a'], idstyle='compact') e = fixture_union('e', ['a'], idstyle=None) +f = fixture_union('f', ['a']) -def test_the_ids(c, d, e): +def test_the_ids(c, d, e, f): pass def test_synthesis(module_results_dct): - assert list(module_results_dct) == ['test_the_ids[c_is_A-1-Ua-a]', - 'test_the_ids[c_is_A-2-Ua-a]', - 'test_the_ids[c_is_B-3-Ua-1-a]', - 'test_the_ids[c_is_B-3-Ua-2-a]', - 'test_the_ids[c_is_B-4-Ua-1-a]', - 'test_the_ids[c_is_B-4-Ua-2-a]', + assert list(module_results_dct) == ['test_the_ids[c=A-1-Ua-a-f_is_a]', + 'test_the_ids[c=A-2-Ua-a-f_is_a]', + 'test_the_ids[c=B-3-Ua-1-a-f_is_a]', + 'test_the_ids[c=B-3-Ua-2-a-f_is_a]', + 'test_the_ids[c=B-4-Ua-1-a-f_is_a]', + 'test_the_ids[c=B-4-Ua-2-a-f_is_a]', ] From ea464a172f75c2dc6d446179edb3b84fb2a12559 Mon Sep 17 00:00:00 2001 From: Sylvain MARIE Date: Sun, 31 May 2020 17:03:00 +0200 Subject: [PATCH 08/10] Last fix for explicit list of ids. --- pytest_cases/common.py | 98 ++++--------------- pytest_cases/main_fixtures.py | 28 +++++- .../fixtures/test_fixture_ref_custom2.py | 2 +- .../fixtures/test_fixture_ref_custom3.py | 39 ++++++++ .../test_fixture_union_custom_mark.py | 40 ++++---- .../tests/issues/test_issue_fixture_union1.py | 2 +- 6 files changed, 106 insertions(+), 103 deletions(-) create mode 100644 pytest_cases/tests/fixtures/test_fixture_ref_custom3.py diff --git a/pytest_cases/common.py b/pytest_cases/common.py index 118eeb55..51598805 100644 --- a/pytest_cases/common.py +++ b/pytest_cases/common.py @@ -539,105 +539,41 @@ def get_pytest_function_scopenum(): return pt_scopes.index("function") -try: - from _pytest.python import _idval -except ImportError: - try: - from _pytest.config import Config - except ImportError: - pass - - import re - try: - from enum import Enum - hasenum = True - except ImportError: - hasenum = False +from _pytest.python import _idval - from _pytest.compat import ascii_escaped, STRING_TYPES, REGEX_TYPE - - def _ascii_escaped_by_config(val, # type: Union[str, bytes] - config # type: Optional[Config] - ): - # type: (...) -> str - if config is None: - escape_option = False - else: - escape_option = config.getini( - "disable_test_id_escaping_and_forfeit_all_rights_to_community_support" - ) - # TODO: If escaping is turned off and the user passes bytes, - # will return a bytes. For now we ignore this but the - # code *probably* doesn't handle this case. - return val if escape_option else ascii_escaped(val) # type: ignore - - def _idval( - val, # type: object - argname, # type: str - idx, # type: int - idfn, # type: Optional[Callable[[object], Optional[object]]] - item, - config, # type: Optional[Config] - ): - # type: (...) -> str - if idfn: - try: - generated_id = idfn(val) - if generated_id is not None: - val = generated_id - except Exception as e: - msg = "{}: error raised while trying to determine id of parameter '{}' at position {}" - msg = msg.format(item.nodeid, argname, idx) - raise ValueError(msg) # from e - elif config: - hook_id = config.hook.pytest_make_parametrize_id( - config=config, val=val, argname=argname - ) # type: Optional[str] - if hook_id: - return hook_id - - if isinstance(val, STRING_TYPES): - return _ascii_escaped_by_config(val, config) - elif val is None or isinstance(val, (float, int, bool)): - return str(val) - elif isinstance(val, REGEX_TYPE): - return ascii_escaped(val.pattern) - elif hasenum and isinstance(val, Enum): - return str(val) - elif isinstance(getattr(val, "__name__", None), str): - # name of a class, function, module, etc. - name = getattr(val, "__name__") # type: str - return name - return str(argname) + str(idx) +if LooseVersion(pytest.__version__) >= LooseVersion('3.0.0'): + _idval_kwargs = dict(idfn=None, + item=None, # item is only used by idfn + config=None # if a config hook was available it would be used before this is called) + ) +else: + _idval_kwargs = dict(idfn=None, + # item=None, # item is only used by idfn + # config=None # if a config hook was available it would be used before this is called) + ) def mini_idval( - val, # type: object - argname, # type: str - idx, # type: int + val, # type: object + argname, # type: str + idx, # type: int ): """ - A simplified version of idval + A simplified version of idval where idfn, item and config do not need to be passed. :param val: :param argname: :param idx: :return: """ - return _idval(val=val, argname=argname, idx=idx, - idfn=None, item=None, # item is only used by idfn - config=None # if a config hook was available it would be used before this is called - ) + return _idval(val=val, argname=argname, idx=idx, **_idval_kwargs) def mini_idvalset(argnames, argvalues, idx): """ mimic _pytest.python._idvalset """ this_id = [ - _idval(val, argname, idx=idx, - idfn=None, item=None, # item is only used by idfn - config=None # if a config hook was available it would be used before this is called - ) + _idval(val, argname, idx=idx,**_idval_kwargs) for val, argname in zip(argvalues, argnames) ] return "-".join(this_id) diff --git a/pytest_cases/main_fixtures.py b/pytest_cases/main_fixtures.py index 19c9ffe6..e6ecd0f7 100644 --- a/pytest_cases/main_fixtures.py +++ b/pytest_cases/main_fixtures.py @@ -1523,6 +1523,15 @@ def parametrize_plus_decorate(test_func): """ test_func_name = test_func.__name__ + # Are there explicit ids provided ? + try: + if len(ids) != len(argvalues): + raise ValueError("Explicit list of `ids` provided has a different length (%s) than the number of " + "parameter sets (%s)" % (len(ids), len(argvalues))) + explicit_ids_to_use = [] + except TypeError: + explicit_ids_to_use = None + # first check if the test function has the parameters as arguments old_sig = signature(test_func) for p in argnames: @@ -1551,12 +1560,20 @@ def parametrize_plus_decorate(test_func): p_fix_name, p_fix_alt = _create_params_alt(test_func_name=test_func_name, hook=hook, union_name=fixture_union_name, from_i=prev_i + 1, to_i=i) fixture_alternatives.append((p_fix_name, p_fix_alt)) + if explicit_ids_to_use is not None: + if isinstance(p_fix_alt, SingleParamAlternative): + explicit_ids_to_use.append(ids[prev_i + 1]) + else: + # the ids provided by the user are propagated to the params of this fix, so we need an id + explicit_ids_to_use.append(ParamIdMakers.explicit(p_fix_alt)) # B/ Now handle the fixture ref at position if j_list is None: # argvalues[i] contains a single argvalue that is a fixture_ref : add the referenced fixture f_fix_name, f_fix_alt = _create_fixture_ref_alt(union_name=fixture_union_name, i=i) fixture_alternatives.append((f_fix_name, f_fix_alt)) + if explicit_ids_to_use is not None: + explicit_ids_to_use.append(ids[i]) else: # argvalues[i] is a tuple, some of them being fixture_ref. create a fixture refering to all of them @@ -1564,6 +1581,8 @@ def parametrize_plus_decorate(test_func): fixture_ref_positions=j_list, test_func_name=test_func_name, hook=hook) fixture_alternatives.append((prod_fix_name, prod_fix_alt)) + if explicit_ids_to_use is not None: + explicit_ids_to_use.append(ids[i]) prev_i = i @@ -1573,6 +1592,12 @@ def parametrize_plus_decorate(test_func): p_fix_name, p_fix_alt = _create_params_alt(test_func_name=test_func_name, union_name=fixture_union_name, from_i=prev_i + 1, to_i=i, hook=hook) fixture_alternatives.append((p_fix_name, p_fix_alt)) + if explicit_ids_to_use is not None: + if isinstance(p_fix_alt, SingleParamAlternative): + explicit_ids_to_use.append(ids[prev_i + 1]) + else: + # the ids provided by the user are propagated to the params of this fix, so we need an id + explicit_ids_to_use.append(ParamIdMakers.explicit(p_fix_alt)) # TO DO if fixtures_to_union has length 1, simplify ? >> No, we leave such "optimization" to the end user @@ -1590,12 +1615,11 @@ def parametrize_plus_decorate(test_func): "Created fixture names are not unique, please report" # Finally create a "main" fixture with a unique name for this test function - # TODO if `ids` were provided, we have to "cut the part where the product params appear # note: the function automatically registers it in the module big_param_fixture = _fixture_union(caller_module, name=fixture_union_name, fix_alternatives=fix_alternatives, unique_fix_alt_names=fix_alt_names, - ids=ids or ParamIdMakers.get(idstyle), hook=hook) + ids=explicit_ids_to_use or ids or ParamIdMakers.get(idstyle), hook=hook) # --create the new test function's signature that we want to expose to pytest # it is the same than existing, except that we want to replace all parameters with the new fixture diff --git a/pytest_cases/tests/fixtures/test_fixture_ref_custom2.py b/pytest_cases/tests/fixtures/test_fixture_ref_custom2.py index 83a097c0..a3239d6d 100644 --- a/pytest_cases/tests/fixtures/test_fixture_ref_custom2.py +++ b/pytest_cases/tests/fixtures/test_fixture_ref_custom2.py @@ -28,7 +28,7 @@ def test_id(arg): def test_synthesis(module_results_dct): - # make sure the id was taken into account + # make sure the id and skip mark were taken into account assert list(module_results_dct) == [ 'test_mark[arg_is_b]', 'test_id[testID]', diff --git a/pytest_cases/tests/fixtures/test_fixture_ref_custom3.py b/pytest_cases/tests/fixtures/test_fixture_ref_custom3.py new file mode 100644 index 00000000..ea778c4a --- /dev/null +++ b/pytest_cases/tests/fixtures/test_fixture_ref_custom3.py @@ -0,0 +1,39 @@ +from distutils.version import LooseVersion + +import pytest +from pytest_cases import parametrize_plus, fixture_ref + +# pytest.param is not available in all versions +if LooseVersion(pytest.__version__) >= LooseVersion('3.0.0'): + @pytest.fixture + def a(): + return 'a' + + + @pytest.fixture(params=['r', 't'], ids="b={}".format) + def b(request): + return "b%s" % request.param + + @parametrize_plus('foo', [1, + fixture_ref(b), + pytest.param('t'), + pytest.param('r', id='W'), + 3, + pytest.param(fixture_ref(a)), + fixture_ref(a) + ], ids=range(7)) + def test_id(foo): + pass + + def test_synthesis(module_results_dct): + # make sure the id and skip mark were taken into account + assert list(module_results_dct) == [ + 'test_id[0]', + 'test_id[1-b=r]', + 'test_id[1-b=t]', + 'test_id[foo_is_P2toP4-2]', + 'test_id[foo_is_P2toP4-W]', + 'test_id[foo_is_P2toP4-4]', + 'test_id[5]', + 'test_id[6]' + ] diff --git a/pytest_cases/tests/fixtures/test_fixture_union_custom_mark.py b/pytest_cases/tests/fixtures/test_fixture_union_custom_mark.py index 2d06b50b..92a17914 100644 --- a/pytest_cases/tests/fixtures/test_fixture_union_custom_mark.py +++ b/pytest_cases/tests/fixtures/test_fixture_union_custom_mark.py @@ -1,30 +1,34 @@ +from distutils.version import LooseVersion + import pytest from pytest_cases import param_fixture, fixture_union -a = param_fixture("a", [1, - pytest.param(2, id='22'), - pytest.param(3, marks=pytest.mark.skip) - ]) +# pytest.param is not available in all versions +if LooseVersion(pytest.__version__) >= LooseVersion('3.0.0'): + a = param_fixture("a", [1, + pytest.param(2, id='22'), + pytest.param(3, marks=pytest.mark.skip) + ]) -b = param_fixture("b", [3, 4]) + b = param_fixture("b", [3, 4]) -c = fixture_union('c', [pytest.param('a', id='A'), - pytest.param(b, marks=pytest.mark.skip) - ], - ids=['ignored', 'B'], - ) + c = fixture_union('c', [pytest.param('a', id='A'), + pytest.param(b, marks=pytest.mark.skip) + ], + ids=['ignored', 'B'], + ) -def test_foo(c): - pass + def test_foo(c): + pass -def test_synthesis(module_results_dct): - # TODO most probably the skip mark on b seeems to mess with the union behaviour. - assert list(module_results_dct) == [ - 'test_foo[A-1]', - 'test_foo[A-22]', - ] + def test_synthesis(module_results_dct): + # TODO most probably the skip mark on b seeems to mess with the union behaviour. + assert list(module_results_dct) == [ + 'test_foo[A-1]', + 'test_foo[A-22]', + ] diff --git a/pytest_cases/tests/issues/test_issue_fixture_union1.py b/pytest_cases/tests/issues/test_issue_fixture_union1.py index 1a78ff14..0b69aee3 100644 --- a/pytest_cases/tests/issues/test_issue_fixture_union1.py +++ b/pytest_cases/tests/issues/test_issue_fixture_union1.py @@ -19,6 +19,6 @@ def test_foo(u): def test_synthesis(module_results_dct): if LooseVersion(pytest.__version__) < LooseVersion('3.0.0'): # the way to make ids uniques in case of duplicates was different in old pytest - assert list(module_results_dct) == ['test_foo[u_is_0a]', 'test_foo[u_is_1a]'] + assert list(module_results_dct) == ['test_foo[0u_is_a]', 'test_foo[1u_is_a]'] else: assert list(module_results_dct) == ['test_foo[u_is_a0]', 'test_foo[u_is_a1]'] From 57229630165fc3d1ba4e1353b82f9001918ce348 Mon Sep 17 00:00:00 2001 From: Sylvain MARIE Date: Sun, 31 May 2020 17:03:56 +0200 Subject: [PATCH 09/10] changelog --- docs/changelog.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/changelog.md b/docs/changelog.md index d64943eb..aaf05a9d 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,11 @@ # Changelog +### 1.15.0 - better `parametrize_plus` and smaller dependencies + + - Better support for `pytest.param` in `parametrize_plus` and also in `fixture_union` and `fixture_param[s]`. Improved corresponding ids. Fixed [#79](https://github.com/smarie/python-pytest-cases/issues/79) and [#86](https://github.com/smarie/python-pytest-cases/issues/86) + + - Removed `six`, `wrapt` and `enum34` dependencies + ### 1.14.0 - bugfixes and hook feature - Fixed `ids` precedence order when using `pytest.mark.parametrize` in a `fixture_plus`. Fixed [#87](https://github.com/smarie/python-pytest-cases/issues/87) From 294c28c23b03b9762ba422bc86cc9d66fd4cb162 Mon Sep 17 00:00:00 2001 From: Sylvain MARIE Date: Sun, 31 May 2020 17:26:22 +0200 Subject: [PATCH 10/10] fixed test for some pytest versions that do not support int ids --- pytest_cases/tests/fixtures/test_fixture_ref_custom3.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pytest_cases/tests/fixtures/test_fixture_ref_custom3.py b/pytest_cases/tests/fixtures/test_fixture_ref_custom3.py index ea778c4a..0fc1aca1 100644 --- a/pytest_cases/tests/fixtures/test_fixture_ref_custom3.py +++ b/pytest_cases/tests/fixtures/test_fixture_ref_custom3.py @@ -21,7 +21,7 @@ def b(request): 3, pytest.param(fixture_ref(a)), fixture_ref(a) - ], ids=range(7)) + ], ids=[str(i) for i in range(7)]) def test_id(foo): pass