Skip to content

Commit 9f12605

Browse files
author
Sylvain MARIE
committed
fixture_union now accept a non-None value for ids. It also has a new idstyle argument allowing users to change the style of ids used. Finally pytest_parametrize_plus relies on this ids argument to set a more readable list of ids for the created union. Fixes #41.
1 parent d15c8be commit 9f12605

10 files changed

+200
-78
lines changed

pytest_cases/main_fixtures.py

Lines changed: 110 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from __future__ import division
33

44
from distutils.version import LooseVersion
5+
from enum import Enum
56
from inspect import isgeneratorfunction, getmodule, currentframe
67
from itertools import product
78
from warnings import warn
@@ -10,7 +11,6 @@
1011
from makefun import with_signature, add_signature_parameters, remove_signature_parameters, wraps
1112

1213
import pytest
13-
from wrapt import ObjectProxy
1414

1515
try: # python 3.3+
1616
from inspect import signature, Parameter
@@ -543,17 +543,56 @@ def __repr__(self):
543543
"""Object representing a fixture value when the fixture is not used"""
544544

545545

546-
class UnionFixtureAlternative(ObjectProxy):
546+
class UnionFixtureAlternative(object):
547547
"""A special class that should be used to wrap a fixture name"""
548548

549-
def __init__(self, fixture_name):
550-
super(UnionFixtureAlternative, self).__init__(fixture_name)
549+
def __init__(self,
550+
fixture_name,
551+
idstyle # type: IdStyle
552+
):
553+
self.fixture_name = fixture_name
554+
self.idstyle = idstyle
551555

552-
def __str__(self):
553-
return str(self.__wrapped__)
556+
# def __str__(self):
557+
# that is maybe too dangerous...
558+
# return self.fixture_name
554559

555560
def __repr__(self):
556-
return "UnionFixtureAlternative<%s>" % str(self)
561+
return "UnionAlternative<%s, idstyle=%s>" % (self.fixture_name, self.idstyle)
562+
563+
@staticmethod
564+
def to_list_of_fixture_names(alternatives_lst # type: List[UnionFixtureAlternative]
565+
):
566+
return [f.fixture_name for f in alternatives_lst]
567+
568+
569+
class IdStyle(Enum):
570+
"""
571+
The enum defining all possible id styles.
572+
"""
573+
none = None
574+
explicit = 'explicit'
575+
compact = 'compact'
576+
577+
578+
def apply_id_style(id, union_fixture_name, idstyle):
579+
"""
580+
Applies the id style defined in `idstyle` to the given id.
581+
See https://github.com/smarie/python-pytest-cases/issues/41
582+
583+
:param id:
584+
:param union_fixture_name:
585+
:param idstyle:
586+
:return:
587+
"""
588+
if idstyle is IdStyle.none:
589+
return id
590+
elif idstyle is IdStyle.explicit:
591+
return "%s_is_%s" % (union_fixture_name, id)
592+
elif idstyle is IdStyle.compact:
593+
return "U%s" % id
594+
else:
595+
raise ValueError("Invalid id style")
557596

558597

559598
def is_fixture_union_params(params):
@@ -578,33 +617,65 @@ def is_used_request(request):
578617
return getattr(request, 'param', None) is not NOT_USED
579618

580619

