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 996866c04a7..02efc71722b 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 @@ -911,7 +912,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 @@ -928,7 +929,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]] @@ -942,7 +943,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[ @@ -966,7 +967,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") @@ -976,7 +977,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]) @@ -1010,7 +1011,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 @@ -1019,7 +1020,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"]) @@ -1100,7 +1101,7 @@ def __str__(self) -> str: ) -@attr.s +@attr.s(**{ATTRS_EQ_FIELD: False}) # type: ignore class ReprFileLocation(TerminalRepr): path = attr.ib(type=str, converter=str) lineno = attr.ib(type=int) @@ -1117,7 +1118,7 @@ def toterminal(self, tw: TerminalWriter) -> None: tw.line(":{}: {}".format(self.lineno, msg)) -@attr.s +@attr.s(**{ATTRS_EQ_FIELD: False}) # type: ignore class ReprLocals(TerminalRepr): lines = attr.ib(type=Sequence[str]) @@ -1126,7 +1127,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 826a377089b..5cbd899905b 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 @@ -180,3 +181,20 @@ def test_not_raise_exception_with_mixed_encoding(self, tw_mock) -> None: tw_mock.lines[0] == r"unicode_string = São Paulo, utf8_string = b'S\xc3\xa3o Paulo'" ) + + +def test_ExceptionChainRepr(): + """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()