Skip to content

Commit

Permalink
return "..." rather than raising RecursionError for repr() on a cycli…
Browse files Browse the repository at this point in the history
…c object

Fixes #95
  • Loading branch information
glyph authored Mar 17, 2018
1 parent 780e1e5 commit 5e46afd
Show file tree
Hide file tree
Showing 3 changed files with 44 additions and 7 deletions.
1 change: 1 addition & 0 deletions changelog.d/95.change.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
``x=X(); x.cycle = x; repr(x)`` will no longer raise a ``RecursionError``, and will instead show as ``X(x=...)``.
36 changes: 29 additions & 7 deletions src/attr/_make.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import hashlib
import linecache
import sys
import threading
import warnings

from operator import itemgetter
Expand Down Expand Up @@ -953,6 +954,9 @@ def _add_cmp(cls, attrs=None):
return cls


_already_repring = threading.local()


def _make_repr(attrs, ns):
"""
Make a repr method for *attr_names* adding *ns* to the full name.
Expand All @@ -967,6 +971,14 @@ def __repr__(self):
"""
Automatically created by attrs.
"""
try:
working_set = _already_repring.working_set
except AttributeError:
working_set = set()
_already_repring.working_set = working_set

if id(self) in working_set:
return "..."
real_cls = self.__class__
if ns is None:
qualname = getattr(real_cls, "__qualname__", None)
Expand All @@ -977,13 +989,23 @@ def __repr__(self):
else:
class_name = ns + "." + real_cls.__name__

return "{0}({1})".format(
class_name,
", ".join(
name + "=" + repr(getattr(self, name, NOTHING))
for name in attr_names
)
)
# Since 'self' remains on the stack (i.e.: strongly referenced) for the
# duration of this call, it's safe to depend on id(...) stability, and
# not need to track the instance and therefore worry about properties
# like weakref- or hash-ability.
working_set.add(id(self))
try:
result = [class_name, "("]
first = True
for name in attr_names:
if first:
first = False
else:
result.append(", ")
result.extend((name, "=", repr(getattr(self, name, NOTHING))))
return "".join(result) + ")"
finally:
working_set.remove(id(self))
return __repr__


Expand Down
14 changes: 14 additions & 0 deletions tests/test_dunders.py
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,20 @@ def test_repr_works(self, cls):
"""
assert "C(a=1, b=2)" == repr(cls(1, 2))

def test_infinite_recursion(self):
"""
In the presence of a cyclic graph, repr will emit an ellipsis and not
raise an exception.
"""
@attr.s
class Cycle(object):
value = attr.ib(default=7)
cycle = attr.ib(default=None)

cycle = Cycle()
cycle.cycle = cycle
assert "Cycle(value=7, cycle=...)" == repr(cycle)

def test_underscores(self):
"""
repr does not strip underscores.
Expand Down

0 comments on commit 5e46afd

Please sign in to comment.