implement pytest.param

this allows a clear addition of parameterization parameters that carry along marks
instead of nesting multiple mark objects and destroying the possibility of creating
function valued parameters,
it just folders everything together into one object carrfying parameters, and the marks.
This commit is contained in:
Ronny Pfannschmidt 2016-09-07 11:00:27 +02:00
parent a122ae85e9
commit e368fb4b29
5 changed files with 224 additions and 102 deletions

View File

@ -6,17 +6,72 @@ from collections import namedtuple
from operator import attrgetter from operator import attrgetter
from .compat import imap from .compat import imap
def alias(name): def alias(name):
return property(attrgetter(name), doc='alias for ' + name) return property(attrgetter(name), doc='alias for ' + name)
class ParameterSet(namedtuple('ParameterSet', 'values, marks, id')):
@classmethod
def param(cls, *values, **kw):
marks = kw.pop('marks', ())
if isinstance(marks, MarkDecorator):
marks = marks,
else:
assert isinstance(marks, (tuple, list, set))
def param_extract_id(id=None):
return id
id = param_extract_id(**kw)
return cls(values, marks, id)
@classmethod
def extract_from(cls, parameterset, legacy_force_tuple=False):
"""
:param parameterset:
a legacy style parameterset that may or may not be a tuple,
and may or may not be wrapped into a mess of mark objects
:param legacy_force_tuple:
enforce tuple wrapping so single argument tuple values
don't get decomposed and break tests
"""
if isinstance(parameterset, cls):
return parameterset
if not isinstance(parameterset, MarkDecorator) and legacy_force_tuple:
return cls.param(parameterset)
newmarks = []
argval = parameterset
while isinstance(argval, MarkDecorator):
newmarks.append(MarkDecorator(Mark(
argval.markname, argval.args[:-1], argval.kwargs)))
argval = argval.args[-1]
assert not isinstance(argval, ParameterSet)
if legacy_force_tuple:
argval = argval,
return cls(argval, marks=newmarks, id=None)
@property
def deprecated_arg_dict(self):
return dict((mark.name, mark) for mark in self.marks)
class MarkerError(Exception): class MarkerError(Exception):
"""Error in use of a pytest marker/attribute.""" """Error in use of a pytest marker/attribute."""
def pytest_namespace(): def pytest_namespace():
return {'mark': MarkGenerator()} return {
'mark': MarkGenerator(),
'param': ParameterSet.param,
}
def pytest_addoption(parser): def pytest_addoption(parser):
@ -212,6 +267,7 @@ def istestfunc(func):
return hasattr(func, "__call__") and \ return hasattr(func, "__call__") and \
getattr(func, "__name__", "<lambda>") != "<lambda>" getattr(func, "__name__", "<lambda>") != "<lambda>"
class MarkDecorator(object): class MarkDecorator(object):
""" A decorator for test functions and test classes. When applied """ A decorator for test functions and test classes. When applied
it will create :class:`MarkInfo` objects which may be it will create :class:`MarkInfo` objects which may be
@ -257,8 +313,11 @@ class MarkDecorator(object):
def markname(self): def markname(self):
return self.name # for backward-compat (2.4.1 had this attr) return self.name # for backward-compat (2.4.1 had this attr)
def __eq__(self, other):
return self.mark == other.mark
def __repr__(self): def __repr__(self):
return "<MarkDecorator %r>" % self.mark return "<MarkDecorator %r>" % (self.mark,)
def __call__(self, *args, **kwargs): def __call__(self, *args, **kwargs):
""" if passed a single callable argument: decorate it with mark info. """ if passed a single callable argument: decorate it with mark info.
@ -291,19 +350,7 @@ class MarkDecorator(object):
return self.__class__(self.mark.combined_with(mark)) return self.__class__(self.mark.combined_with(mark))
def extract_argvalue(maybe_marked_args):
# TODO: incorrect mark data, the old code wanst able to collect lists
# individual parametrized argument sets can be wrapped in a series
# of markers in which case we unwrap the values and apply the mark
# at Function init
newmarks = {}
argval = maybe_marked_args
while isinstance(argval, MarkDecorator):
newmark = MarkDecorator(Mark(
argval.markname, argval.args[:-1], argval.kwargs))
newmarks[newmark.name] = newmark
argval = argval.args[-1]
return argval, newmarks
class Mark(namedtuple('Mark', 'name, args, kwargs')): class Mark(namedtuple('Mark', 'name, args, kwargs')):

