Skip to content

Commit 1c52dfd

Browse files
author
Sylvain MARIE
committed
get_current_cases now has a far better coverage: it is more robust, and also cases used to parametrize fixtures also appear in the dictionary as subdicts. To enable this,
- the `ParamAlternative` class was modified so that there is a link back to the decorated item. - `@fixture` was modified so that when it creates its "combined" parameter values, it creates them as instances of a special `CombinedFixtureParamValue` instead of just a tuple.
1 parent a246abd commit 1c52dfd

9 files changed

+501
-110
lines changed

pytest_cases/case_parametrizer_new.py

Lines changed: 152 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,14 @@
2525
from .common_pytest import safe_isclass, MiniMetafunc, is_fixture, get_fixture_name, inject_host, add_fixture_params, \
2626
list_all_fixtures_in
2727

28-
from . import fixture
2928
from .case_funcs import matches_tag_query, is_case_function, is_case_class, CASE_PREFIX_FUN, copy_case_info, \
3029
get_case_id, get_case_marks, GEN_BY_US
30+
31+
from .fixture_core1_unions import USED, NOT_USED
32+
from .fixture_core2 import CombinedFixtureParamValue, fixture
3133
from .fixture__creation import check_name_available, CHANGE
32-
from .fixture_parametrize_plus import fixture_ref, _parametrize_plus, UnionFixtureAlternative
34+
from .fixture_parametrize_plus import fixture_ref, _parametrize_plus, FixtureParamAlternative, ParamAlternative, \
35+
SingleParamAlternative, MultiParamAlternative
3336

3437
try:
3538
ModuleNotFoundError
@@ -322,15 +325,15 @@ def get_parametrize_args(host_class_or_module, # type: Union[Type, ModuleType
322325
debug)]
323326

324327

325-
class CaseParameter(object):
328+
class CaseParamValue(object):
326329
"""Common class for lazy values and fixture refs created from cases"""
327330
__slots__ = ()
328331

329332
def get_case_function(self, request):
330333
raise NotImplementedError()
331334

332335

