Skip to content

Fix DumpError when a report value is a str subclass - #1389

Closed
DawnofGenX wants to merge 3 commits into
pytest-dev:masterfrom
DawnofGenX:fix-strenum-serialization-1161
Closed

DawnofGenX wants to merge 3 commits into
pytest-dev:masterfrom
DawnofGenX:fix-strenum-serialization-1161

Conversation

@DawnofGenX

@DawnofGenX DawnofGenX commented Sep 27, 2026 •

Copy link
Copy Markdown

Fix DumpError when a report value is a str subclass

Closes #1161.

execnet serializes by exact type, so a str subclass — notably a StrEnum
member, which is a str — raised

execnet.gateway_base.DumpError: can't serialize <enum 'MyEnum'>

when it crossed the worker→controller channel. The report payload can carry
such values; a pytest-subtests message is one real way in, and the issue's
reproducer (a StrEnum used as a subtest message) still fails on current
master:

$ pytest -n 2 repro.py
FAILED repro.py::test_something[0] - execnet.gateway_base.DumpError: can'...
FAILED repro.py::test_something[1] - execnet.gateway_base.DumpError: can'...
2 failed

The same test without -n passes.

The fix

WorkerInteractor.sendevent is the single choke point for worker→controller
sends, so str subclasses are coerced to plain str there. Three details that
are easy to get wrong, and that the tests below pin:

  • Containers are walked, since the offending value is usually nested (here in
    report._subtest.context['msg']).
  • Tuple subclasses are rebuilt field-by-field. type(value)(converted)
    raises TypeError: NT.__new__() missing 1 required positional argument on a
    namedtuple, and report location is one — my first version had exactly that
    bug and the unit test caught it.
  • str.__str__(value), not str(value). A class mixing str with
    enum.Enum inherits Enum.__str__, so str(X.RED) is "X.RED" — the value
    would have been replaced by its repr. The f-string form is not usable either,
    since it differs between 3.11 and 3.12.

Values execnet genuinely cannot serialize — a non-str Enum, a set — are
passed through untouched, so their own error still surfaces instead of being
masked by a lossy conversion.

Tests

testing/test_remote.py — unit tests for the conversion: str subclass,
nested dict/list/tuple, the str()-vs-str.__str__ trap, namedtuple round-trip,
and the pass-through cases.

testing/acceptance_test.py — two end-to-end tests under -n 2: a report
carrying a str-subclass value now passes, and a report carrying a truly
unserializable value still fails the run, so the coercion cannot mask real bugs.

Both acceptance tests fail on master and pass here.

Verified on CPython 3.9, 3.10 and 3.12: 11 targeted tests pass, and the full
suite shows the same 17 pre-existing failures as master (TestLocking,
test_looponfail, test_remote.TestWorkerInteractor::test_basic_collect_and_runtests),
with 7 new tests added and no new failures.

The py311-pytestmain job also fails, but identically on master and on this
branch (TestGroupScope, 5 tests, caused by pytest main) — not from this
change.

execnet serializes by exact type, so a str subclass -- notably a StrEnum
member, which *is* a str -- raised ``DumpError: can't serialize <enum
'...' >`` when it crossed the worker->controller channel. Report payloads
can carry such values; a pytest-subtests message is one real way in.

Coerce str subclasses to plain str in ``WorkerInteractor.sendevent``, the
single choke point for worker->controller sends. Containers are walked so
nested values are converted too. Tuple subclasses are rebuilt field-by-field
(``type(value)(converted)`` would raise TypeError on a namedtuple, and report
``location`` is one), and anything execnet genuinely cannot handle -- a
non-str Enum, a set -- is still passed through so its own error surfaces
rather than being masked.

Fixes pytest-dev#1161.
Two problems with the previous commit:

- ``enum.StrEnum`` is 3.11+, and xdist supports 3.9, so the tests failed at
  import on the whole 3.9/3.10 CI matrix. The condition under test is "a str
  subclass", so the fixtures use ``class X(str, enum.Enum)`` instead, which
  exists on every supported version.
- ``str()`` is the wrong conversion for such a class: mixing ``str`` with
  ``enum.Enum`` gives ``Enum.__str__``, so ``str(X.RED)`` is
  ``"X.RED"`` -- the value would have been replaced by its repr. Use
  ``str.__str__``, which always yields the underlying string data. (The
  f-string form differs between 3.11 and 3.12, so it cannot be used here
  either.) Covered by a test asserting ``str(_Colour.RED) != "red"``.

Verified on CPython 3.9, 3.10 and 3.12.
pre-commit's mypy (strict, warn_unreachable) rejected three things:

