From c90b28618c17ebb2ffe38844a5cf50eef3e8f4ff Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Fri, 3 Apr 2020 00:56:53 +0200 Subject: [PATCH] Fix TerminalRepr instances to be hashable pytest-xdist assumes `ExceptionChainRepr` is hashable. Fixes https://github.com/pytest-dev/pytest/issues/6925. Fixes https://github.com/pytest-dev/pytest-xdist/issues/515. (cherry picked from commit 20f6331afd3cf5af1d708f77aede08ad1a9d8661) https://github.com/pytest-dev/pytest/pull/6988 Conflicts: src/_pytest/_code/code.py testing/code/test_code.py --- changelog/6925.bugfix.rst | 1 + src/_pytest/_code/code.py | 21 +++++++++++---------- testing/code/test_code.py | 18 ++++++++++++++++++ 3 files changed, 30 insertions(+), 10 deletions(-) create mode 100644 changelog/6925.bugfix.rst diff --git a/changelog/6925.bugfix.rst b/changelog/6925.bugfix.rst new file mode 100644 index 00000000000..ed7e99b5dd2 --- /dev/null +++ b/changelog/6925.bugfix.rst @@ -0,0 +1 @@ +Fix TerminalRepr instances to be hashable again. diff --git a/src/_pytest/_code/code.py b/src/_pytest/_code/code.py index 71d04059dea..f4d218d9ced 100644 --- a/src/_pytest/_code/code.py +++ b/src/_pytest/_code/code.py @@ -32,6 +32,7 @@ from _pytest._io import TerminalWriter from _pytest._io.saferepr import safeformat from _pytest._io.saferepr import saferepr +from _pytest.compat import ATTRS_EQ_FIELD from _pytest.compat import overload from _pytest.compat import TYPE_CHECKING from _pytest.outcomes import OutcomeException @@ -942,7 +943,7 @@ def repr_excinfo(self, excinfo: ExceptionInfo) -> "ExceptionChainRepr": return ExceptionChainRepr(repr_chain) -@attr.s +@attr.s(**{ATTRS_EQ_FIELD: False}) # type: ignore class TerminalRepr: def __str__(self) -> str: # FYI this is called from pytest-xdist's serialization of exception @@ -959,7 +960,7 @@ def toterminal(self, tw: TerminalWriter) -> None: raise NotImplementedError() -@attr.s +@attr.s(**{ATTRS_EQ_FIELD: False}) # type: ignore class ExceptionRepr(TerminalRepr): def __attrs_post_init__(self): self.sections = [] # type: List[Tuple[str, str, str]] @@ -973,7 +974,7 @@ def toterminal(self, tw: TerminalWriter) -> None: tw.line(content) -@attr.s +@attr.s(**{ATTRS_EQ_FIELD: False}) # type: ignore class ExceptionChainRepr(ExceptionRepr): chain = attr.ib( type=Sequence[ @@ -997,7 +998,7 @@ def toterminal(self, tw: TerminalWriter) -> None: super().toterminal(tw) -@attr.s +@attr.s(**{ATTRS_EQ_FIELD: False}) # type: ignore class ReprExceptionInfo(ExceptionRepr): reprtraceback = attr.ib(type="ReprTraceback") reprcrash = attr.ib(type="ReprFileLocation") @@ -1007,7 +1008,7 @@ def toterminal(self, tw: TerminalWriter) -> None: super().toterminal(tw) -@attr.s +@attr.s(**{ATTRS_EQ_FIELD: False}) # type: ignore class ReprTraceback(TerminalRepr): reprentries = attr.ib(type=Sequence[Union["ReprEntry", "ReprEntryNative"]]) extraline = attr.ib(type=Optional[str]) @@ -1041,7 +1042,7 @@ def __init__(self, tblines: Sequence[str]) -> None: self.extraline = None -@attr.s +@attr.s(**{ATTRS_EQ_FIELD: False}) # type: ignore class ReprEntryNative(TerminalRepr): lines = attr.ib(type=Sequence[str]) style = "native" # type: _TracebackStyle @@ -1050,7 +1051,7 @@ def toterminal(self, tw: TerminalWriter) -> None: tw.write("".join(self.lines)) -@attr.s +@attr.s(**{ATTRS_EQ_FIELD: False}) # type: ignore class ReprEntry(TerminalRepr): lines = attr.ib(type=Sequence[str]) reprfuncargs = attr.ib(type=Optional["ReprFuncArgs"]) @@ -1113,7 +1114,7 @@ def __str__(self) -> str: ) -@attr.s +@attr.s(**{ATTRS_EQ_FIELD: False}) # type: ignore[call-overload] class ReprFileLocation(TerminalRepr): path = attr.ib(type=str, converter=str) lineno = attr.ib(type="Optional[int]") @@ -1139,7 +1140,7 @@ def toterminal(self, tw: TerminalWriter, style: "_TracebackStyle" = None) -> Non tw.line(": {}".format(self._get_short_msg())) -@attr.s +@attr.s(**{ATTRS_EQ_FIELD: False}) # type: ignore class ReprLocals(TerminalRepr): lines = attr.ib(type=Sequence[str]) @@ -1148,7 +1149,7 @@ def toterminal(self, tw: TerminalWriter, indent="") -> None: tw.line(indent + line) -@attr.s +@attr.s(**{ATTRS_EQ_FIELD: False}) # type: ignore class ReprFuncArgs(TerminalRepr): args = attr.ib(type=Sequence[Tuple[str, object]]) diff --git a/testing/code/test_code.py b/testing/code/test_code.py index f9ab9b819b5..8bc164521bf 100644 --- a/testing/code/test_code.py +++ b/testing/code/test_code.py @@ -6,6 +6,7 @@ from _pytest._code import Code from _pytest._code import ExceptionInfo from _pytest._code import Frame +from _pytest._code.code import ExceptionChainRepr from _pytest._code.code import ReprFuncArgs from _pytest.compat import TYPE_CHECKING @@ -253,3 +254,20 @@ def test(): "*= 1 error in *", ] ) + + +def test_ExceptionChainRepr() -> None: + """Test ExceptionChainRepr, especially with regard to being hashable.""" + try: + raise ValueError() + except ValueError: + excinfo1 = ExceptionInfo.from_current() + excinfo2 = ExceptionInfo.from_current() + + repr1 = excinfo1.getrepr() + repr2 = excinfo2.getrepr() + assert repr1 != repr2 + + assert isinstance(repr1, ExceptionChainRepr) + assert hash(repr1) != hash(repr2) + assert repr1 is not excinfo1.getrepr()