View File

@ -788,36 +788,35 @@ class Metafunc(fixtures.FuncargnamesCompatAttr):
to set a dynamic scope using test context or configuration. to set a dynamic scope using test context or configuration.
""" """
from _pytest.fixtures import scope2index from _pytest.fixtures import scope2index
from _pytest.mark import extract_argvalue from _pytest.mark import ParameterSet
from py.io import saferepr from py.io import saferepr
unwrapped_argvalues = []
newkeywords = []
for maybe_marked_args in argvalues:
argval, newmarks = extract_argvalue(maybe_marked_args)
unwrapped_argvalues.append(argval)
newkeywords.append(newmarks)
argvalues = unwrapped_argvalues
if not isinstance(argnames, (tuple, list)): if not isinstance(argnames, (tuple, list)):
argnames = [x.strip() for x in argnames.split(",") if x.strip()] argnames = [x.strip() for x in argnames.split(",") if x.strip()]
if len(argnames) == 1: force_tuple = len(argnames) == 1
argvalues = [(val,) for val in argvalues] else:
if not argvalues: force_tuple = False
argvalues = [(NOTSET,) * len(argnames)] parameters = [
# we passed a empty list to parameterize, skip that test ParameterSet.extract_from(x, legacy_force_tuple=force_tuple)
# for x in argvalues]
del argvalues
if not parameters:
fs, lineno = getfslineno(self.function) fs, lineno = getfslineno(self.function)
newmark = pytest.mark.skip( reason = "got empty parameter set %r, function %s at %s:%d" % (
reason="got empty parameter set %r, function %s at %s:%d" % ( argnames, self.function.__name__, fs, lineno)
argnames, self.function.__name__, fs, lineno)) mark = pytest.mark.skip(reason=reason)
newkeywords = [{newmark.markname: newmark}] parameters.append(ParameterSet(
values=(NOTSET,) * len(argnames),
marks=[mark],
id=None,
))
if scope is None: if scope is None:
scope = _find_parametrized_scope(argnames, self._arg2fixturedefs, indirect) scope = _find_parametrized_scope(argnames, self._arg2fixturedefs, indirect)
scopenum = scope2index( scopenum = scope2index(scope, descr='call to {0}'.format(self.parametrize))
scope, descr='call to {0}'.format(self.parametrize))
valtypes = {} valtypes = {}
for arg in argnames: for arg in argnames:
if arg not in self.fixturenames: if arg not in self.fixturenames:
@ -845,22 +844,22 @@ class Metafunc(fixtures.FuncargnamesCompatAttr):
idfn = ids idfn = ids
ids = None ids = None
if ids: if ids:
if len(ids) != len(argvalues): if len(ids) != len(parameters):
raise ValueError('%d tests specified with %d ids' %( raise ValueError('%d tests specified with %d ids' % (
len(argvalues), len(ids))) len(parameters), len(ids)))
for id_value in ids: for id_value in ids:
if id_value is not None and not isinstance(id_value, py.builtin._basestring): if id_value is not None and not isinstance(id_value, py.builtin._basestring):
msg = 'ids must be list of strings, found: %s (type: %s)' msg = 'ids must be list of strings, found: %s (type: %s)'
raise ValueError(msg % (saferepr(id_value), type(id_value).__name__)) raise ValueError(msg % (saferepr(id_value), type(id_value).__name__))
ids = idmaker(argnames, argvalues, idfn, ids, self.config) ids = idmaker(argnames, parameters, idfn, ids, self.config)
newcalls = [] newcalls = []
for callspec in self._calls or [CallSpec2(self)]: for callspec in self._calls or [CallSpec2(self)]:
elements = zip(ids, argvalues, newkeywords, count()) elements = zip(ids, parameters, count())
for a_id, valset, keywords, param_index in elements: for a_id, param, param_index in elements:
assert len(valset) == len(argnames) assert len(param.values) == len(argnames)
newcallspec = callspec.copy(self) newcallspec = callspec.copy(self)
newcallspec.setmulti(valtypes, argnames, valset, a_id, newcallspec.setmulti(valtypes, argnames, param.values, a_id,
keywords, scopenum, param_index) param.deprecated_arg_dict, scopenum, param_index)
newcalls.append(newcallspec) newcalls.append(newcallspec)
self._calls = newcalls self._calls = newcalls
@ -959,17 +958,19 @@ def _idval(val, argname, idx, idfn, config=None):
return val.__name__ return val.__name__
return str(argname)+str(idx) return str(argname)+str(idx)
def _idvalset(idx, valset, argnames, idfn, ids, config=None): def _idvalset(idx, parameterset, argnames, idfn, ids, config=None):
if parameterset.id is not None:
return parameterset.id
if ids is None or (idx >= len(ids) or ids[idx] is None): if ids is None or (idx >= len(ids) or ids[idx] is None):
this_id = [_idval(val, argname, idx, idfn, config) this_id = [_idval(val, argname, idx, idfn, config)
for val, argname in zip(valset, argnames)] for val, argname in zip(parameterset.values, argnames)]
return "-".join(this_id) return "-".join(this_id)
else: else:
return _escape_strings(ids[idx]) return _escape_strings(ids[idx])
def idmaker(argnames, argvalues, idfn=None, ids=None, config=None): def idmaker(argnames, parametersets, idfn=None, ids=None, config=None):
ids = [_idvalset(valindex, valset, argnames, idfn, ids, config) ids = [_idvalset(valindex, parameterset, argnames, idfn, ids, config)
for valindex, valset in enumerate(argvalues)] for valindex, parameterset in enumerate(parametersets)]
if len(set(ids)) != len(ids): if len(set(ids)) != len(ids):
# The ids are not unique # The ids are not unique
duplicates = [testid for testid in ids if ids.count(testid) > 1] duplicates = [testid for testid in ids if ids.count(testid) > 1]

View File

@ -55,7 +55,7 @@ them in turn::
$ pytest $ pytest
======= test session starts ======== ======= test session starts ========
platform linux -- Python 3.5.2, pytest-3.0.7, py-1.4.32, pluggy-0.4.0 platform linux -- Python 3.5.2, pytest-3.0.3, py-1.4.31, pluggy-0.4.0
rootdir: $REGENDOC_TMPDIR, inifile: rootdir: $REGENDOC_TMPDIR, inifile:
collected 3 items collected 3 items
@ -73,7 +73,7 @@ them in turn::
]) ])
def test_eval(test_input, expected): def test_eval(test_input, expected):
> assert eval(test_input) == expected > assert eval(test_input) == expected
E AssertionError: assert 54 == 42 E assert 54 == 42
E + where 54 = eval('6*9') E + where 54 = eval('6*9')
test_expectation.py:8: AssertionError test_expectation.py:8: AssertionError
@ -94,16 +94,37 @@ for example with the builtin ``mark.xfail``::
@pytest.mark.parametrize("test_input,expected", [ @pytest.mark.parametrize("test_input,expected", [
("3+5", 8), ("3+5", 8),
("2+4", 6), ("2+4", 6),
pytest.mark.xfail(("6*9", 42)), pytest.param("6*9", 42,
marks=pytest.mark.xfail),
]) ])
def test_eval(test_input, expected): def test_eval(test_input, expected):
assert eval(test_input) == expected assert eval(test_input) == expected
.. note::
prior to version 3.1 the supported mechanism for marking values
used the syntax::
import pytest
@pytest.mark.parametrize("test_input,expected", [
("3+5", 8),
("2+4", 6),
pytest.mark.xfail(("6*9", 42),),
])
def test_eval(test_input, expected):
assert eval(test_input) == expected
This was an initial hack to support the feature but soon was demonstrated to be incomplete,
broken for passing functions or applying multiple marks with the same name but different parameters.
The old syntax will be removed in pytest-4.0.
Let's run this:: Let's run this::
$ pytest $ pytest
======= test session starts ======== ======= test session starts ========
platform linux -- Python 3.5.2, pytest-3.0.7, py-1.4.32, pluggy-0.4.0 platform linux -- Python 3.5.2, pytest-3.0.3, py-1.4.31, pluggy-0.4.0
rootdir: $REGENDOC_TMPDIR, inifile: rootdir: $REGENDOC_TMPDIR, inifile:
collected 3 items collected 3 items
@ -186,7 +207,7 @@ Let's also run with a stringinput that will lead to a failing test::
def test_valid_string(stringinput): def test_valid_string(stringinput):
> assert stringinput.isalpha() > assert stringinput.isalpha()
E AssertionError: assert False E assert False
E + where False = <built-in method isalpha of str object at 0xdeadbeef>() E + where False = <built-in method isalpha of str object at 0xdeadbeef>()
E + where <built-in method isalpha of str object at 0xdeadbeef> = '!'.isalpha E + where <built-in method isalpha of str object at 0xdeadbeef> = '!'.isalpha

