From 98a5f3f83883a6c924cfa1360647dcbf3152ea10 Mon Sep 17 00:00:00 2001 From: Raymond Hettinger Date: Mon, 13 Sep 2010 21:36:00 +0000 Subject: [PATCH] Issue 9840: Add reprlib.recursive_repr(), a decorator for handling recursive calls to __repr__ methods. --- Doc/library/reprlib.rst | 23 +++++++++++++++++++++++ Lib/collections.py | 18 ++++++------------ Lib/reprlib.py | 31 ++++++++++++++++++++++++++++++- Lib/test/test_reprlib.py | 29 +++++++++++++++++++++++++++++ Misc/NEWS | 3 +++ 5 files changed, 91 insertions(+), 13 deletions(-) diff --git a/Doc/library/reprlib.rst b/Doc/library/reprlib.rst index 056c5dc4427..c794a8d389c 100644 --- a/Doc/library/reprlib.rst +++ b/Doc/library/reprlib.rst @@ -34,6 +34,29 @@ This module provides a class, an instance, and a function: similar to that returned by the built-in function of the same name, but with limits on most sizes. +In addition to size-limiting tools, the module also provides a decorator for +detecting recursive calls to :meth:`__repr__` and substituting a placeholder +string instead. + +.. decorator:: recursive_repr(fillvalue="...") + + Decorator for :meth:`__repr__` methods to detect recursive calls within the + same thread. If a recursive call is made, the *fillvalue* is returned, + otherwise, the usual :meth:`__repr__` call is made. For example: + + >>> class MyList(list): + ... @recursive_repr() + ... def __repr__(self): + ... return '<' + '|'.join(map(repr, self)) + '>' + ... + >>> m = MyList('abc') + >>> m.append(m) + >>> m.append('x') + >>> print(m) + <'a'|'b'|'c'|...|'x'> + + .. versionadded:: 3.2 + .. _repr-objects: diff --git a/Lib/collections.py b/Lib/collections.py index 9120ab69d56..78b4115eb1d 100644 --- a/Lib/collections.py +++ b/Lib/collections.py @@ -13,6 +13,7 @@ import sys as _sys import heapq as _heapq from weakref import proxy as _proxy from itertools import repeat as _repeat, chain as _chain, starmap as _starmap +from reprlib import recursive_repr as _recursive_repr ################################################################################ ### OrderedDict @@ -43,7 +44,6 @@ class OrderedDict(dict, MutableMapping): ''' if len(args) > 1: raise TypeError('expected at most 1 arguments, got %d' % len(args)) - self.__in_repr = False # detects recursive repr try: self.__root except AttributeError: @@ -97,10 +97,10 @@ class OrderedDict(dict, MutableMapping): def __reduce__(self): 'Return state information for pickling' items = [[k, self[k]] for k in self] - tmp = self.__map, self.__root, self.__in_repr - del self.__map, self.__root, self.__in_repr + tmp = self.__map, self.__root + del self.__map, self.__root inst_dict = vars(self).copy() - self.__map, self.__root, self.__in_repr = tmp + self.__map, self.__root = tmp if inst_dict: return (self.__class__, (items,), inst_dict) return self.__class__, (items,) @@ -167,18 +167,12 @@ class OrderedDict(dict, MutableMapping): items = MutableMapping.items __ne__ = MutableMapping.__ne__ + @_recursive_repr() def __repr__(self): 'od.__repr__() <==> repr(od)' if not self: return '%s()' % (self.__class__.__name__,) - if self.__in_repr: - return '...' - self.__in_repr = True - try: - result = '%s(%r)' % (self.__class__.__name__, list(self.items())) - finally: - self.__in_repr = False - return result + return '%s(%r)' % (self.__class__.__name__, list(self.items())) def copy(self): 'od.copy() -> a shallow copy of od' diff --git a/Lib/reprlib.py b/Lib/reprlib.py index 9893c71cb82..c44c75c8b3f 100644 --- a/Lib/reprlib.py +++ b/Lib/reprlib.py @@ -1,9 +1,38 @@ """Redo the builtin repr() (representation) but with limits on most sizes.""" -__all__ = ["Repr","repr"] +__all__ = ["Repr", "repr", "recursive_repr"] import builtins from itertools import islice +try: + from _thread import get_ident +except AttributeError: + from _dummy_thread import get_ident + +def recursive_repr(fillvalue='...'): + 'Decorator to make a repr function return fillvalue for a recursive call' + + def decorating_function(user_function): + repr_running = set() + + def wrapper(self): + key = id(self), get_ident() + if key in repr_running: + return fillvalue + repr_running.add(key) + try: + result = user_function(self) + finally: + repr_running.discard(key) + return result + + # Can't use functools.wraps() here because of bootstrap issues + wrapper.__module__ = getattr(user_function, '__module__') + wrapper.__doc__ = getattr(user_function, '__doc__') + wrapper.__name__ = getattr(user_function, '__name__') + return wrapper + + return decorating_function class Repr: diff --git a/Lib/test/test_reprlib.py b/Lib/test/test_reprlib.py index 0e799f647cb..42714821062 100644 --- a/Lib/test/test_reprlib.py +++ b/Lib/test/test_reprlib.py @@ -11,6 +11,7 @@ import unittest from test.support import run_unittest from reprlib import repr as r # Don't shadow builtin repr from reprlib import Repr +from reprlib import recursive_repr def nestedTuple(nesting): @@ -301,10 +302,38 @@ class ClassWithFailingRepr: def __repr__(self): raise Exception("This should be caught by Repr.repr_instance") +class MyContainer: + 'Helper class for TestRecursiveRepr' + def __init__(self, values): + self.values = list(values) + def append(self, value): + self.values.append(value) + @recursive_repr() + def __repr__(self): + return '<' + ', '.join(map(str, self.values)) + '>' + +class MyContainer2(MyContainer): + @recursive_repr('+++') + def __repr__(self): + return '<' + ', '.join(map(str, self.values)) + '>' + +class TestRecursiveRepr(unittest.TestCase): + def test_recursive_repr(self): + m = MyContainer(list('abcde')) + m.append(m) + m.append('x') + m.append(m) + self.assertEqual(repr(m), '') + m = MyContainer2(list('abcde')) + m.append(m) + m.append('x') + m.append(m) + self.assertEqual(repr(m), '') def test_main(): run_unittest(ReprTests) run_unittest(LongReprTest) + run_unittest(TestRecursiveRepr) if __name__ == "__main__": diff --git a/Misc/NEWS b/Misc/NEWS index ee01289931c..e30f97c9d5b 100644 --- a/Misc/NEWS +++ b/Misc/NEWS @@ -52,6 +52,9 @@ Core and Builtins Library ------- +- Issue 9840: Added a decorator to reprlib for wrapping __repr__ methods + to make them handle recursive calls within the same thread. + - logging: Enhanced HTTPHandler with secure and credentials initializers. - Issue #767645: Set os.path.supports_unicode_filenames to True on Mac OS X