581-
def fixture_union(name, fixtures, scope="function", ids=None, autouse=False, **kwargs):
620+
def fixture_alternative_to_str(fixture_alternative, # type: UnionFixtureAlternative
621+
):
622+
return fixture_alternative.fixture_name
623+
624+
625+
def fixture_union(name, fixtures, scope="function", idstyle='explicit',
626+
ids=fixture_alternative_to_str, autouse=False, **kwargs):
582627
"""
583-
Creates a fixture that will take all values of the provided fixtures in order.
628+
Creates a fixture that will take all values of the provided fixtures in order. That fixture is automatically
629+
registered into the callers' module, but you may wish to assign it to a variable for convenience. In that case
630+
make sure that you use the same name, e.g. `a = fixture_union('a', ['b', 'c'])`
584631
585-
:param name:
632+
The style of test ids corresponding to the union alternatives can be changed with `idstyle`. Three values are
633+
allowed:
634+
635+
- `'explicit'` (default) favors readability,
636+
- `'compact'` adds a small mark so that at least one sees which parameters are union parameters and which others
637+
are normal parameters,
638+
- `None` does not change the ids.
639+
640+
:param name: the name of the fixture to create
586641
:param fixtures: an array-like containing fixture names and/or fixture symbols
587642
:param scope: the scope of the union. Since the union depends on the sub-fixtures, it should be smaller than the
588-
smallest scope of fictures referenced.
589-
:return:
643+
smallest scope of fixtures referenced.
644+
:param idstyle: The style of test ids corresponding to the union alternatives. One of `'explicit'` (default),
645+
`'compact'`, or `None`.
646+
:param ids: as in pytest. The default value returns the correct fixture
647+
:param autouse: as in pytest
648+
:param kwargs: other pytest fixture options. They might not be supported correctly.
649+
:return: the new fixture. Note: you do not need to capture that output in a symbol, since the fixture is
650+
automatically registered in your module. However if you decide to do so make sure that you use the same name.
590651
"""
591652
caller_module = get_caller_module()
592-
return _fixture_union(caller_module, name, fixtures, scope=scope, ids=ids, autouse=autouse, **kwargs)
653+
return _fixture_union(caller_module, name, fixtures, scope=scope, idstyle=idstyle, ids=ids, autouse=autouse,
654+
**kwargs)
593655

594656

595-
def _fixture_union(caller_module, name, fixtures, scope="function", ids=None, autouse=False, **kwargs):
657+
def _fixture_union(caller_module, name, fixtures, idstyle, scope="function", ids=fixture_alternative_to_str,
658+
autouse=False, **kwargs):
596659
"""
597660
Internal implementation for fixture_union
598661
599662
:param caller_module:
600663
:param name:
601664
:param fixtures:
665+
:param idstyle:
602666
:param scope:
603667
:param ids:
604668
:param autouse:
605669
:param kwargs:
606670
:return:
607671
"""
672+
# test the `fixtures` argument to avoid common mistakes
673+
if not isinstance(fixtures, (tuple, set, list)):
674+
raise TypeError("fixture_union: the `fixtures` argument should be a tuple, set or list")
675+
676+
# validate the idstyle
677+
idstyle = IdStyle(idstyle)
678+
608679
# first get all required fixture names
609680
f_names = []
610681
for f in fixtures:
@@ -621,14 +692,20 @@ def _new_fixture(request, **all_fixtures):
621692
if not is_used_request(request):
622693
return NOT_USED
623694
else:
624-
fixture_to_use = request.param
625-
return all_fixtures[fixture_to_use]
695+
alternative = request.param
696+
if isinstance(alternative, UnionFixtureAlternative):
697+
fixture_to_use = alternative.fixture_name
698+
return all_fixtures[fixture_to_use]
699+
else:
700+
raise TypeError("Union Fixture %s received invalid parameter type: %s. Please report this issue."
701+
"" % (name, alternative.__class__))
626702

627703
_new_fixture.__name__ = name
628704