View File

@ -207,37 +207,40 @@ class TestMetafunc(object):
@pytest.mark.issue250 @pytest.mark.issue250
def test_idmaker_autoname(self): def test_idmaker_autoname(self):
from _pytest.python import idmaker from _pytest.python import idmaker
result = idmaker(("a", "b"), [("string", 1.0), result = idmaker(("a", "b"), [pytest.param("string", 1.0),
("st-ring", 2.0)]) pytest.param("st-ring", 2.0)])
assert result == ["string-1.0", "st-ring-2.0"] assert result == ["string-1.0", "st-ring-2.0"]
result = idmaker(("a", "b"), [(object(), 1.0), result = idmaker(("a", "b"), [pytest.param(object(), 1.0),
(object(), object())]) pytest.param(object(), object())])
assert result == ["a0-1.0", "a1-b1"] assert result == ["a0-1.0", "a1-b1"]
# unicode mixing, issue250 # unicode mixing, issue250
result = idmaker((py.builtin._totext("a"), "b"), [({}, b'\xc3\xb4')]) result = idmaker(
(py.builtin._totext("a"), "b"),
[pytest.param({}, b'\xc3\xb4')])
assert result == ['a0-\\xc3\\xb4'] assert result == ['a0-\\xc3\\xb4']
def test_idmaker_with_bytes_regex(self): def test_idmaker_with_bytes_regex(self):
from _pytest.python import idmaker from _pytest.python import idmaker
result = idmaker(("a"), [(re.compile(b'foo'), 1.0)]) result = idmaker(("a"), [pytest.param(re.compile(b'foo'), 1.0)])
assert result == ["foo"] assert result == ["foo"]
def test_idmaker_native_strings(self): def test_idmaker_native_strings(self):
from _pytest.python import idmaker from _pytest.python import idmaker
totext = py.builtin._totext totext = py.builtin._totext
result = idmaker(("a", "b"), [(1.0, -1.1), result = idmaker(("a", "b"), [
(2, -202), pytest.param(1.0, -1.1),
("three", "three hundred"), pytest.param(2, -202),
(True, False), pytest.param("three", "three hundred"),
(None, None), pytest.param(True, False),
(re.compile('foo'), re.compile('bar')), pytest.param(None, None),
(str, int), pytest.param(re.compile('foo'), re.compile('bar')),
(list("six"), [66, 66]), pytest.param(str, int),
(set([7]), set("seven")), pytest.param(list("six"), [66, 66]),
(tuple("eight"), (8, -8, 8)), pytest.param(set([7]), set("seven")),
(b'\xc3\xb4', b"name"), pytest.param(tuple("eight"), (8, -8, 8)),
(b'\xc3\xb4', totext("other")), pytest.param(b'\xc3\xb4', b"name"),
pytest.param(b'\xc3\xb4', totext("other")),
]) ])
assert result == ["1.0--1.1", assert result == ["1.0--1.1",
"2--202", "2--202",
@ -257,7 +260,7 @@ class TestMetafunc(object):
from _pytest.python import idmaker from _pytest.python import idmaker
enum = pytest.importorskip("enum") enum = pytest.importorskip("enum")
e = enum.Enum("Foo", "one, two") e = enum.Enum("Foo", "one, two")
result = idmaker(("a", "b"), [(e.one, e.two)]) result = idmaker(("a", "b"), [pytest.param(e.one, e.two)])
assert result == ["Foo.one-Foo.two"] assert result == ["Foo.one-Foo.two"]
@pytest.mark.issue351 @pytest.mark.issue351
@ -268,9 +271,10 @@ class TestMetafunc(object):
if isinstance(val, Exception): if isinstance(val, Exception):
return repr(val) return repr(val)
result = idmaker(("a", "b"), [(10.0, IndexError()), result = idmaker(("a", "b"), [
(20, KeyError()), pytest.param(10.0, IndexError()),
("three", [1, 2, 3]), pytest.param(20, KeyError()),
pytest.param("three", [1, 2, 3]),
], idfn=ids) ], idfn=ids)
assert result == ["10.0-IndexError()", assert result == ["10.0-IndexError()",
"20-KeyError()", "20-KeyError()",
@ -284,9 +288,9 @@ class TestMetafunc(object):
def ids(val): def ids(val):
return 'a' return 'a'
result = idmaker(("a", "b"), [(10.0, IndexError()), result = idmaker(("a", "b"), [pytest.param(10.0, IndexError()),
(20, KeyError()), pytest.param(20, KeyError()),
("three", [1, 2, 3]), pytest.param("three", [1, 2, 3]),
], idfn=ids) ], idfn=ids)
assert result == ["a-a0", assert result == ["a-a0",
"a-a1", "a-a1",
@ -306,9 +310,10 @@ class TestMetafunc(object):
rec = WarningsRecorder() rec = WarningsRecorder()
with rec: with rec:
idmaker(("a", "b"), [(10.0, IndexError()), idmaker(("a", "b"), [
(20, KeyError()), pytest.param(10.0, IndexError()),
("three", [1, 2, 3]), pytest.param(20, KeyError()),
pytest.param("three", [1, 2, 3]),
], idfn=ids) ], idfn=ids)
assert [str(i.message) for i in rec.list] == [ assert [str(i.message) for i in rec.list] == [
@ -351,14 +356,21 @@ class TestMetafunc(object):
def test_idmaker_with_ids(self): def test_idmaker_with_ids(self):
from _pytest.python import idmaker from _pytest.python import idmaker
result = idmaker(("a", "b"), [(1, 2), result = idmaker(("a", "b"), [pytest.param(1, 2),
(3, 4)], pytest.param(3, 4)],
ids=["a", None]) ids=["a", None])
assert result == ["a", "3-4"] assert result == ["a", "3-4"]
def test_idmaker_with_paramset_id(self):
from _pytest.python import idmaker
result = idmaker(("a", "b"), [pytest.param(1, 2, id="me"),
pytest.param(3, 4, id="you")],
ids=["a", None])
assert result == ["me", "you"]
def test_idmaker_with_ids_unique_names(self): def test_idmaker_with_ids_unique_names(self):
from _pytest.python import idmaker from _pytest.python import idmaker
result = idmaker(("a"), [1,2,3,4,5], result = idmaker(("a"), map(pytest.param, [1,2,3,4,5]),
ids=["a", "a", "b", "c", "b"]) ids=["a", "a", "b", "c", "b"])
assert result == ["a0", "a1", "b0", "c", "b1"] assert result == ["a0", "a1", "b0", "c", "b1"]
@ -1438,6 +1450,31 @@ class TestMarkersWithParametrization(object):
reprec = testdir.inline_run() reprec = testdir.inline_run()
reprec.assertoutcome(passed=2) reprec.assertoutcome(passed=2)
@pytest.mark.parametrize('strict', [True, False])
def test_parametrize_marked_value(self, testdir, strict):
s = """
import pytest
@pytest.mark.parametrize(("n", "expected"), [
pytest.param(
2,3,
marks=pytest.mark.xfail("sys.version_info > (0, 0, 0)", reason="some bug", strict={strict}),
),
pytest.param(
2,3,
marks=[pytest.mark.xfail("sys.version_info > (0, 0, 0)", reason="some bug", strict={strict})],
),
])
def test_increment(n, expected):
assert n + 1 == expected
""".format(strict=strict)
testdir.makepyfile(s)
reprec = testdir.inline_run()
passed, failed = (0, 2) if strict else (2, 0)
reprec.assertoutcome(passed=passed, failed=failed)
def test_pytest_make_parametrize_id(self, testdir): def test_pytest_make_parametrize_id(self, testdir):
testdir.makeconftest(""" testdir.makeconftest("""
def pytest_make_parametrize_id(config, val): def pytest_make_parametrize_id(config, val):

View File

@ -1,8 +1,9 @@
from __future__ import absolute_import, division, print_function from __future__ import absolute_import, division, print_function
import os import os
import sys
import py, pytest import pytest
from _pytest.mark import MarkGenerator as Mark from _pytest.mark import MarkGenerator as Mark, ParameterSet
class TestMark(object): class TestMark(object):
def test_markinfo_repr(self): def test_markinfo_repr(self):
@ -10,9 +11,11 @@ class TestMark(object):
m = MarkInfo(Mark("hello", (1,2), {})) m = MarkInfo(Mark("hello", (1,2), {}))
repr(m) repr(m)
def test_pytest_exists_in_namespace_all(self): @pytest.mark.parametrize('attr', ['mark', 'param'])
assert 'mark' in py.test.__all__ @pytest.mark.parametrize('modulename', ['py.test', 'pytest'])
assert 'mark' in pytest.__all__ def test_pytest_exists_in_namespace_all(self, attr, modulename):
module = sys.modules[modulename]
assert attr in module.__all__
def test_pytest_mark_notcallable(self): def test_pytest_mark_notcallable(self):
mark = Mark() mark = Mark()
@ -415,7 +418,7 @@ class TestFunctional(object):
""") """)
items, rec = testdir.inline_genitems(p) items, rec = testdir.inline_genitems(p)
for item in items: for item in items:
print (item, item.keywords) print(item, item.keywords)
assert 'a' in item.keywords assert 'a' in item.keywords
def test_mark_decorator_subclass_does_not_propagate_to_base(self, testdir): def test_mark_decorator_subclass_does_not_propagate_to_base(self, testdir):
@ -739,3 +742,16 @@ class TestKeywordSelection(object):
assert_test_is_not_selected("__") assert_test_is_not_selected("__")
assert_test_is_not_selected("()") assert_test_is_not_selected("()")
@pytest.mark.parametrize('argval, expected', [
(pytest.mark.skip()((1, 2)),
ParameterSet(values=(1, 2), marks=[pytest.mark.skip], id=None)),
(pytest.mark.xfail(pytest.mark.skip()((1, 2))),
ParameterSet(values=(1, 2),
marks=[pytest.mark.xfail, pytest.mark.skip], id=None)),
])
def test_parameterset_extractfrom(argval, expected):
extracted = ParameterSet.extract_from(argval)
assert extracted == expected