From ebfdb5fe28cf10e0f70619bc4c7852b9f3e041a4 Mon Sep 17 00:00:00 2001 From: Vladimir Gorkavenko Date: Sun, 31 Oct 2021 11:57:48 +0400 Subject: [PATCH] Implement unpacking nested pytest.param objects - Merge nested ids (if parent pytest.param don't have id) - Merge nested marks with parent --- AUTHORS | 1 + changelog/9074.improvement.rst | 17 ++++++++++ src/_pytest/python.py | 25 ++++++++++++--- testing/test_mark.py | 57 +++++++++++++++++++++++++++++++++- 4 files changed, 95 insertions(+), 5 deletions(-) create mode 100644 changelog/9074.improvement.rst diff --git a/AUTHORS b/AUTHORS index 153375dca..9ef30ddb2 100644 --- a/AUTHORS +++ b/AUTHORS @@ -334,6 +334,7 @@ Virgil Dupras Vitaly Lashmanov Vlad Dragos Vlad Radziuk +Vladimir Gorkavenko Vladyslav Rachek Volodymyr Piskun Wei Lin diff --git a/changelog/9074.improvement.rst b/changelog/9074.improvement.rst new file mode 100644 index 000000000..21f18905b --- /dev/null +++ b/changelog/9074.improvement.rst @@ -0,0 +1,17 @@ +Now ``pytest`` do unpack nested ``pytest.param`` values and merge their ids and marks. +If parent ``pytest.param`` has an id - it rewrites nested ids. +For example, now such a construction is possible:: + + @pytest.mark.parametrize( + "a,b,c", [ + pytest.param( + pytest.param(1, id="one", marks=pytest.mark.one), + pytest.param(2, id="two", marks=pytest.mark.two), + pytest.param(3, id="three", marks=pytest.mark.three), + marks=pytest.mark.full + ) + ] + ) + + +Earlier in this case the values ``a, b, c`` in test are a ``ParameterSet`` objects \ No newline at end of file diff --git a/src/_pytest/python.py b/src/_pytest/python.py index 8acef2539..73ba4cfa1 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -1126,9 +1126,21 @@ class Metafunc: # Create the new calls: if we are parametrize() multiple times (by applying the decorator # more than once) then we accumulate those calls generating the cartesian product # of all calls. + # If we parametrize the test using values that contained nested pytest.param objects, + # then we unpack them values and merge marks. newcalls = [] for callspec in self._calls or [CallSpec2()]: for param_index, (param_id, param_set) in enumerate(zip(ids, parameters)): + normalized_values = [] + merged_marks = list(param_set.marks) if param_set.marks else [] + for param_value in param_set.values: + if not isinstance(param_value, ParameterSet): + normalized_values.append(param_value) + else: + normalized_values.append(*param_value.values) + if param_value.marks: + merged_marks.append(*param_value.marks) + param_set = ParameterSet(values=tuple(normalized_values), marks=merged_marks, id=None) newcallspec = callspec.setmulti( valtypes=arg_values_types, argnames=argnames, @@ -1385,10 +1397,15 @@ def _idvalset( return parameterset.id id = None if ids is None or idx >= len(ids) else ids[idx] if id is None: - this_id = [ - _idval(val, argname, idx, idfn, nodeid=nodeid, config=config) - for val, argname in zip(parameterset.values, argnames) - ] + this_id = [] + for val, argname in zip(parameterset.values, argnames): + to_get_idval = val + if isinstance(val, ParameterSet): + if val.id: + this_id.append(val.id) + continue + to_get_idval = (val.values[0] if len(val.values) == 1 else val.values) + this_id.append(_idval(to_get_idval, argname, idx, idfn, nodeid=nodeid, config=config)) return "-".join(this_id) else: return _ascii_escaped_by_config(id, config) diff --git a/testing/test_mark.py b/testing/test_mark.py index ce65e7d56..5d1b9d71f 100644 --- a/testing/test_mark.py +++ b/testing/test_mark.py @@ -1,6 +1,6 @@ import os import sys -from typing import List +from typing import List, NamedTuple, Tuple, Any from typing import Optional from unittest import mock @@ -344,6 +344,61 @@ def test_parametrize_with_module(pytester: Pytester) -> None: assert passed[0].nodeid.split("::")[-1] == expected_id +@pytest.fixture() +def pytester_internal_test_values(): + pytest.internal_test_values = [] # type: ignore[attr-defined] + yield pytest.internal_test_values # type: ignore[attr-defined] + del pytest.internal_test_values # type: ignore[attr-defined] + + +def test_parametrize_with_nested_paramsets(pytester: Pytester, pytester_internal_test_values: List[Any]) -> None: + """Test parametrize with nested pytest.param objects in value""" + case = NamedTuple( + "case", + [ + ("expected_test_id", str), + ("expected_value", Tuple[Any, ...]), + ("expected_marks", Tuple[str, ...]), + ], + ) + cases = ( + case("1-nested_2-3", (1, 2, 3), ("parametrize",)), + case("one_two_three", (1, 2, 3), ("parametrize",)), + case("1-nested_2-tuple_value", (1, 2, (3, 3)), ("parametrize",)), + case("1-2-3", (1, 2, 3), ("parametrize", "a", "b", "c", "all",)), + ) + pytester.makepyfile( + """ + import pytest + @pytest.mark.parametrize( + "a, b, c", + [ + (1, pytest.param(2, id="nested_2"), pytest.param(3)), + pytest.param(1, pytest.param(2, id="nested_2"), pytest.param(3), id="one_two_three"), + (1, pytest.param(2, id="nested_2"), pytest.param((3, 3), id="tuple_value")), + pytest.param( + pytest.param(1, marks=pytest.mark.a), + pytest.param(2, marks=pytest.mark.b), + pytest.param(3, marks=pytest.mark.c), + marks=pytest.mark.all + ), + ] + ) + def test_func(a, b, c): + pytest.internal_test_values.append((a, b, c)) + """ + ) + rec = pytester.inline_run() + items = [x.item for x in rec.getcalls("pytest_itemcollected")] + passed, _, _ = rec.listoutcomes() + for i, case_ in enumerate(cases): + markers = {m.name for m in (items[i].iter_markers() or ())} + expected_id = "test_func[" + case_.expected_test_id + "]" + assert markers == set(case_.expected_marks) + assert passed[i].nodeid.split("::")[-1] == expected_id + assert pytester_internal_test_values[i] == case_.expected_value + + @pytest.mark.parametrize( ("expr", "expected_error"), [