diff --git a/changelog.d/95.change.rst b/changelog.d/95.change.rst new file mode 100644 index 000000000..dc9651735 --- /dev/null +++ b/changelog.d/95.change.rst @@ -0,0 +1 @@ +``x=X(); x.cycle = x; repr(x)`` will no longer raise a ``RecursionError``, and will instead show as ``X(x=...)``. diff --git a/src/attr/_make.py b/src/attr/_make.py index e8f55d8b8..ebe12078b 100644 --- a/src/attr/_make.py +++ b/src/attr/_make.py @@ -3,6 +3,7 @@ import hashlib import linecache import sys +import threading import warnings from operator import itemgetter @@ -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. @@ -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) @@ -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__ diff --git a/tests/test_dunders.py b/tests/test_dunders.py index d8ae41a60..43a793f4e 100644 --- a/tests/test_dunders.py +++ b/tests/test_dunders.py @@ -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.