diff --git a/AUTHORS b/AUTHORS index 136ffd6c5ee..a6bcf91d896 100644 --- a/AUTHORS +++ b/AUTHORS @@ -4,6 +4,7 @@ merlinux GmbH, Germany, office at merlinux eu Contributors include:: Aaron Coleman +Aathithya Arasu S Abdeali JK Abdelrahman Elbehery Abhijeet Kasurde diff --git a/changelog/15337.bugfix.rst b/changelog/15337.bugfix.rst new file mode 100644 index 00000000000..9b11bc665fd --- /dev/null +++ b/changelog/15337.bugfix.rst @@ -0,0 +1 @@ +Fixed an ``INTERNALERROR`` crash when running pytest on Python 3.10.0 exactly: a slotted dataclass field was never initialized on that interpreter version (:issue:`15337`). diff --git a/src/_pytest/nodeid.py b/src/_pytest/nodeid.py index f859b153475..74c88b9b45b 100644 --- a/src/_pytest/nodeid.py +++ b/src/_pytest/nodeid.py @@ -57,8 +57,13 @@ def parse(cls, nodeid: str) -> NodeId: return self def __str__(self) -> str: - if self._str_cache is not None: - return self._str_cache + # Read ``_str_cache`` defensively: on Python 3.10.0, slotted dataclass + # fields with ``init=False`` and a default value are never set + # (https://github.com/python/cpython/issues/88815), so the attribute + # may be missing entirely. + cache: str | None = getattr(self, "_str_cache", None) + if cache is not None: + return cache s = "::".join((self.path, *self.names)) if self.params is not None: s += f"[{self.params}]" diff --git a/testing/test_nodeid.py b/testing/test_nodeid.py index c6c545ea206..d2ced5e408a 100644 --- a/testing/test_nodeid.py +++ b/testing/test_nodeid.py @@ -23,6 +23,19 @@ def test_str_with_params(self) -> None: node_id = NodeId(path="a/test_b.py", names=("test_c",), params="1-x") assert str(node_id) == "a/test_b.py::test_c[1-x]" + def test_str_missing_str_cache(self) -> None: + # Simulate Python 3.10.0 (https://github.com/python/cpython/issues/88815): + # slotted dataclass fields with init=False and a default are never set, + # so _str_cache may be missing entirely (gh-15337). + node_id = NodeId(path="a/test_b.py", names=("test_c",)) + # Ensure the attribute exists first so the deletion works on every + # interpreter version (on 3.10.0 it is never set in the first place). + object.__setattr__(node_id, "_str_cache", None) + object.__delattr__(node_id, "_str_cache") + assert str(node_id) == "a/test_b.py::test_c" + # The cache must be repopulated by the first __str__ call. + assert node_id._str_cache == "a/test_b.py::test_c" + # -- child() and leaf() -- def test_child(self) -> None: