diff --git a/changelog/3082.removal.rst b/changelog/3082.removal.rst new file mode 100644 index 000000000..750f097bc --- /dev/null +++ b/changelog/3082.removal.rst @@ -0,0 +1,3 @@ +Removed support for applying marks directly to values in ``@pytest.mark.parametrize``. Use ``pytest.param`` instead. + +See our `docs `__ on information on how to update your code. diff --git a/doc/en/deprecations.rst b/doc/en/deprecations.rst index ca95bab38..dc716951b 100644 --- a/doc/en/deprecations.rst +++ b/doc/en/deprecations.rst @@ -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 should be used instead. +.. contents:: + :depth: 3 + :local: + + Deprecated Features ------------------- @@ -81,40 +86,6 @@ As part of a large :ref:`marker-revamp`, :meth:`_pytest.nodes.Node.get_marker` i :ref:`the documentation ` 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``) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -145,6 +116,55 @@ collection. 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. + +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 ~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/src/_pytest/deprecated.py b/src/_pytest/deprecated.py index 02b26ba77..494a453b6 100644 --- a/src/_pytest/deprecated.py +++ b/src/_pytest/deprecated.py @@ -54,12 +54,6 @@ MARK_INFO_ATTRIBUTE = RemovedInPytest4Warning( "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(..., '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" diff --git a/src/_pytest/mark/structures.py b/src/_pytest/mark/structures.py index 14a684745..18fb6fa6d 100644 --- a/src/_pytest/mark/structures.py +++ b/src/_pytest/mark/structures.py @@ -13,7 +13,6 @@ from ..compat import getfslineno from ..compat import MappingMixin from ..compat import NOTSET from ..deprecated import MARK_INFO_ATTRIBUTE -from ..deprecated import MARK_PARAMETERSET_UNPACKING from _pytest.outcomes import fail @@ -82,39 +81,23 @@ class ParameterSet(namedtuple("ParameterSet", "values, marks, id")): return cls(values, marks, id_) @classmethod - def extract_from(cls, parameterset, belonging_definition, legacy_force_tuple=False): + def extract_from(cls, parameterset, 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: + :param force_tuple: enforce tuple wrapping so single argument tuple values don't get decomposed and break tests - - :param belonging_definition: the item that we will be extracting the parameters from. """ if isinstance(parameterset, cls): return parameterset - if not isinstance(parameterset, MarkDecorator) and legacy_force_tuple: + if 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,) - - if newmarks and belonging_definition is not None: - belonging_definition.warn(MARK_PARAMETERSET_UNPACKING) - - return cls(argval, marks=newmarks, id=None) + else: + return cls(parameterset, marks=[], id=None) @classmethod def _for_parametrize(cls, argnames, argvalues, func, config, function_definition): @@ -124,12 +107,7 @@ class ParameterSet(namedtuple("ParameterSet", "values, marks, id")): else: force_tuple = False parameters = [ - ParameterSet.extract_from( - x, - legacy_force_tuple=force_tuple, - belonging_definition=function_definition, - ) - for x in argvalues + ParameterSet.extract_from(x, force_tuple=force_tuple) for x in argvalues ] del argvalues @@ -137,11 +115,21 @@ class ParameterSet(namedtuple("ParameterSet", "values, marks, id")): # check all parameter sets have the correct number of values for param in parameters: if len(param.values) != len(argnames): - raise ValueError( - 'In "parametrize" the number of values ({}) must be ' - "equal to the number of names ({})".format( - param.values, argnames - ) + msg = ( + '{nodeid}: in "parametrize" the number of names ({names_len}):\n' + " {names}\n" + "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: # empty parameter set (likely computed at runtime): create a single diff --git a/testing/python/collect.py b/testing/python/collect.py index 53b3bc18b..3147ee9e2 100644 --- a/testing/python/collect.py +++ b/testing/python/collect.py @@ -244,13 +244,6 @@ class TestClass(object): "ignore:usage of Generator.Function is deprecated, please use pytest.Function instead" ) 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): item = testdir.getitem("def test_func(): pass") modcol = item.getparent(pytest.Module) @@ -472,7 +465,6 @@ class TestFunction(object): rec = testdir.inline_run() rec.assertoutcome(passed=1) - @pytest.mark.filterwarnings("ignore:Applying marks directly to parameters") def test_parametrize_with_mark(self, testdir): items = testdir.getitems( """ @@ -480,7 +472,7 @@ class TestFunction(object): @pytest.mark.foo @pytest.mark.parametrize('arg', [ 1, - pytest.mark.bar(pytest.mark.baz(2)) + pytest.param(2, marks=[pytest.mark.baz, pytest.mark.bar]) ]) def test_function(arg): pass @@ -558,37 +550,37 @@ class TestFunction(object): assert colitems[2].name == "test2[a-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( """ import pytest 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): assert x < 2 """ ) - result = testdir.runpytest(*ignore_parametrized_marks_args) + result = testdir.runpytest() 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( """ import pytest 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): assert x < 2 """ ) - result = testdir.runpytest(*ignore_parametrized_marks_args) + result = testdir.runpytest() 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( """ import pytest @@ -600,40 +592,40 @@ class TestFunction(object): assert x < 2 """ ) - result = testdir.runpytest(*ignore_parametrized_marks_args) + result = testdir.runpytest() 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( """ import pytest 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): assert x < 2 """ ) - result = testdir.runpytest(*ignore_parametrized_marks_args) + result = testdir.runpytest() 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( """ import pytest 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): pass """ ) - result = testdir.runpytest(*ignore_parametrized_marks_args) + result = testdir.runpytest() 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( """ import pytest @@ -645,7 +637,7 @@ class TestFunction(object): pass """ ) - result = testdir.runpytest(*ignore_parametrized_marks_args) + result = testdir.runpytest() result.stdout.fnmatch_lines("* 3 passed in *") def test_function_original_name(self, testdir): diff --git a/testing/python/metafunc.py b/testing/python/metafunc.py index 0b05a7c5e..54a6ecb91 100644 --- a/testing/python/metafunc.py +++ b/testing/python/metafunc.py @@ -1372,7 +1372,6 @@ class TestMetafuncFunctionalAuto(object): assert output.count("preparing foo-3") == 1 -@pytest.mark.filterwarnings("ignore:Applying marks directly to parameters") @pytest.mark.issue308 class TestMarkersWithParametrization(object): def test_simple_mark(self, testdir): @@ -1382,7 +1381,7 @@ class TestMarkersWithParametrization(object): @pytest.mark.foo @pytest.mark.parametrize(("n", "expected"), [ (1, 2), - pytest.mark.bar((1, 3)), + pytest.param(1, 3, marks=pytest.mark.bar), (2, 3), ]) def test_increment(n, expected): @@ -1402,7 +1401,7 @@ class TestMarkersWithParametrization(object): @pytest.mark.parametrize(("n", "expected"), [ (1, 2), - pytest.mark.foo((2, 3)), + pytest.param(2, 3, marks=pytest.mark.foo), (3, 4), ]) def test_increment(n, expected): @@ -1442,7 +1441,7 @@ class TestMarkersWithParametrization(object): @pytest.mark.parametrize(("n", "expected"), [ (1, 2), - pytest.mark.xfail((1, 3)), + pytest.param(1, 3, marks=pytest.mark.xfail), (2, 3), ]) def test_increment(n, expected): @@ -1459,7 +1458,7 @@ class TestMarkersWithParametrization(object): @pytest.mark.parametrize("n", [ 2, - pytest.mark.xfail(3), + pytest.param(3, marks=pytest.mark.xfail), 4, ]) def test_isEven(n): @@ -1475,7 +1474,7 @@ class TestMarkersWithParametrization(object): @pytest.mark.parametrize(("n", "expected"), [ (1, 2), - pytest.mark.xfail("True")((1, 3)), + pytest.param(1, 3, marks=pytest.mark.xfail("True")), (2, 3), ]) def test_increment(n, expected): @@ -1491,7 +1490,7 @@ class TestMarkersWithParametrization(object): @pytest.mark.parametrize(("n", "expected"), [ (1, 2), - pytest.mark.xfail(reason="some bug")((1, 3)), + pytest.param(1, 3, marks=pytest.mark.xfail(reason="some bug")), (2, 3), ]) def test_increment(n, expected): @@ -1507,7 +1506,7 @@ class TestMarkersWithParametrization(object): @pytest.mark.parametrize(("n", "expected"), [ (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), ]) def test_increment(n, expected): @@ -1522,9 +1521,11 @@ class TestMarkersWithParametrization(object): s = """ import pytest + m = pytest.mark.xfail("sys.version_info > (0, 0, 0)", reason="some bug", strict={strict}) + @pytest.mark.parametrize(("n", "expected"), [ (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), ]) def test_increment(n, expected): @@ -1548,7 +1549,7 @@ class TestMarkersWithParametrization(object): failingTestData = [(1, 3), (2, 2)] - testData = passingTestData + [pytest.mark.xfail(d) + testData = passingTestData + [pytest.param(*d, marks=pytest.mark.xfail) for d in failingTestData] metafunc.parametrize(("n", "expected"), testData) diff --git a/testing/test_mark.py b/testing/test_mark.py index 8bf715995..4888b1c55 100644 --- a/testing/test_mark.py +++ b/testing/test_mark.py @@ -10,7 +10,6 @@ import six import pytest from _pytest.mark import EMPTY_PARAMETERSET_OPTION from _pytest.mark import MarkGenerator as Mark -from _pytest.mark import ParameterSet from _pytest.mark import transfer_markers from _pytest.nodes import Collector from _pytest.nodes import Node @@ -477,8 +476,10 @@ def test_parametrized_collect_with_wrong_args(testdir): result = testdir.runpytest(py_file) result.stdout.fnmatch_lines( [ - 'E ValueError: In "parametrize" the number of values ((1, 2, 3)) ' - "must be equal to the number of names (['foo', 'bar'])" + 'test_parametrized_collect_with_wrong_args.py::test_func: in "parametrize" the number of names (2):', + " ['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("()") -@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(): class FakeModule(object): pytestmark = []