Remove support for applying marks to values in parametrize

Fix #3082
This commit is contained in:
Bruno Oliveira 2018-12-20 16:13:43 -02:00
parent d888d5c933
commit c378cb4793
7 changed files with 110 additions and 141 deletions

View File

@ -0,0 +1,3 @@
Removed support for applying marks directly to values in ``@pytest.mark.parametrize``. Use ``pytest.param`` instead.
See our `docs <https://docs.pytest.org/en/latest/deprecations.html#marks-in-pytest-mark-parametrize>`__ on information on how to update your code.

View File

@ -7,6 +7,11 @@ This page lists all pytest features that are currently deprecated or have been r
The objective is to give users a clear rationale why a certain feature has been removed, and what alternatives The objective is to give users a clear rationale why a certain feature has been removed, and what alternatives
should be used instead. should be used instead.
.. contents::
:depth: 3
:local:
Deprecated Features Deprecated Features
------------------- -------------------
@ -81,40 +86,6 @@ As part of a large :ref:`marker-revamp`, :meth:`_pytest.nodes.Node.get_marker` i
:ref:`the documentation <update marker code>` on tips on how to update your code. :ref:`the documentation <update marker code>` on tips on how to update your code.
marks in ``pytest.mark.parametrize``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. deprecated:: 3.2
Applying marks to values of a ``pytest.mark.parametrize`` call is now deprecated. For example:
.. code-block:: python
@pytest.mark.parametrize(
"a, b", [(3, 9), pytest.mark.xfail(reason="flaky")(6, 36), (10, 100)]
)
def test_foo(a, b):
...
This code applies the ``pytest.mark.xfail(reason="flaky")`` mark to the ``(6, 36)`` value of the above parametrization
call.
This was considered hard to read and understand, and also its implementation presented problems to the code preventing
further internal improvements in the marks architecture.
To update the code, use ``pytest.param``:
.. code-block:: python
@pytest.mark.parametrize(
"a, b",
[(3, 9), pytest.param((6, 36), marks=pytest.mark.xfail(reason="flaky")), (10, 100)],
)
def test_foo(a, b):
...
Result log (``--result-log``) Result log (``--result-log``)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@ -145,6 +116,55 @@ collection.
This issue should affect only advanced plugins who create new collection types, so if you see this warning This issue should affect only advanced plugins who create new collection types, so if you see this warning
message please contact the authors so they can change the code. message please contact the authors so they can change the code.
marks in ``pytest.mark.parametrize``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
*Removed in version 4.0.*
Applying marks to values of a ``pytest.mark.parametrize`` call is now deprecated. For example:
.. code-block:: python
@pytest.mark.parametrize(
"a, b",
[
(3, 9),
pytest.mark.xfail(reason="flaky")(6, 36),
(10, 100),
(20, 200),
(40, 400),
(50, 500),
],
)
def test_foo(a, b):
...
This code applies the ``pytest.mark.xfail(reason="flaky")`` mark to the ``(6, 36)`` value of the above parametrization
call.
This was considered hard to read and understand, and also its implementation presented problems to the code preventing
further internal improvements in the marks architecture.
To update the code, use ``pytest.param``:
.. code-block:: python
@pytest.mark.parametrize(
"a, b",
[
(3, 9),
pytest.param(6, 36, marks=pytest.mark.xfail(reason="flaky")),
(10, 100),
(20, 200),
(40, 400),
(50, 500),
],
)
def test_foo(a, b):
...
``pytest_funcarg__`` prefix ``pytest_funcarg__`` prefix
~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~

View File

