Skip to content

Commit 20f6331

Browse files
authored
Fix TerminalRepr instances to be hashable (#6988)
pytest-xdist assumes `ExceptionChainRepr` is hashable. Fixes #6925. Fixes pytest-dev/pytest-xdist#515.
1 parent 2d9dac9 commit 20f6331

File tree

3 files changed

+30
-10
lines changed

3 files changed

+30
-10
lines changed

changelog/6925.bugfix.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix TerminalRepr instances to be hashable again.

src/_pytest/_code/code.py

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
from _pytest._io import TerminalWriter
3333
from _pytest._io.saferepr import safeformat
3434
from _pytest._io.saferepr import saferepr
35+
from _pytest.compat import ATTRS_EQ_FIELD
3536
from _pytest.compat import overload
3637
from _pytest.compat import TYPE_CHECKING
3738

@@ -911,7 +912,7 @@ def repr_excinfo(self, excinfo: ExceptionInfo) -> "ExceptionChainRepr":
911912
return ExceptionChainRepr(repr_chain)
912913

913914

914-
@attr.s
915+
@attr.s(**{ATTRS_EQ_FIELD: False}) # type: ignore
915916
class TerminalRepr:
916917
def __str__(self) -> str:
917918
# FYI this is called from pytest-xdist's serialization of exception
@@ -928,7 +929,7 @@ def toterminal(self, tw: TerminalWriter) -> None:
928929
raise NotImplementedError()
929930

930931

931-
@attr.s
932+
@attr.s(**{ATTRS_EQ_FIELD: False}) # type: ignore
932933
class ExceptionRepr(TerminalRepr):
933934
def __attrs_post_init__(self):
934935
self.sections = [] # type: List[Tuple[str, str, str]]
@@ -942,7 +943,7 @@ def toterminal(self, tw: TerminalWriter) -> None:
942943
tw.line(content)
943944

944945

945-
@attr.s
946+
@attr.s(**{ATTRS_EQ_FIELD: False}) # type: ignore
946947
class ExceptionChainRepr(ExceptionRepr):
947948
chain = attr.ib(
948949
type=Sequence[
@@ -966,7 +967,7 @@ def toterminal(self, tw: TerminalWriter) -> None:
966967
super().toterminal(tw)
967968

968969

969-
@attr.s
970+
@attr.s(**{ATTRS_EQ_FIELD: False}) # type: ignore
970971
class ReprExceptionInfo(ExceptionRepr):
971972
reprtraceback = attr.ib(type="ReprTraceback")
972973
reprcrash = attr.ib(type="ReprFileLocation")
@@ -976,7 +977,7 @@ def toterminal(self, tw: TerminalWriter) -> None:
976977
super().toterminal(tw)
977978

978979

979-
@attr.s
980+
@attr.s(**{ATTRS_EQ_FIELD: False}) # type: ignore
980981
class ReprTraceback(TerminalRepr):
981982
reprentries = attr.ib(type=Sequence[Union["ReprEntry", "ReprEntryNative"]])
982983
extraline = attr.ib(type=Optional[str])
@@ -1010,7 +1011,7 @@ def __init__(self, tblines: Sequence[str]) -> None:
10101011
self.extraline = None
10111012

10121013

1013-
@attr.s
1014+
@attr.s(**{ATTRS_EQ_FIELD: False}) # type: ignore
10141015
class ReprEntryNative(TerminalRepr):
10151016
lines = attr.ib(type=Sequence[str])
10161017
style = "native" # type: _TracebackStyle
@@ -1019,7 +1020,7 @@ def toterminal(self, tw: TerminalWriter) -> None:
10191020
tw.write("".join(self.lines))
10201021

10211022

1022-
@attr.s
1023+
@attr.s(**{ATTRS_EQ_FIELD: False}) # type: ignore
10231024
class ReprEntry(TerminalRepr):
10241025
lines = attr.ib(type=Sequence[str])
10251026
reprfuncargs = attr.ib(type=Optional["ReprFuncArgs"])
@@ -1100,7 +1101,7 @@ def __str__(self) -> str:
11001101
)
11011102

11021103

1103-
@attr.s
1104+
@attr.s(**{ATTRS_EQ_FIELD: False}) # type: ignore
11041105
class ReprFileLocation(TerminalRepr):
11051106
path = attr.ib(type=str, converter=str)
11061107
lineno = attr.ib(type=int)
@@ -1117,7 +1118,7 @@ def toterminal(self, tw: TerminalWriter) -> None:
11171118
tw.line(":{}: {}".format(self.lineno, msg))
11181119

11191120

1120-
@attr.s
1121+
@attr.s(**{ATTRS_EQ_FIELD: False}) # type: ignore
11211122
class ReprLocals(TerminalRepr):
11221123
lines = attr.ib(type=Sequence[str])
11231124

@@ -1126,7 +1127,7 @@ def toterminal(self, tw: TerminalWriter, indent="") -> None:
11261127
tw.line(indent + line)
11271128

11281129

1129-
@attr.s
1130+
@attr.s(**{ATTRS_EQ_FIELD: False}) # type: ignore
11301131
class ReprFuncArgs(TerminalRepr):
11311132
args = attr.ib(type=Sequence[Tuple[str, object]])
11321133

testing/code/test_code.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from _pytest._code import Code
77
from _pytest._code import ExceptionInfo
88
from _pytest._code import Frame
9+
from _pytest._code.code import ExceptionChainRepr
910
from _pytest._code.code import ReprFuncArgs
1011

1112

@@ -180,3 +181,20 @@ def test_not_raise_exception_with_mixed_encoding(self, tw_mock) -> None:
180181
tw_mock.lines[0]
181182
== r"unicode_string = São Paulo, utf8_string = b'S\xc3\xa3o Paulo'"
182183
)
184+
185+
186+
def test_ExceptionChainRepr():
187+
"""Test ExceptionChainRepr, especially with regard to being hashable."""
188+
try:
189+
raise ValueError()
190+
except ValueError:
191+
excinfo1 = ExceptionInfo.from_current()
192+
excinfo2 = ExceptionInfo.from_current()
193+
194+
repr1 = excinfo1.getrepr()
195+
repr2 = excinfo2.getrepr()
196+
assert repr1 != repr2
197+
198+
assert isinstance(repr1, ExceptionChainRepr)
199+
assert hash(repr1) != hash(repr2)
200+
assert repr1 is not excinfo1.getrepr()

0 commit comments

Comments
 (0)