629705
# finally create the fixture per se.
630706
# WARNING we do not use pytest.fixture but pytest_fixture_plus so that NOT_USED is discarded
631-
f_decorator = pytest_fixture_plus(scope=scope, params=[UnionFixtureAlternative(_name) for _name in f_names],
707+
f_decorator = pytest_fixture_plus(scope=scope,
708+
params=[UnionFixtureAlternative(_name, idstyle) for _name in f_names],
632709
autouse=autouse, ids=ids, **kwargs)
633710
fix = f_decorator(_new_fixture)
634711

@@ -656,7 +733,8 @@ def pytest_parametrize_plus(argnames, argvalues, indirect=False, ids=None, scope
656733
fixtures with `fixture_ref(<fixture>)` where <fixture> can be the fixture name or fixture function.
657734
658735
When such a fixture reference is detected in the argvalues, a new function-scope fixture will be created with a
659-
unique name, and the test function will be wrapped so as to be injected .
736+
unique name, and the test function will be wrapped so as to be injected with the correct parameters. Special test
737+
ids will be created to illustrate the switching between normal parameters and fixtures.
660738
661739
:param argnames:
662740
:param argvalues:
@@ -694,9 +772,9 @@ def create_param_fixture(from_i, to_i, p_fix_name):
694772
selected_ids = ids
695773

696774
if to_i == from_i + 1:
697-
p_fix_name = "%s__%s" % (p_fix_name, from_i)
775+
p_fix_name = "%s_is_%s" % (p_fix_name, from_i)
698776
else:
699-
p_fix_name = "%s__%s_to_%s" % (p_fix_name, from_i, to_i - 1)
777+
p_fix_name = "%s_is_%sto%s" % (p_fix_name, from_i, to_i - 1)
700778
p_fix_name = check_name_available(caller_module, p_fix_name, if_name_exists=CHANGE, caller=pytest_parametrize_plus)
701779
param_fix = _param_fixture(caller_module, p_fix_name, selected_argvalues, selected_ids)
702780
return param_fix
@@ -718,31 +796,41 @@ def parametrize_plus_decorate(test_func):
718796
"" % (p, test_func.__name__, old_sig))
719797

720798
# The base name for all fixtures that will be created below
721-
base_name = test_func.__name__ + '_param__' + argnames.replace(' ', '').replace(',', '_')
799+
# style_template = "%s_param__%s"
800+
style_template = "%s_%s"
801+
base_name = style_template % (test_func.__name__, argnames.replace(' ', '').replace(',', '_'))
722802
base_name = check_name_available(caller_module, base_name, if_name_exists=CHANGE, caller=pytest_parametrize_plus)
723803

724804
# Retrieve (if ref) or create (for normal argvalues) the fixtures that we will union
725805
# TODO important note: we could either wish to create one fixture for parameter value or to create one for
726806
# each consecutive group as shown below. This should not lead to different results but perf might differ.
727807
# maybe add a parameter in the signature so that users can test it ?
728808
fixtures_to_union = []
809+
fixtures_to_union_names_for_ids = []
729810
prev_i = -1
730811
for i in fixture_indices:
731812
if i > prev_i + 1:
732813
param_fix = create_param_fixture(prev_i + 1, i, base_name)
733814
fixtures_to_union.append(param_fix)
815+
fixtures_to_union_names_for_ids.append(get_fixture_name(param_fix))
816+
734817
fixtures_to_union.append(argvalues[i].fixture)
818+
id_for_fixture = apply_id_style(get_fixture_name(argvalues[i].fixture), base_name, IdStyle.explicit)
819+
fixtures_to_union_names_for_ids.append(id_for_fixture)
735820
prev_i = i
736821

737822
# last bit if any
738823
i = len(argvalues)
739824
if i > prev_i + 1:
740825
param_fix = create_param_fixture(prev_i + 1, i, base_name)
741826
fixtures_to_union.append(param_fix)
827+
fixtures_to_union_names_for_ids.append(get_fixture_name(param_fix))
742828

743829
# Finally create a "main" fixture with a unique name for this test function
744830
# note: the function automatically registers it in the module
745-
big_param_fixture = _fixture_union(caller_module, base_name, fixtures_to_union)
831+
# note 2: idstyle is set to None because we provide an explicit enough list of ids
832+
big_param_fixture = _fixture_union(caller_module, base_name, fixtures_to_union, idstyle=None,
833+
ids=fixtures_to_union_names_for_ids)
746834

747835
# --create the new test function's signature that we want to expose to pytest
748836
# it is the same than existing, except that we want to replace all parameters with the new fixture

pytest_cases/plugin.py

Lines changed: 26 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010
from pytest_cases.common import get_pytest_nodeid, get_pytest_function_scopenum, \
1111
is_function_node, get_param_names
12-
from pytest_cases.main_fixtures import NOT_USED, is_fixture_union_params
12+
from pytest_cases.main_fixtures import NOT_USED, is_fixture_union_params, UnionFixtureAlternative, apply_id_style
1313

1414
try: # python 3.3+
1515
from inspect import signature
@@ -258,15 +258,17 @@ def _build_closure(self,
258258

259259
if _params is not None and is_fixture_union_params(_params):
260260
# create an UNION fixture
261-
if _fixdef.ids is not None:
262-
raise ValueError("ids cannot be set on a union fixture")
261+
262+
# transform the _params into a list of names
263+
alternative_f_names = UnionFixtureAlternative.to_list_of_fixture_names(_params)
263264

264265
# if there are direct dependencies that are not the union members, add them to pending
265-
non_member_dependencies = [f for f in _fixdef.argnames if f not in _params]
266+
non_member_dependencies = [f for f in _fixdef.argnames if f not in alternative_f_names]
266267
pending_fixture_names += non_member_dependencies
267268

268269
# propagate WITH the pending
269-
self.split_and_build(fixture_defs_mgr, fixname, fixturedefs, _params, pending_fixture_names)
270+
self.split_and_build(fixture_defs_mgr, fixname, fixturedefs, alternative_f_names,
271+
pending_fixture_names)
270272

271273
# empty the pending
272274
pending_fixture_names = []
@@ -301,7 +303,7 @@ def split_and_build(self,
301303
fixture_defs_mgr, # type: FixtureDefsCache
302304
split_fixture_name, # type: str
303305
split_fixture_defs, # type: Tuple[FixtureDefinition]
304-
alternative_fixture_names, #
306+
alternative_fixture_names, # type: List[str]
305307
pending_fixtures_list #
306308
):
307309
""" Declares that this node contains a union with alternatives (child nodes=subtrees) """
@@ -446,12 +448,15 @@ def getfixtureclosure(fm, fixturenames, parentnode, ignore_args=()):
446448
merge(fixturenames, _init_fixnames)
447449

448450
# Finally create the closure tree
451+
if _DEBUG:
452+
print("Creating closure for %s:" % parentid)
453+
449454
fixture_defs_mger = FixtureDefsCache(fm, parentid)
450455
fixturenames_closure_node = FixtureClosureNode()
451456
fixturenames_closure_node.build_closure(fixture_defs_mger, _init_fixnames)
452457

453458
if _DEBUG:
454-
print("Closure for %s:" % parentid)
459+
print("Closure for %s completed:" % parentid)
455460
print(fixturenames_closure_node)
456461

457462
# sort the fixture names (note: only in recent pytest)
@@ -510,15 +515,15 @@ def pytest_generate_tests(metafunc):
510515
_ = yield
511516

512517

513-
class UnionParamz(namedtuple('UnionParamz', ['union_fixture_name', 'alternative_names', 'scope', 'kwargs'])):
518+
class UnionParamz(namedtuple('UnionParamz', ['union_fixture_name', 'alternative_names', 'ids', 'scope', 'kwargs'])):
514519
""" Represents some parametrization to be applied, for a union fixture """
515520

516521
__slots__ = ()
517522

518523
def __str__(self):
519-
return "[UNION] %s=[%s], scope=%s, kwargs=%s" \
524+
return "[UNION] %s=[%s], ids=%s, scope=%s, kwargs=%s" \
520525
"" % (self.union_fixture_name, ','.join([str(a) for a in self.alternative_names]),
521-
self.scope, self.kwargs)
526+
self.ids, self.scope, self.kwargs)
522527

523528

524529
class NormalParamz(namedtuple('NormalParamz', ['argnames', 'argvalues', 'indirect', 'ids', 'scope', 'kwargs'])):
@@ -560,11 +565,11 @@ def parametrize(metafunc, argnames, argvalues, indirect=False, ids=None, scope=N
560565
raise ValueError("Union fixtures can not be parametrized")
561566
union_fixture_name = argnames
562567
union_fixture_alternatives = argvalues
563-
if indirect is False or ids is not None or len(kwargs) > 0:
564-
raise ValueError("indirect or ids cannot be set on a union fixture")
568+
if indirect is False or len(kwargs) > 0:
569+
raise ValueError("indirect cannot be set on a union fixture, as well as unknown kwargs")
565570

566571
# add a union parametrization in the queue (but do not apply it now)
567-
calls_reactor.append(UnionParamz(union_fixture_name, union_fixture_alternatives, scope, kwargs))
572+
calls_reactor.append(UnionParamz(union_fixture_name, union_fixture_alternatives, ids, scope, kwargs))
568573
else:
569574
# add a normal parametrization in the queue (but do not apply it now)
570575
calls_reactor.append(NormalParamz(argnames, argvalues, indirect, ids, scope, kwargs))
@@ -834,13 +839,20 @@ def _process_node(self, current_node, pending, calls):
834839
# always use 'indirect' since that's a fixture.
835840
calls = self._parametrize_calls(calls, p_to_apply.union_fixture_name,
836841
p_to_apply.alternative_names, indirect=True,
842+
ids=p_to_apply.ids,
837843
scope=p_to_apply.scope, **p_to_apply.kwargs)
838844

845+
# Change the ids
846+
for callspec in calls:
847+
callspec._idlist[-1] = apply_id_style(callspec._idlist[-1],
848+
p_to_apply.union_fixture_name,
849+
p_to_apply.alternative_names[0].idstyle)
850+
839851
# now move to the children
840852
nodes_children = [None] * len(calls)
841853
for i in range(len(calls)):
842854
active_alternative = calls[i].params[p_to_apply.union_fixture_name]
843-
child_node = current_node.children[active_alternative]
855+
child_node = current_node.children[active_alternative.fixture_name]
844856
child_pending = pending.copy()
845857

846858
# place the childs parameter in the first position if it is in the list

pytest_cases/tests/fixtures/test_fixture_in_parametrize.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,8 @@ def test_foo(arg, bar):
3131

3232

3333
def test_synthesis(module_results_dct):
34-
assert list(module_results_dct) == ['test_foo[test_foo_param__arg__0-z-bar]',
35-
'test_foo[a-bar]',
36-
'test_foo[b-a-bar]',
37-
'test_foo[b-b_param__second_letter__1-o-bar]',
38-
'test_foo[test_foo_param__arg__3-o-bar]']
34+
assert list(module_results_dct) == ['test_foo[test_foo_arg_is_0-z-bar]',
35+
'test_foo[test_foo_arg_is_a-bar]',
36+
'test_foo[test_foo_arg_is_b-b_second_letter_is_a-bar]',
37+
'test_foo[test_foo_arg_is_b-b_second_letter_is_1-o-bar]',
38+
'test_foo[test_foo_arg_is_3-o-bar]']

pytest_cases/tests/fixtures/test_fixture_in_parametrize_basic.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,11 @@ def test_prints(main_msg, ending):
2020

2121

2222
def test_synthesis(module_results_dct):
23-
assert list(module_results_dct) == ['test_prints[test_prints_param__main_msg__0-nothing-?]',
24-
'test_prints[test_prints_param__main_msg__0-nothing-!]',
25-
'test_prints[world_str-?]',
26-
'test_prints[world_str-!]',
27-
'test_prints[greetings-world_str-?]',
28-
'test_prints[greetings-world_str-!]',
29-
'test_prints[greetings-greetings_param__who__1-you-?]',
30-
'test_prints[greetings-greetings_param__who__1-you-!]']
23+
assert list(module_results_dct) == ['test_prints[test_prints_main_msg_is_0-nothing-?]',
24+
'test_prints[test_prints_main_msg_is_0-nothing-!]',
25+
'test_prints[test_prints_main_msg_is_world_str-?]',
26+
'test_prints[test_prints_main_msg_is_world_str-!]',
27+
'test_prints[test_prints_main_msg_is_greetings-greetings_who_is_world_str-?]',
28+
'test_prints[test_prints_main_msg_is_greetings-greetings_who_is_world_str-!]',
29+
'test_prints[test_prints_main_msg_is_greetings-greetings_who_is_1-you-?]',
30+
'test_prints[test_prints_main_msg_is_greetings-greetings_who_is_1-you-!]']

0 commit comments

Comments
 (0)