From 5ece3858e4547039d5a425d1bdf9c58727fb2017 Mon Sep 17 00:00:00 2001 From: holger krekel Date: Thu, 29 Apr 2010 14:17:07 +0200 Subject: [PATCH] introduce new py.io.saferepr for printing the 'repr' of an object safely and without consuming too much space --HG-- branch : trunk --- CHANGELOG | 4 ++ py/__init__.py | 1 + py/_code/code.py | 48 ++--------------- py/_io/saferepr.py | 55 ++++++++++++++++++++ testing/code/test_code.py | 38 -------------- testing/io_/test_saferepr.py | 99 ++++++++++++++++++++++++++++++++++++ 6 files changed, 162 insertions(+), 83 deletions(-) create mode 100644 py/_io/saferepr.py create mode 100644 testing/io_/test_saferepr.py diff --git a/CHANGELOG b/CHANGELOG index 0fb396b19..76af068e2 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -8,6 +8,10 @@ Changes between 1.2.1 and 1.2.2 (release pending) - fixes for handling of unicode exception values and unprintable objects - (issue87) fix unboundlocal error in assertionold code - (issue86) improve documentation for looponfailing +- expose previously internal commonly useful methods: + py.io.get_terminal_with() -> return terminal width + py.io.ansi_print(...) -> print colored/bold text on linux/win32 + py.io.saferepr(obj) -> return limited representation string - expose test outcome related exceptions as py.test.skip.Exception, py.test.raises.Exception etc., useful mostly for plugins doing special outcome interpreteration/tweaking diff --git a/py/__init__.py b/py/__init__.py index c8e3faae9..20e416925 100644 --- a/py/__init__.py +++ b/py/__init__.py @@ -140,6 +140,7 @@ py.apipkg.initpkg(__name__, dict( 'TerminalWriter' : '._io.terminalwriter:TerminalWriter', 'ansi_print' : '._io.terminalwriter:ansi_print', 'get_terminal_width' : '._io.terminalwriter:get_terminal_width', + 'saferepr' : '._io.saferepr:saferepr', }, # small and mean xml/html generation diff --git a/py/_code/code.py b/py/_code/code.py index 64b6c279b..e8bddb904 100644 --- a/py/_code/code.py +++ b/py/_code/code.py @@ -147,7 +147,7 @@ class Frame(object): def repr(self, object): """ return a 'safe' (non-recursive, one-line) string repr for 'object' """ - return safe_repr(object) + return py.io.saferepr(object) def is_true(self, object): return object @@ -457,7 +457,7 @@ class FormattedExcinfo(object): return source def _saferepr(self, obj): - return safe_repr(obj) + return py.io.saferepr(obj) def repr_args(self, entry): if self.funcargs: @@ -605,7 +605,7 @@ def unicode_or_repr(obj): except KeyboardInterrupt: raise except Exception: - return "" % safe_repr(obj) + return "" % py.io.saferepr(obj) class ReprExceptionInfo(TerminalRepr): def __init__(self, reprtraceback, reprcrash): @@ -717,48 +717,6 @@ class ReprFuncArgs(TerminalRepr): -class SafeRepr(reprlib.Repr): - """ subclass of repr.Repr that limits the resulting size of repr() - and includes information on exceptions raised during the call. - """ - def __init__(self, *args, **kwargs): - reprlib.Repr.__init__(self, *args, **kwargs) - self.maxstring = 240 # 3 * 80 chars - self.maxother = 160 # 2 * 80 chars - - def repr(self, x): - return self._callhelper(reprlib.Repr.repr, self, x) - - def repr_instance(self, x, level): - return self._callhelper(builtin_repr, x) - - def _callhelper(self, call, x, *args): - try: - # Try the vanilla repr and make sure that the result is a string - s = call(x, *args) - except (KeyboardInterrupt, MemoryError, SystemExit): - raise - except: - cls, e, tb = sys.exc_info() - try: - exc_name = cls.__name__ - except: - exc_name = 'unknown' - try: - exc_info = str(e) - except: - exc_info = 'unknown' - return '<[%s("%s") raised in repr()] %s object at 0x%x>' % ( - exc_name, exc_info, x.__class__.__name__, id(x)) - else: - if len(s) > self.maxstring: - i = max(0, (self.maxstring-3)//2) - j = max(0, self.maxstring-3-i) - s = s[:i] + '...' + s[len(s)-j:] - return s - -safe_repr = SafeRepr().repr - oldbuiltins = {} def patch_builtins(assertion=True, compile=True): diff --git a/py/_io/saferepr.py b/py/_io/saferepr.py new file mode 100644 index 000000000..31fabda28 --- /dev/null +++ b/py/_io/saferepr.py @@ -0,0 +1,55 @@ +import py +import sys, os.path + +builtin_repr = repr + +reprlib = py.builtin._tryimport('repr', 'reprlib') + +sysex = (KeyboardInterrupt, MemoryError, SystemExit) + +class SafeRepr(reprlib.Repr): + """ subclass of repr.Repr that limits the resulting size of repr() + and includes information on exceptions raised during the call. + """ + def repr(self, x): + return self._callhelper(reprlib.Repr.repr, self, x) + + def repr_instance(self, x, level): + return self._callhelper(builtin_repr, x) + + def _callhelper(self, call, x, *args): + try: + # Try the vanilla repr and make sure that the result is a string + s = call(x, *args) + except sysex: + raise + except: + cls, e, tb = sys.exc_info() + exc_name = getattr(cls, '__name__', 'unknown') + try: + exc_info = str(e) + except sysex: + raise + except: + exc_info = 'unknown' + return '<[%s("%s") raised in repr()] %s object at 0x%x>' % ( + exc_name, exc_info, x.__class__.__name__, id(x)) + else: + if len(s) > self.maxsize: + i = max(0, (self.maxsize-3)//2) + j = max(0, self.maxsize-3-i) + s = s[:i] + '...' + s[len(s)-j:] + return s + +def saferepr(obj, maxsize=240): + """ return a size-limited safe repr-string for the given object. + Failing __repr__ functions of user instances will be represented + with a short exception info and 'saferepr' generally takes + care to never raise exceptions itself. This function is a wrapper + around the Repr/reprlib functionality of the standard 2.6 lib. + """ + # review exception handling + srepr = SafeRepr() + srepr.maxstring = maxsize + srepr.maxsize = maxsize + return srepr.repr(obj) diff --git a/testing/code/test_code.py b/testing/code/test_code.py index f58eea789..feb88ebea 100644 --- a/testing/code/test_code.py +++ b/testing/code/test_code.py @@ -1,7 +1,6 @@ from __future__ import generators import py import sys -from py._code.code import safe_repr failsonjython = py.test.mark.xfail("sys.platform.startswith('java')") @@ -139,43 +138,6 @@ def test_code_from_func(): -class TestSafeRepr: - def test_simple_repr(self): - assert safe_repr(1) == '1' - assert safe_repr(None) == 'None' - - def test_exceptions(self): - class BrokenRepr: - def __init__(self, ex): - self.ex = ex - foo = 0 - def __repr__(self): - raise self.ex - class BrokenReprException(Exception): - __str__ = None - __repr__ = None - assert 'Exception' in safe_repr(BrokenRepr(Exception("broken"))) - s = safe_repr(BrokenReprException("really broken")) - assert 'TypeError' in s - if py.std.sys.version_info < (2,6): - assert 'unknown' in safe_repr(BrokenRepr("string")) - else: - assert 'TypeError' in safe_repr(BrokenRepr("string")) - - def test_big_repr(self): - from py._code.code import SafeRepr - assert len(safe_repr(range(1000))) <= \ - len('[' + SafeRepr().maxlist * "1000" + ']') - - def test_repr_on_newstyle(self): - class Function(object): - def __repr__(self): - return "<%s>" %(self.name) - try: - s = safe_repr(Function()) - except Exception: - py.test.fail("saferepr failed for newstyle class") - def test_builtin_patch_unpatch(monkeypatch): cpy_builtin = py.builtin.builtins comp = cpy_builtin.compile diff --git a/testing/io_/test_saferepr.py b/testing/io_/test_saferepr.py new file mode 100644 index 000000000..c2fb91966 --- /dev/null +++ b/testing/io_/test_saferepr.py @@ -0,0 +1,99 @@ +from __future__ import generators +import py +import sys + +saferepr = py.io.saferepr + +class TestSafeRepr: + def test_simple_repr(self): + assert saferepr(1) == '1' + assert saferepr(None) == 'None' + + def test_maxsize(self): + s = saferepr('x'*50, maxsize=25) + assert len(s) == 25 + expected = repr('x'*10 + '...' + 'x'*10) + assert s == expected + + def test_maxsize_error_on_instance(self): + class A: + def __repr__(self): + raise ValueError('...') + + s = saferepr(('*'*50, A()), maxsize=25) + assert len(s) == 25 + assert s[0] == '(' and s[-1] == ')' + + def test_exceptions(self): + class BrokenRepr: + def __init__(self, ex): + self.ex = ex + foo = 0 + def __repr__(self): + raise self.ex + class BrokenReprException(Exception): + __str__ = None + __repr__ = None + assert 'Exception' in saferepr(BrokenRepr(Exception("broken"))) + s = saferepr(BrokenReprException("really broken")) + assert 'TypeError' in s + if py.std.sys.version_info < (2,6): + assert 'unknown' in saferepr(BrokenRepr("string")) + else: + assert 'TypeError' in saferepr(BrokenRepr("string")) + + def test_big_repr(self): + from py._io.saferepr import SafeRepr + assert len(saferepr(range(1000))) <= \ + len('[' + SafeRepr().maxlist * "1000" + ']') + + def test_repr_on_newstyle(self): + class Function(object): + def __repr__(self): + return "<%s>" %(self.name) + try: + s = saferepr(Function()) + except Exception: + py.test.fail("saferepr failed for newstyle class") + +def test_builtin_patch_unpatch(monkeypatch): + cpy_builtin = py.builtin.builtins + comp = cpy_builtin.compile + def mycompile(*args, **kwargs): + return comp(*args, **kwargs) + class Sub(AssertionError): + pass + monkeypatch.setattr(cpy_builtin, 'AssertionError', Sub) + monkeypatch.setattr(cpy_builtin, 'compile', mycompile) + py.code.patch_builtins() + assert cpy_builtin.AssertionError != Sub + assert cpy_builtin.compile != mycompile + py.code.unpatch_builtins() + assert cpy_builtin.AssertionError is Sub + assert cpy_builtin.compile == mycompile + + +def test_unicode_handling(): + value = py.builtin._totext('\xc4\x85\xc4\x87\n', 'utf-8').encode('utf8') + def f(): + raise Exception(value) + excinfo = py.test.raises(Exception, f) + s = str(excinfo) + if sys.version_info[0] < 3: + u = unicode(excinfo) + +def test_unicode_or_repr(): + from py._code.code import unicode_or_repr + assert unicode_or_repr('hello') == "hello" + if sys.version_info[0] < 3: + s = unicode_or_repr('\xf6\xc4\x85') + else: + s = eval("unicode_or_repr(b'\\f6\\xc4\\x85')") + assert 'print-error' in s + assert 'c4' in s + class A: + def __repr__(self): + raise ValueError() + s = unicode_or_repr(A()) + assert 'print-error' in s + assert 'ValueError' in s