- a generator passed where a sequence was required, for the splatted
  namedtuple reconstruction;
- ``type(value)(...)`` on a ``type[tuple]``, which mypy types as taking
  a single iterable. A namedtuple's __new__ takes positional fields, so
  a cast to Any is the accurate description of the call;
- now-unnecessary ``type: ignore`` comments in the tests.

The list and tuple branches are also split, which removes the shared
list/tuple special case entirely.
@DawnofGenX

Copy link
Copy Markdown
Author

The two failing jobs (py311-pytestmain on ubuntu + windows) are not caused by this PR. Bisected it down to a pytest API change on main, and this is already tracked in #1386, so I am not opening a duplicate.

What fails

testing/acceptance_test.py::TestGroupScope::test_by_module
testing/acceptance_test.py::TestGroupScope::test_by_class
testing/acceptance_test.py::TestGroupScope::test_module_single_start
testing/acceptance_test.py::TestGroupScope::test_multiple_group_marks
testing/acceptance_test.py::TestGroupScope::test_multiple_group_order

These live at lines 1449-1600. This PR only appends to testing/acceptance_test.py at line 1757 (two new tests), so the diff does not touch them. But "I did not touch it" is weak evidence, so I reproduced it.

Reproduction

Cloned this branch and ran TestGroupScope under both pytest versions the matrix uses:

pytest 8.4.2 pytest main (9.2.0.dev348)
this branch 6 passed 5 failed
master, no change - 5 failed

Master fails identically, so the branch is not the trigger. The only job that fails is the one pinned to pytest built from main; every released-pin job is green.

Cause

pytest turned Node.nodeid into a read-only property:

@property
def nodeid(self) -> str:
    return str(self._id)

There is no setter. src/xdist/remote.py:264 still does:

item._nodeid = f"{item.nodeid}@{'_'.join(sorted(gnames))}"

That write now lands on an attribute nothing reads, so the @group suffix is never appended. Because the assignment does not raise, --dist=loadgroup silently stops grouping and the tests above fail on their assert lines instead. Confirmed with a sentinel in a scratch run:

item._nodeid = nodeid + "@SENTINEL"   -> test_a.py::test_1            (ignored)
item._id    = nodeid + "@SENTINEL"    -> test_a.py::test_1@SENTINEL   (works)

Asking before touching anything

The one-line fix is _nodeid -> _id in remote.py, but that is the #1386 fix and it is not in scope for a DumpError bugfix, so I would rather not bundle it in here and make the diff harder to review.

How would you like to handle it? Options as I see them:

  1. Merge the required-checks failure as-is and fix --dist=loadgroup silently stops grouping on pytest main: the nodeid mutation no longer takes effect #1386 separately, on its own PR.
  2. I open a separate PR against --dist=loadgroup silently stops grouping on pytest main: the nodeid mutation no longer takes effect #1386 and leave this one alone.
  3. Maintainer preference - I am happy either way.

For reference, the 22 passing checks here do include the two new tests from this PR, so the str subclass fix itself is behaving as intended.

@DawnofGenX

Copy link
Copy Markdown
Author

Closing due to inactivity — 6d idle, blocked upstream. Feel free to reopen if still wanted.

@DawnofGenX DawnofGenX closed this Oct 4, 2026

Copy link
Copy Markdown
Member

🤖 Written by Claude Opus 5.5 via Claude Code for the pytest-xdist maintainers; I prompted it, it did the work, I read it.

Thanks for the investigation. As I understand it, the right place for this fix is pytest, not xdist. pytest requires reports to be serializable (see the discussion in #1273), and execnet's serializer is intentionally limited to exact builtin types, so coercing every payload in WorkerInteractor.sendevent papers over the producer.

pytest already handles the related cases:

The remaining gap is the native subtests.test(msg=...) path, where a str subclass (e.g. a StrEnum member) is stored on SubtestContext.msg as-is. That is the #1161 case.

I started a branch for it in pytest: pytest-dev/pytest@main...claude/project-thread-pe7mtw. It converts str subclasses in SubtestContext.__post_init__ with str.__str__, the same trick you used here, and extends the existing xdist regression test.


Generated by Claude Code

@DawnofGenX

Copy link
Copy Markdown
Author

Acknowledged, and thanks for the detailed redirect — agreed the fix belongs in pytest, not xdist. I see you've opened pytest-dev/pytest#15134 for the SubtestContext.msg str-subclass case, which is exactly the gap. I'll leave it there rather than reopen this PR. No further action from me on this one.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Serialization error with StrEnum obj

2 participants