@ -54,12 +54,6 @@ MARK_INFO_ATTRIBUTE = RemovedInPytest4Warning(
"Docs: https://docs.pytest.org/en/latest/mark.html#updating-code" "Docs: https://docs.pytest.org/en/latest/mark.html#updating-code"
) )
MARK_PARAMETERSET_UNPACKING = RemovedInPytest4Warning(
"Applying marks directly to parameters is deprecated,"
" please use pytest.param(..., marks=...) instead.\n"
"For more details, see: https://docs.pytest.org/en/latest/parametrize.html"
)
RAISES_EXEC = PytestDeprecationWarning( RAISES_EXEC = PytestDeprecationWarning(
"raises(..., 'code(as_a_string)') is deprecated, use the context manager form or use `exec()` directly\n\n" "raises(..., 'code(as_a_string)') is deprecated, use the context manager form or use `exec()` directly\n\n"
"See https://docs.pytest.org/en/latest/deprecations.html#raises-warns-exec" "See https://docs.pytest.org/en/latest/deprecations.html#raises-warns-exec"

View File

@ -13,7 +13,6 @@ from ..compat import getfslineno
from ..compat import MappingMixin from ..compat import MappingMixin
from ..compat import NOTSET from ..compat import NOTSET
from ..deprecated import MARK_INFO_ATTRIBUTE from ..deprecated import MARK_INFO_ATTRIBUTE
from ..deprecated import MARK_PARAMETERSET_UNPACKING
from _pytest.outcomes import fail from _pytest.outcomes import fail
@ -82,39 +81,23 @@ class ParameterSet(namedtuple("ParameterSet", "values, marks, id")):
return cls(values, marks, id_) return cls(values, marks, id_)
@classmethod @classmethod
def extract_from(cls, parameterset, belonging_definition, legacy_force_tuple=False): def extract_from(cls, parameterset, force_tuple=False):
""" """
:param parameterset: :param parameterset:
a legacy style parameterset that may or may not be a tuple, 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 and may or may not be wrapped into a mess of mark objects
:param legacy_force_tuple: :param force_tuple:
enforce tuple wrapping so single argument tuple values enforce tuple wrapping so single argument tuple values
don't get decomposed and break tests don't get decomposed and break tests
:param belonging_definition: the item that we will be extracting the parameters from.
""" """
if isinstance(parameterset, cls): if isinstance(parameterset, cls):
return parameterset return parameterset
if not isinstance(parameterset, MarkDecorator) and legacy_force_tuple: if force_tuple:
return cls.param(parameterset) return cls.param(parameterset)
else:
newmarks = [] return cls(parameterset, marks=[], id=None)
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,)
if newmarks and belonging_definition is not None:
belonging_definition.warn(MARK_PARAMETERSET_UNPACKING)
return cls(argval, marks=newmarks, id=None)
@classmethod @classmethod
def _for_parametrize(cls, argnames, argvalues, func, config, function_definition): def _for_parametrize(cls, argnames, argvalues, func, config, function_definition):
@ -124,12 +107,7 @@ class ParameterSet(namedtuple("ParameterSet", "values, marks, id")):
else: else:
force_tuple = False force_tuple = False
parameters = [ parameters = [
ParameterSet.extract_from( ParameterSet.extract_from(x, force_tuple=force_tuple) for x in argvalues
x,
legacy_force_tuple=force_tuple,
belonging_definition=function_definition,
)
for x in argvalues
] ]
del argvalues del argvalues
@ -137,11 +115,21 @@ class ParameterSet(namedtuple("ParameterSet", "values, marks, id")):
# check all parameter sets have the correct number of values # check all parameter sets have the correct number of values
for param in parameters: for param in parameters:
if len(param.values) != len(argnames): if len(param.values) != len(argnames):
raise ValueError( msg = (
'In "parametrize" the number of values ({}) must be ' '{nodeid}: in "parametrize" the number of names ({names_len}):\n'
"equal to the number of names ({})".format( " {names}\n"
param.values, argnames "must be equal to the number of values ({values_len}):\n"
) " {values}"
)
fail(
msg.format(
nodeid=function_definition.nodeid,
values=param.values,
names=argnames,
names_len=len(argnames),
values_len=len(param.values),
),
pytrace=False,
) )
else: else:
# empty parameter set (likely computed at runtime): create a single # empty parameter set (likely computed at runtime): create a single

View File

@ -244,13 +244,6 @@ class TestClass(object):
"ignore:usage of Generator.Function is deprecated, please use pytest.Function instead" "ignore:usage of Generator.Function is deprecated, please use pytest.Function instead"
) )
class TestFunction(object): class TestFunction(object):
@pytest.fixture
def ignore_parametrized_marks_args(self):
"""Provides arguments to pytester.runpytest() to ignore the warning about marks being applied directly
to parameters.
"""
return ("-W", "ignore:Applying marks directly to parameters")
def test_getmodulecollector(self, testdir): def test_getmodulecollector(self, testdir):
item = testdir.getitem("def test_func(): pass") item = testdir.getitem("def test_func(): pass")
modcol = item.getparent(pytest.Module) modcol = item.getparent(pytest.Module)
@ -472,7 +465,6 @@ class TestFunction(object):
rec = testdir.inline_run() rec = testdir.inline_run()
rec.assertoutcome(passed=1) rec.assertoutcome(passed=1)
@pytest.mark.filterwarnings("ignore:Applying marks directly to parameters")
def test_parametrize_with_mark(self, testdir): def test_parametrize_with_mark(self, testdir):
items = testdir.getitems( items = testdir.getitems(
""" """
@ -480,7 +472,7 @@ class TestFunction(object):
@pytest.mark.foo @pytest.mark.foo
@pytest.mark.parametrize('arg', [ @pytest.mark.parametrize('arg', [
1, 1,
pytest.mark.bar(pytest.mark.baz(2)) pytest.param(2, marks=[pytest.mark.baz, pytest.mark.bar])
]) ])
def test_function(arg): def test_function(arg):
pass pass
@ -558,37 +550,37 @@ class TestFunction(object):
assert colitems[2].name == "test2[a-c]" assert colitems[2].name == "test2[a-c]"
assert colitems[3].name == "test2[b-c]" assert colitems[3].name == "test2[b-c]"
def test_parametrize_skipif(self, testdir, ignore_parametrized_marks_args): def test_parametrize_skipif(self, testdir):
testdir.makepyfile( testdir.makepyfile(
""" """
import pytest import pytest
m = pytest.mark.skipif('True') m = pytest.mark.skipif('True')
@pytest.mark.parametrize('x', [0, 1, m(2)]) @pytest.mark.parametrize('x', [0, 1, pytest.param(2, marks=m)])
def test_skip_if(x): def test_skip_if(x):
assert x < 2 assert x < 2
""" """
) )
result = testdir.runpytest(*ignore_parametrized_marks_args) result = testdir.runpytest()
result.stdout.fnmatch_lines("* 2 passed, 1 skipped in *") result.stdout.fnmatch_lines("* 2 passed, 1 skipped in *")
def test_parametrize_skip(self, testdir, ignore_parametrized_marks_args): def test_parametrize_skip(self, testdir):
testdir.makepyfile( testdir.makepyfile(
""" """
import pytest import pytest
m = pytest.mark.skip('') m = pytest.mark.skip('')
@pytest.mark.parametrize('x', [0, 1, m(2)]) @pytest.mark.parametrize('x', [0, 1, pytest.param(2, marks=m)])
def test_skip(x): def test_skip(x):
assert x < 2 assert x < 2
""" """
) )
result = testdir.runpytest(*ignore_parametrized_marks_args) result = testdir.runpytest()
result.stdout.fnmatch_lines("* 2 passed, 1 skipped in *") result.stdout.fnmatch_lines("* 2 passed, 1 skipped in *")
def test_parametrize_skipif_no_skip(self, testdir, ignore_parametrized_marks_args): def test_parametrize_skipif_no_skip(self, testdir):
testdir.makepyfile( testdir.makepyfile(
""" """
import pytest import pytest
@ -600,40 +592,40 @@ class TestFunction(object):
assert x < 2 assert x < 2
""" """
) )
result = testdir.runpytest(*ignore_parametrized_marks_args) result = testdir.runpytest()
result.stdout.fnmatch_lines("* 1 failed, 2 passed in *") result.stdout.fnmatch_lines("* 1 failed, 2 passed in *")
def test_parametrize_xfail(self, testdir, ignore_parametrized_marks_args): def test_parametrize_xfail(self, testdir):
testdir.makepyfile( testdir.makepyfile(
""" """
import pytest import pytest
m = pytest.mark.xfail('True') m = pytest.mark.xfail('True')
@pytest.mark.parametrize('x', [0, 1, m(2)]) @pytest.mark.parametrize('x', [0, 1, pytest.param(2, marks=m)])
def test_xfail(x): def test_xfail(x):
assert x < 2 assert x < 2
""" """
) )
result = testdir.runpytest(*ignore_parametrized_marks_args) result = testdir.runpytest()
result.stdout.fnmatch_lines("* 2 passed, 1 xfailed in *") result.stdout.fnmatch_lines("* 2 passed, 1 xfailed in *")
def test_parametrize_passed(self, testdir, ignore_parametrized_marks_args): def test_parametrize_passed(self, testdir):
testdir.makepyfile( testdir.makepyfile(
""" """
import pytest import pytest
m = pytest.mark.xfail('True') m = pytest.mark.xfail('True')
@pytest.mark.parametrize('x', [0, 1, m(2)]) @pytest.mark.parametrize('x', [0, 1, pytest.param(2, marks=m)])
def test_xfail(x): def test_xfail(x):
pass pass
""" """
) )
result = testdir.runpytest(*ignore_parametrized_marks_args) result = testdir.runpytest()
result.stdout.fnmatch_lines("* 2 passed, 1 xpassed in *") result.stdout.fnmatch_lines("* 2 passed, 1 xpassed in *")
def test_parametrize_xfail_passed(self, testdir, ignore_parametrized_marks_args): def test_parametrize_xfail_passed(self, testdir):
testdir.makepyfile( testdir.makepyfile(
""" """
import pytest import pytest
@ -645,7 +637,7 @@ class TestFunction(object):
pass pass
""" """
) )
result = testdir.runpytest(*ignore_parametrized_marks_args) result = testdir.runpytest()
result.stdout.fnmatch_lines("* 3 passed in *") result.stdout.fnmatch_lines("* 3 passed in *")
def test_function_original_name(self, testdir): def test_function_original_name(self, testdir):

View File

@ -1372,7 +1372,6 @@ class TestMetafuncFunctionalAuto(object):
assert output.count("preparing foo-3") == 1 assert output.count("preparing foo-3") == 1
@pytest.mark.filterwarnings("ignore:Applying marks directly to parameters")
@pytest.mark.issue308 @pytest.mark.issue308
class TestMarkersWithParametrization(object): class TestMarkersWithParametrization(object):
def test_simple_mark(self, testdir): def test_simple_mark(self, testdir):
@ -1382,7 +1381,7 @@ class TestMarkersWithParametrization(object):
@pytest.mark.foo @pytest.mark.foo
@pytest.mark.parametrize(("n", "expected"), [ @pytest.mark.parametrize(("n", "expected"), [
(1, 2), (1, 2),
pytest.mark.bar((1, 3)), pytest.param(1, 3, marks=pytest.mark.bar),
(2, 3), (2, 3),
]) ])
def test_increment(n, expected): def test_increment(n, expected):
@ -1402,7 +1401,7 @@ class TestMarkersWithParametrization(object):
@pytest.mark.parametrize(("n", "expected"), [ @pytest.mark.parametrize(("n", "expected"), [
(1, 2), (1, 2),
pytest.mark.foo((2, 3)), pytest.param(2, 3, marks=pytest.mark.foo),
(3, 4), (3, 4),
]) ])
def test_increment(n, expected): def test_increment(n, expected):
@ -1442,7 +1441,7 @@ class TestMarkersWithParametrization(object):
@pytest.mark.parametrize(("n", "expected"), [ @pytest.mark.parametrize(("n", "expected"), [
(1, 2), (1, 2),
pytest.mark.xfail((1, 3)), pytest.param(1, 3, marks=pytest.mark.xfail),
(2, 3), (2, 3),
]) ])
def test_increment(n, expected): def test_increment(n, expected):
@ -1459,7 +1458,7 @@ class TestMarkersWithParametrization(object):
@pytest.mark.parametrize("n", [ @pytest.mark.parametrize("n", [
2, 2,
pytest.mark.xfail(3), pytest.param(3, marks=pytest.mark.xfail),
4, 4,
]) ])
def test_isEven(n): def test_isEven(n):
@ -1475,7 +1474,7 @@ class TestMarkersWithParametrization(object):
@pytest.mark.parametrize(("n", "expected"), [ @pytest.mark.parametrize(("n", "expected"), [
(1, 2), (1, 2),
pytest.mark.xfail("True")((1, 3)), pytest.param(1, 3, marks=pytest.mark.xfail("True")),
(2, 3), (2, 3),
]) ])
def test_increment(n, expected): def test_increment(n, expected):
@ -1491,7 +1490,7 @@ class TestMarkersWithParametrization(object):
@pytest.mark.parametrize(("n", "expected"), [ @pytest.mark.parametrize(("n", "expected"), [
(1, 2), (1, 2),
pytest.mark.xfail(reason="some bug")((1, 3)), pytest.param(1, 3, marks=pytest.mark.xfail(reason="some bug")),
(2, 3), (2, 3),
]) ])
def test_increment(n, expected): def test_increment(n, expected):
@ -1507,7 +1506,7 @@ class TestMarkersWithParametrization(object):
@pytest.mark.parametrize(("n", "expected"), [ @pytest.mark.parametrize(("n", "expected"), [
(1, 2), (1, 2),
pytest.mark.xfail("True", reason="some bug")((1, 3)), pytest.param(1, 3, marks=pytest.mark.xfail("True", reason="some bug")),
(2, 3), (2, 3),
]) ])
def test_increment(n, expected): def test_increment(n, expected):
@ -1522,9 +1521,11 @@ class TestMarkersWithParametrization(object):
s = """ s = """
import pytest import pytest
m = pytest.mark.xfail("sys.version_info > (0, 0, 0)", reason="some bug", strict={strict})
@pytest.mark.parametrize(("n", "expected"), [ @pytest.mark.parametrize(("n", "expected"), [
(1, 2), (1, 2),
pytest.mark.xfail("sys.version_info > (0, 0, 0)", reason="some bug", strict={strict})((2, 3)), pytest.param(2, 3, marks=m),
(3, 4), (3, 4),
]) ])
def test_increment(n, expected): def test_increment(n, expected):
@ -1548,7 +1549,7 @@ class TestMarkersWithParametrization(object):
failingTestData = [(1, 3), failingTestData = [(1, 3),
(2, 2)] (2, 2)]
testData = passingTestData + [pytest.mark.xfail(d) testData = passingTestData + [pytest.param(*d, marks=pytest.mark.xfail)
for d in failingTestData] for d in failingTestData]
metafunc.parametrize(("n", "expected"), testData) metafunc.parametrize(("n", "expected"), testData)

View File

@ -10,7 +10,6 @@ import six
import pytest import pytest
from _pytest.mark import EMPTY_PARAMETERSET_OPTION from _pytest.mark import EMPTY_PARAMETERSET_OPTION
from _pytest.mark import MarkGenerator as Mark from _pytest.mark import MarkGenerator as Mark
from _pytest.mark import ParameterSet
from _pytest.mark import transfer_markers from _pytest.mark import transfer_markers
from _pytest.nodes import Collector from _pytest.nodes import Collector
from _pytest.nodes import Node from _pytest.nodes import Node
@ -477,8 +476,10 @@ def test_parametrized_collect_with_wrong_args(testdir):
result = testdir.runpytest(py_file) result = testdir.runpytest(py_file)
result.stdout.fnmatch_lines( result.stdout.fnmatch_lines(
[ [
'E ValueError: In "parametrize" the number of values ((1, 2, 3)) ' 'test_parametrized_collect_with_wrong_args.py::test_func: in "parametrize" the number of names (2):',
"must be equal to the number of names (['foo', 'bar'])" " ['foo', 'bar']",
"must be equal to the number of values (3):",
" (1, 2, 3)",
] ]
) )
@ -1042,36 +1043,6 @@ class TestKeywordSelection(object):
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
),
),
],
)
@pytest.mark.filterwarnings("default")
def test_parameterset_extractfrom(argval, expected):
from _pytest.deprecated import MARK_PARAMETERSET_UNPACKING
warn_called = []
class DummyItem:
def warn(self, warning):
warn_called.append(warning)
extracted = ParameterSet.extract_from(argval, belonging_definition=DummyItem())
assert extracted == expected
assert warn_called == [MARK_PARAMETERSET_UNPACKING]
def test_legacy_transfer(): def test_legacy_transfer():
class FakeModule(object): class FakeModule(object):
pytestmark = [] pytestmark = []