Skip to content

Commit

Permalink
Fix TerminalRepr instances to be hashable (pytest-dev#6988)
Browse files Browse the repository at this point in the history
pytest-xdist assumes `ExceptionChainRepr` is hashable.

Fixes pytest-dev#6925.
Fixes pytest-dev/pytest-xdist#515.

(cherry picked from commit 20f6331)
  • Loading branch information
blueyed committed Apr 2, 2020
1 parent e3a3c90 commit 9d8aa17
Show file tree
Hide file tree
Showing 3 changed files with 30 additions and 10 deletions.
1 change: 1 addition & 0 deletions changelog/6925.bugfix.rst
@@ -0,0 +1 @@
Fix TerminalRepr instances to be hashable again.
21 changes: 11 additions & 10 deletions src/_pytest/_code/code.py
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand All @@ -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]]
Expand All @@ -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[
Expand All @@ -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")
Expand All @@ -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])
Expand Down Expand Up @@ -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
Expand All @@ -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"])
Expand Down Expand Up @@ -1093,7 +1094,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)
Expand All @@ -1110,7 +1111,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])

Expand All @@ -1119,7 +1120,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]])

Expand Down
18 changes: 18 additions & 0 deletions testing/code/test_code.py
Expand Up @@ -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


Expand Down Expand Up @@ -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()

0 comments on commit 9d8aa17

Please sign in to comment.