Skip to content

Commit 1762965

Browse files
author
Sylvain MARIE
committed
Major improvement of @pytest_fixture_plus: instead of generating fixtures, it now correctly parametrizes the fixture. Skip/fail Marks are correctly copied too. Fixes #28
`pytest_fixture_plus` does not accept any more the `params` and `ids` arguments, it only relies on parametrization marks. Added `pytest-harvest` dependency and improved the related tests. All `pytest`-related utils have moved to common.py, for clarity.
1 parent 915688f commit 1762965

File tree

8 files changed

+270
-189
lines changed

8 files changed

+270
-189
lines changed

ci_tools/requirements-pip.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ xunitparser
1313
pytest$PYTEST_VERSION
1414
pytest-logging # ==2015.11.4
1515
pytest-steps
16+
pytest-harvest
1617

1718
# --- to generate the reports (see scripts in ci_tools, called by .travis)
1819
pytest-cov==2.6.0 # after 2.6.1 it requires pytest 3.6

pytest_cases/common.py

Lines changed: 132 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,11 @@
33
except ImportError:
44
from funcsigs import signature
55

6-
import pytest
6+
from distutils.version import LooseVersion
7+
from warnings import warn
78

9+
import pytest
10+
from _pytest.mark import ParameterSet
811

912
# Create a symbol that will work to create a fixture containing 'yield', whatever the pytest version
1013
# Note: if more prevision is needed, use if LooseVersion(pytest.__version__) < LooseVersion('3.0.0')
@@ -14,17 +17,10 @@
1417
yield_fixture = pytest.fixture
1518

1619

17-
class _LegacyMark:
18-
__slots__ = "args", "kwargs"
19-
20-
def __init__(self, *args, **kwargs):
21-
self.args = args
22-
self.kwargs = kwargs
23-
24-
20+
# ------------ container for the mark information that we grab from the fixtures (`@pytest_fixture_plus`)
2521
class _ParametrizationMark:
2622
"""
27-
Represents the information required by `decorate_pytest_fixture_plus` to work.
23+
Represents the information required by `@pytest_fixture_plus` to work.
2824
"""
2925
__slots__ = "param_names", "param_values", "param_ids"
3026

@@ -40,9 +36,36 @@ def __init__(self, mark):
4036
self.param_ids = bound.arguments.get('ids', None)
4137

4238

39+
# -------- tools to get the parametrization mark whatever the pytest version
40+
class _LegacyMark:
41+
__slots__ = "args", "kwargs"
42+
43+
def __init__(self, *args, **kwargs):
44+
self.args = args
45+
self.kwargs = kwargs
46+
47+
48+
def get_pytest_marks_on_function(f):
49+
"""
50+
Utility to return *ALL* pytest marks (not only parametrization) applied on a function
51+
52+
:param f:
53+
:return:
54+
"""
55+
try:
56+
return f.pytestmark
57+
except AttributeError:
58+
try:
59+
# old pytest < 3: marks are set as fields on the function object
60+
# but they do not have a particulat type, their type is 'instance'...
61+
return [v for v in vars(f).values() if str(v).startswith("<MarkInfo '")]
62+
except AttributeError:
63+
return []
64+
65+
4366
def get_pytest_parametrize_marks(f):
4467
"""
45-
Returns the @pytest.mark.parametrize marks associated with a function
68+
Returns the @pytest.mark.parametrize marks associated with a function (and only those)
4669
4770
:param f:
4871
:return: a tuple containing all 'parametrize' marks
@@ -74,3 +97,101 @@ def get_parametrize_signature():
7497
:return: a reference signature representing
7598
"""
7699
return signature(_pytest_mark_parametrize)
100+
101+
102+
# ---------- test ids utils ---------
103+
def get_test_ids_from_param_values(param_names,
104+
param_values,
105+
):
106+
"""
107+
Replicates pytest behaviour to generate the ids when there are several parameters in a single `parametrize`
108+
109+
:param param_names:
110+
:param param_values:
111+
:return:
112+
"""
113+
nb_params = len(param_names)
114+
if nb_params == 0:
115+
raise ValueError("empty list provided")
116+
elif nb_params == 1:
117+
paramids = tuple(str(v) for v in param_values)
118+
else:
119+
paramids = []
120+
for vv in param_values:
121+
if len(vv) != nb_params:
122+
raise ValueError("Inconsistent lenghts for parameter names and values: '%s' and '%s'"
123+
"" % (param_names, vv))
124+
paramids.append('-'.join([str(v) for v in vv]))
125+
return paramids
126+
127+
128+
# ---- ParameterSet api ---
129+
def is_marked_parameter_value(v):
130+
return isinstance(v, ParameterSet)
131+
132+
133+
def get_marked_parameter_marks(v):
134+
return v.marks
135+
136+
137+
def get_marked_parameter_values(v):
138+
return v.values
139+
140+
141+
# ---- tools to reapply marks on test parameter values, whatever the pytest version ----
142+
143+
# Compatibility for the way we put marks on single parameters in the list passed to @pytest.mark.parametrize
144+
# see https://docs.pytest.org/en/3.3.0/skipping.html?highlight=mark%20parametrize#skip-xfail-with-parametrize
145+
146+
try:
147+
# check if pytest.param exists
148+
_ = pytest.param
149+
except AttributeError:
150+
# if not this is how it was done
151+
# see e.g. https://docs.pytest.org/en/2.9.2/skipping.html?highlight=mark%20parameter#skip-xfail-with-parametrize
152+
def make_marked_parameter_value(c, marks):
153+
if len(marks) > 1:
154+
raise ValueError("Multiple marks on parameters not supported for old versions of pytest")
155+
else:
156+
# get a decorator for each of the markinfo
157+
marks_mod = transform_marks_into_decorators(marks)
158+
159+
# decorate
160+
return marks_mod[0](c)
161+
else:
162+
# Otherwise pytest.param exists, it is easier
163+
def make_marked_parameter_value(c, marks):
164+
# get a decorator for each of the markinfo
165+
marks_mod = transform_marks_into_decorators(marks)
166+
167+
# decorate
168+
return pytest.param(c, marks=marks_mod)
169+
170+
171+
def transform_marks_into_decorators(marks):
172+
"""
173+
Transforms the provided marks (MarkInfo) obtained from marked cases, into MarkDecorator so that they can
174+
be re-applied to generated pytest parameters in the global @pytest.mark.parametrize.
175+
176+
:param marks:
177+
:return:
178+
"""
179+
marks_mod = []
180+
try:
181+
for m in marks:
182+
md = pytest.mark.MarkDecorator()
183+
if LooseVersion(pytest.__version__) >= LooseVersion('3.0.0'):
184+
md.mark = m
185+
else:
186+
md.name = m.name
187+
# md.markname = m.name
188+
md.args = m.args
189+
md.kwargs = m.kwargs
190+
191+
# markinfodecorator = getattr(pytest.mark, markinfo.name)
192+
# markinfodecorator(*markinfo.args)
193+
194+
marks_mod.append(md)
195+
except Exception as e:
196+
warn("Caught exception while trying to mark case: [%s] %s" % (type(e), e))
197+
return marks_mod

0 commit comments

Comments
 (0)