333-
class _NonFixtureCase(LazyValue, CaseParameter):
336+
class _LazyValueCaseParamValue(LazyValue, CaseParamValue):
334337
"""A case that does not require any fixture is transformed into a `lazy_value` parameter
335338
when passed to @parametrize.
336339
@@ -340,8 +343,17 @@ class _NonFixtureCase(LazyValue, CaseParameter):
340343
def get_case_function(self, request):
341344
return self.valuegetter
342345

346+
def as_lazy_tuple(self, nb_params):
347+
return _LazyTupleCaseParamValue(self, nb_params)
348+
349+
350+
class _LazyTupleCaseParamValue(LazyTuple, CaseParamValue):
351+
"""A case representing a tuple"""
352+
def get_case_function(self, request):
353+
return self._lazyvalue.valuegetter
354+
343355

344-
class _FixtureCase(fixture_ref, CaseParameter):
356+
class _FixtureRefCaseParamValue(fixture_ref, CaseParamValue):
345357
"""A case that requires at least a fixture is transformed into a `fixture_ref` parameter
346358
when passed to @parametrize"""
347359

@@ -393,7 +405,7 @@ def case_to_argvalues(host_class_or_module, # type: Union[Type, ModuleType]
393405
case_fun_str = qname(case_fun.func if isinstance(case_fun, functools.partial) else case_fun)
394406
print("Case function %s > 1 lazy_value() with id %s and additional marks %s"
395407
% (case_fun_str, case_id, case_marks))
396-
return (_NonFixtureCase(case_fun, id=case_id, marks=case_marks),)
408+
return (_LazyValueCaseParamValue(case_fun, id=case_id, marks=case_marks),)
397409
# else:
398410
# THIS WAS A PREMATURE OPTIMIZATION WITH MANY SHORTCOMINGS. For example what if the case function is
399411
# itself parametrized with lazy values ? Let's consider that a parametrized case should be a fixture,
@@ -421,7 +433,7 @@ def case_to_argvalues(host_class_or_module, # type: Union[Type, ModuleType]
421433
import_fixtures=import_fixtures, debug=debug)
422434

423435
# reference that case fixture, and preserve the case id in the associated id whatever the generated fixture name
424-
argvalues = _FixtureCase(fix_name, id=case_id)
436+
argvalues = _FixtureRefCaseParamValue(fix_name, id=case_id)
425437
if debug:
426438
case_fun_str = qname(case_fun.func if isinstance(case_fun, functools.partial) else case_fun)
427439
print("Case function %s > fixture_ref(%r) with marks %s" % (case_fun_str, fix_name, remaining_marks))
@@ -815,16 +827,21 @@ def _of_interest(x): # noqa
815827

816828
def get_current_cases(request_or_item):
817829
"""
818-
Returns a dictionary of {argname: (actual_id, case_function)} for a given `pytest` item. The `actual_id`
819-
might differ from the case_id defined on the case, since it might be overridden through pytest cusomtization.
820-
To get more information on the case function, you can use `get_case_id(f)`, `get_case_marks(f)`, `get_case_tags(f)`.
830+
Returns a dictionary containing all case parameters for the currently active `pytest` item.
831+
You can either pass the `pytest` item (available in some hooks) or the `request` (available in hooks, and also
832+
directly as a fixture).
833+
834+
For each test function argument parametrized using a `@parametrize_with_case(<argname>, ...)` this dictionary
835+
contains an entry `{<argname>: (actual_id, case_function)}`. If several argnames are parametrized this way,
836+
a dedicated entry will be present for each argname.
837+
838+
If a fixture parametrized with cases is active, the dictionary will contain an entry `{<fixturename>: <dct>}` where
839+
`<dct>` is a dictionary `{<argname>: (actual_id, case_function)}`.
821840
841+
To get more information on a case function, you can use `get_case_id(f)`, `get_case_marks(f)`, `get_case_tags(f)`.
822842
You can also use `matches_tag_query` to check if a case function matches some expectations either concerning its id
823843
or its tags. See https://smarie.github.io/python-pytest-cases/#filters-and-tags
824844
825-
You can either pass the `pytest` item (available in some hooks) or the `request` (available in hooks, and also
826-
directly as a fixture).
827-
828845
Note that you can get the same contents directly by using the `current_cases` fixture.
829846
"""
830847
try:
@@ -835,32 +852,138 @@ def get_current_cases(request_or_item):
835852
else:
836853
request = request_or_item
837854

855+
# (1) scan for MultiParamAlternatives to store fixturename -> argnames
856+
mp_fix_to_args = dict()
857+
for argname_or_fixturename, current_param_value in item.callspec.params.items():
858+
if isinstance(current_param_value, MultiParamAlternative):
859+
mp_fix_to_args[current_param_value.alternative_name] = current_param_value.argnames, current_param_value.decorated
860+
861+
# (2) now extract the cases
838862
results = dict()
839-
for param_or_fixture_name, current_param_value in item.callspec.params.items():
863+
for argname_or_fixturename, current_param_value in item.callspec.params.items():
864+
865+
# (1) Combined parameters on a fixture, except those including fixture refs
866+
if isinstance(current_param_value, CombinedFixtureParamValue):
867+
# create a sub dictionary
868+
fix_results = _get_or_create_subdict(results, argname_or_fixturename)
869+
870+
# now de-combine each distinct @parametrize that was made on that fixture
871+
# (we had to combine them in our @fixture because pytest does not support multiple parametrize on fixtures)
872+
for argnames, argvals in current_param_value.iterparams():
873+
# this is a single @parametrize(argnames, argvals)
874+
if len(argnames) == 1:
875+
_possibly_add_cases_to_results(request, fix_results, mp_fix_to_args, argnames[0], argvals)
876+
else:
877+
if isinstance(argvals, LazyTuple):
878+
for item, argname in enumerate(argnames):
879+
argval = LazyTupleItem(argvals, item)
880+
_possibly_add_cases_to_results(request, fix_results, mp_fix_to_args, argname, argval)
881+
else:
882+
print()
883+
884+
# (2) Parameters on a test function, or parameters on a fixture with a fixture_ref inside (other fixture gen)
885+
else:
886+
_possibly_add_cases_to_results(request, results, mp_fix_to_args, argname_or_fixturename, current_param_value)
887+
888+
return results
889+
840890

841-
# First, unpack possible lazy tuples
891+
def _possibly_add_cases_to_results(request, results, mp_fix_to_args, argname_or_fixturename, current_param_value):
892+
893+
actual_id = None
894+
argnames = None
895+
argname = None
896+
case_func = None
897+
parametrized = None
898+
899+
# Does this parameter correspond to a fixture generated by a MultiParamAlternative ?
900+
try:
901+
argnames, parametrized = mp_fix_to_args[argname_or_fixturename]
902+
except KeyError:
903+
can_be_a_complex_parametrize_value = True
904+
else:
905+
can_be_a_complex_parametrize_value = False
906+
907+
# Is this parameter a ParamAlternative ? (this would mean that at least one case was a fixture)
908+
if can_be_a_complex_parametrize_value and isinstance(current_param_value, ParamAlternative):
909+
# Common part
910+
parametrized = current_param_value.decorated
911+
actual_id = current_param_value.get_alternative_id()
912+
913+
# Handle all kind of parameters when a fixture union was created (at least a fixture ref).
914+
if isinstance(current_param_value, FixtureParamAlternative):
915+
# a fixture case (fixture_ref)
916+
if isinstance(current_param_value.argval, _FixtureRefCaseParamValue):
917+
argnames = current_param_value.argnames
918+
case_func = current_param_value.argval.get_case_function(request)
919+
else:
920+
# another fixture ref - not a case, silently return
921+
return
922+
923+
elif isinstance(current_param_value, SingleParamAlternative):
924+
# a non-fixture case (lazy_value) when at least one other case is a fixture
925+
if isinstance(current_param_value.argval, _LazyTupleCaseParamValue):
926+
# an entire tuple. This only happens here, as for simpler params the tuple items appear directly.
927+
# actual_id = current_param_value.argval.get_id() not needed
928+
argnames = current_param_value.argnames
929+
case_func = current_param_value.argval.get_case_function(request)
930+
else:
931+
# a single value. continue: this will be handled similar to what is below
932+
current_param_value = current_param_value.argval
933+
934+
# elif isinstance(current_param_value, ProductParamAlternative):
935+
# # This should not happen with cases, this is a tuple where a *member* is a fixture_ref
936+
# pass
937+
938+
elif isinstance(current_param_value, MultiParamAlternative):
939+
# ignore silently, already handled in the pass before the main loop
940+
return
941+
942+
# If the parametrization target is not the test but a fixture, store the cases in a dub-dict
943+
if parametrized is not None and parametrized.__name__ != request.node.function.__name__:
944+
results = _get_or_create_subdict(results, parametrized.__name__)
945+
946+
# If we did not yet find the case function, this is because this was a simple parametrize without fixture ref.
947+
if case_func is None:
842948
if isinstance(current_param_value, LazyTupleItem):
843-
# a non-fixture case that corresponds to several arguments. There will be an entry for each argument
949+
# a non-fixture case (lazy_value) that corresponds to several arguments. There will be an entry for each arg
844950
current_param_value = current_param_value.host._lazyvalue
845951

846-
if isinstance(current_param_value, CaseParameter):
847-
# a non-fixture case : a `lazy_value`
952+
# ------ and finally
953+
954+
if isinstance(current_param_value, _LazyValueCaseParamValue):
955+
# a non-fixture case (lazy_value)
848956
case_func = current_param_value.get_case_function(request)
849957
actual_id = current_param_value.get_id()
850-
results[param_or_fixture_name] = (actual_id, case_func)
958+
if argnames is None:
959+
argname = argname_or_fixturename
851960

852-
elif isinstance(current_param_value, UnionFixtureAlternative):
853-
# a fixture case: we have to dig one level more in order to access the actual `fixture_ref`
854-
# Also for consistency with the non-fixture parameters, we create an entry for each argname
855-
actual_id = current_param_value.get_alternative_id()
856-
case_func = current_param_value.argval.get_case_function(request)
857-
for argname in current_param_value.argnames:
858-
results[argname] = (actual_id, case_func)
961+
elif current_param_value in (NOT_USED, USED):
962+
# ignore silently
963+
return
964+
else:
965+
# raise TypeError("Internal error - type not expected : %r" % type(current_param_value))
966+
# some other parameter - return silently
967+
return
968+
969+
# Finally do it
970+
if argnames is not None:
971+
if (actual_id is None) or (case_func is None):
972+
raise ValueError("Internal error - please report")
973+
for argname in argnames:
974+
results[argname] = (actual_id, case_func)
975+
else:
976+
if (argname is None) or (actual_id is None) or (case_func is None):
977+
raise ValueError("Internal error - please report")
978+
results[argname] = (actual_id, case_func)
859979

860-
elif isinstance(current_param_value, LazyTuple):
861-
raise TypeError("This should not happen, please report")
862980

863-
return results
981+
def _get_or_create_subdict(dct, key):
982+
try:
983+
return dct[key]
984+
except KeyError:
985+
dct[key] = dict()
986+
return dct[key]
864987

865988

866989
def get_current_case_id(request_or_item,

pytest_cases/fixture_core2.py

Lines changed: 50 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -323,6 +323,34 @@ def fixture(scope="function", # type: str
323323
hook=hook, _caller_module_offset_when_unpack=3, **kwargs)
324324

325325

326+
class FixtureParam(object):
327+
__slots__ = 'argnames',
328+
329+
def __init__(self, argnames):
330+
self.argnames = argnames
331+
332+
def __repr__(self):
333+
return "FixtureParam(argnames=%s)" % self.argnames
334+
335+
336+
class CombinedFixtureParamValue(object):
337+
"""Represents a parameter value created when @parametrize is used on a @fixture """
338+
__slots__ = 'param_defs', 'argvalues',
339+
340+
def __init__(self,
341+
param_defs, # type: Iterable[FixtureParam]
342+
argvalues):
343+
self.param_defs = param_defs
344+
self.argvalues = argvalues
345+
346+
def iterparams(self):
347+
return ((pdef.argnames, v) for pdef, v in zip(self.param_defs, self.argvalues))
348+
349+
def __repr__(self):
350+
list_str = " ; ".join(["<%r: %s>" % (a, v) for a, v in self.iterparams()])
351+
return "CombinedFixtureParamValue(%s)" % list_str
352+
353+
326354
def _decorate_fixture_plus(fixture_func,
327355
scope="function", # type: str
328356
autouse=False, # type: bool
@@ -398,7 +426,7 @@ def _decorate_fixture_plus(fixture_func,
398426

399427
# (2) create the huge "param" containing all params combined
400428
# --loop (use the same order to get it right)
401-
params_names_or_name_combinations = []
429+
param_defs = []
402430
params_values = []
403431
params_ids = []
404432
params_marks = []
@@ -411,7 +439,7 @@ def _decorate_fixture_plus(fixture_func,
411439
"name in a @pytest.mark.parametrize mark")
412440

413441
# remember the argnames
414-
params_names_or_name_combinations.append(pmark.param_names)
442+
param_defs.append(FixtureParam(pmark.param_names))
415443

416444
# separate specific configuration (pytest.param()) from the values
417445
custom_pids, _pmarks, _pvalues = extract_parameterset_info(pmark.param_names, pmark.param_values, check_nb=True)
@@ -426,19 +454,24 @@ def _decorate_fixture_plus(fixture_func,
426454
params_values.append(tuple(_pvalues))
427455

428456
# (3) generate the ids and values, possibly reapplying marks
429-
if len(params_names_or_name_combinations) == 1:
430-
# we can simplify - that will be more readable
457+
if len(param_defs) == 1:
458+
# A single @parametrize : we can simplify - that will be more readable
431459
final_ids = params_ids[0]
432460
final_marks = params_marks[0]
433-
final_values = list(params_values[0])
461+
# note: we dot his even for a single @parametrize as it allows `current_case` to get the parameter names easily
462+
final_values = [CombinedFixtureParamValue(param_defs, (v,)) for v in params_values[0]]
434463

435464
# reapply the marks
436465
for i, marks in enumerate(final_marks):
437466
if marks is not None:
438467
final_values[i] = make_marked_parameter_value((final_values[i],), marks=marks)
439468
else:
440-
final_values = list(product(*params_values))
469+
# Multiple @parametrize: since pytest does not support several, we merge them with "apparence" of several
470+
# --equivalent id
441471
final_ids = combine_ids(product(*params_ids))
472+
# --merge all values, we'll unpack them in the wrapper below
473+
final_values = [CombinedFixtureParamValue(param_defs, v) for v in product(*params_values)]
474+
442475
final_marks = tuple(product(*params_marks))
443476

444477
# reapply the marks
@@ -451,7 +484,7 @@ def _decorate_fixture_plus(fixture_func,
451484
raise ValueError("Internal error related to fixture parametrization- please report")
452485

453486
# (4) wrap the fixture function so as to remove the parameter names and add 'request' if needed
454-
all_param_names = tuple(v for pnames in params_names_or_name_combinations for v in pnames)
487+
all_param_names = tuple(v for pnames in param_defs for v in pnames.argnames)
455488

456489
# --create the new signature that we want to expose to pytest
457490
old_sig = signature(fixture_func)
@@ -470,19 +503,22 @@ def _decorate_fixture_plus(fixture_func,
470503
def _map_arguments(*_args, **_kwargs):
471504
request = _kwargs['request'] if func_needs_request else _kwargs.pop('request')
472505

506+
# sanity check: we have created this combined value in the combined parametrization.
507+
_paramz = request.param
508+
if not isinstance(_paramz, CombinedFixtureParamValue):
509+
# This can happen when indirect parametrization has been used.
510+
# In that case we can work but this parameter will not appear in `current_cases` fixture
511+
_paramz = CombinedFixtureParamValue(param_defs, _paramz if len(param_defs) > 1 else (_paramz,))
512+
473513
# populate the parameters
474-
if len(params_names_or_name_combinations) == 1:
475-
_params = [request.param] # remove the simplification
476-
else:
477-
_params = request.param
478-
for p_names, fixture_param_value in zip(params_names_or_name_combinations, _params):
514+
for p_names, p_argvals in _paramz.iterparams():
479515
if len(p_names) == 1:
480516
# a single parameter for that generated fixture (@pytest.mark.parametrize with a single name)
481-
_kwargs[p_names[0]] = get_lazy_args(fixture_param_value, request)
517+
_kwargs[p_names[0]] = get_lazy_args(p_argvals, request)
482518
else:
483519
# several parameters for that generated fixture (@pytest.mark.parametrize with several names)
484520
# unpack all of them and inject them in the kwargs
485-
for old_p_name, old_p_value in zip(p_names, fixture_param_value):
521+
for old_p_name, old_p_value in zip(p_names, p_argvals):
486522
_kwargs[old_p_name] = get_lazy_args(old_p_value, request)
487523

488524
return _args, _kwargs

0 commit comments

Comments
 (0)