From 6fc8c29da0f1803c8707f6759e243104004beb08 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 5 Oct 2026 08:14:42 +0000 Subject: [PATCH] subtests: send str subclass messages as plain str A str subclass passed as the subtest msg, such as a StrEnum member, was stored on the report as-is. execnet serializes by exact type, so pytest-xdist failed with "DumpError: can't serialize ". Convert such messages with str.__str__ in SubtestContext, next to the existing saferepr handling of kwargs. str.__str__ keeps the string data; str() on a (str, Enum) mixin would give "Cls.MEMBER". Refs pytest-dev/pytest-xdist#1161. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_014AXM2385U5x4VaR1ppeo4X --- changelog/15134.bugfix.rst | 1 + src/_pytest/subtests.py | 6 ++++++ testing/test_subtests.py | 28 +++++++++++++++++++++++++++- 3 files changed, 34 insertions(+), 1 deletion(-) create mode 100644 changelog/15134.bugfix.rst diff --git a/changelog/15134.bugfix.rst b/changelog/15134.bugfix.rst new file mode 100644 index 00000000000..2a231460478 --- /dev/null +++ b/changelog/15134.bugfix.rst @@ -0,0 +1 @@ +Fixed ``DumpError`` from ``pytest-xdist`` when a ``str`` subclass (such as a :class:`~enum.StrEnum` member) is passed as the ``msg`` of :fixture:`subtests` -- the message is now converted to a plain ``str``. diff --git a/src/_pytest/subtests.py b/src/_pytest/subtests.py index 1a908a15e8f..ea0d9af6573 100644 --- a/src/_pytest/subtests.py +++ b/src/_pytest/subtests.py @@ -67,6 +67,12 @@ def __post_init__(self) -> None: object.__setattr__( self, "kwargs", {k: saferepr(v) for (k, v) in self.kwargs.items()} ) + # Ensure msg is a plain str: str subclasses such as StrEnum members + # cannot be serialized by execnet (pytest-dev/pytest-xdist#1161). + # str.__str__ keeps the string data, while str() on a (str, Enum) + # mixin would return "Cls.MEMBER". + if isinstance(self.msg, str) and type(self.msg) is not str: + object.__setattr__(self, "msg", str.__str__(self.msg)) def _to_json(self) -> dict[str, Any]: result = dataclasses.asdict(self) diff --git a/testing/test_subtests.py b/testing/test_subtests.py index 877e32b5204..822d6f631b7 100644 --- a/testing/test_subtests.py +++ b/testing/test_subtests.py @@ -1048,6 +1048,25 @@ def test_serialization() -> None: ) +class MyStrEnum(str, Enum): + B = "B" + + +@pytest.mark.parametrize( + ("msg", "expected"), + [ + pytest.param(MyStrEnum.B, "B", id="str-subclass"), + pytest.param("plain", "plain", id="str"), + pytest.param(None, None, id="none"), + ], +) +def test_msg_is_plain_str(msg: str | None, expected: str | None) -> None: + """Ensure subtest's msg is a plain str, which execnet can serialize (pytest-dev/pytest-xdist#1161).""" + context = SubtestContext(msg=msg, kwargs={}) + assert context.msg == expected + assert type(context.msg) is type(expected) + + def test_serialization_xdist(pytester: pytest.Pytester) -> None: # pragma: no cover """Regression test for pytest-dev/pytest-xdist#1273.""" pytest.importorskip("xdist") @@ -1059,10 +1078,17 @@ def test_serialization_xdist(pytester: pytest.Pytester) -> None: # pragma: no c class MyEnum(Enum): A = "A" + class MyStrEnum(str, Enum): + B = "B" + def test(subtests): with subtests.test(a=MyEnum.A): pass + def test_str_subclass_msg(subtests): + with subtests.test(msg=MyStrEnum.B): + pass + class T(unittest.TestCase): def test(self): @@ -1072,4 +1098,4 @@ def test(self): ) pytester.syspathinsert() result = pytester.runpytest("-n1", "-pxdist.plugin") - result.assert_outcomes(passed=2) + result.assert_outcomes(passed=3)