Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

No recursion error for cycles in repr #358

Merged
merged 6 commits into from
Mar 17, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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

This comment was marked as spam.

This comment was marked as spam.

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