From dbb6c18c44bef06f77bf98acda274a6e093e7c49 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 20 Jan 2019 10:40:20 -0800 Subject: [PATCH 1/3] copy saferepr from pylib verbatim Copied from b9da2ed6178cd37d4ed6b41f9fa8234dce96973f --- src/_pytest/_io/__init__.py | 0 src/_pytest/_io/saferepr.py | 71 +++++++++++++++++++++++++++++++++++ testing/io/test_saferepr.py | 75 +++++++++++++++++++++++++++++++++++++ 3 files changed, 146 insertions(+) create mode 100644 src/_pytest/_io/__init__.py create mode 100644 src/_pytest/_io/saferepr.py create mode 100644 testing/io/test_saferepr.py diff --git a/src/_pytest/_io/__init__.py b/src/_pytest/_io/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/_pytest/_io/saferepr.py b/src/_pytest/_io/saferepr.py new file mode 100644 index 000000000..8518290ef --- /dev/null +++ b/src/_pytest/_io/saferepr.py @@ -0,0 +1,71 @@ +import py +import sys + +builtin_repr = repr + +reprlib = py.builtin._tryimport('repr', 'reprlib') + +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_unicode(self, x, level): + # Strictly speaking wrong on narrow builds + def repr(u): + if "'" not in u: + return py.builtin._totext("'%s'") % u + elif '"' not in u: + return py.builtin._totext('"%s"') % u + else: + return py.builtin._totext("'%s'") % u.replace("'", r"\'") + s = repr(x[:self.maxstring]) + if len(s) > self.maxstring: + i = max(0, (self.maxstring-3)//2) + j = max(0, self.maxstring-3-i) + s = repr(x[:i] + x[len(x)-j:]) + s = s[:i] + '...' + s[len(s)-j:] + return s + + 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 py.builtin._sysex: + raise + except: + cls, e, tb = sys.exc_info() + exc_name = getattr(cls, '__name__', 'unknown') + try: + exc_info = str(e) + except py.builtin._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 + srepr.maxother = 160 + return srepr.repr(obj) diff --git a/testing/io/test_saferepr.py b/testing/io/test_saferepr.py new file mode 100644 index 000000000..97be1416f --- /dev/null +++ b/testing/io/test_saferepr.py @@ -0,0 +1,75 @@ +# -*- coding: utf-8 -*- + +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 + assert 'TypeError' in saferepr(BrokenRepr("string")) + + s2 = saferepr(BrokenRepr(BrokenReprException('omg even worse'))) + assert 'NameError' not in s2 + assert 'unknown' in s2 + + 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_unicode(self): + val = py.builtin._totext('£€', 'utf-8') + reprval = py.builtin._totext("'£€'", 'utf-8') + assert saferepr(val) == reprval + +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) + From 095ce2ca7fd3828f451a10c191f41517c9dfe34b Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 20 Jan 2019 10:50:18 -0800 Subject: [PATCH 2/3] Fix linting errors and py references in saferepr.py --- src/_pytest/_io/saferepr.py | 55 +++++++++--------- testing/io/test_saferepr.py | 113 +++++++++++++++++------------------- 2 files changed, 80 insertions(+), 88 deletions(-) diff --git a/src/_pytest/_io/saferepr.py b/src/_pytest/_io/saferepr.py index 8518290ef..4d1d18d3b 100644 --- a/src/_pytest/_io/saferepr.py +++ b/src/_pytest/_io/saferepr.py @@ -1,14 +1,13 @@ -import py import sys -builtin_repr = repr +from six.moves import reprlib -reprlib = py.builtin._tryimport('repr', 'reprlib') class SafeRepr(reprlib.Repr): - """ subclass of repr.Repr that limits the resulting size of repr() - and includes information on exceptions raised during the call. + """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) @@ -16,48 +15,50 @@ class SafeRepr(reprlib.Repr): # Strictly speaking wrong on narrow builds def repr(u): if "'" not in u: - return py.builtin._totext("'%s'") % u + return u"'%s'" % u elif '"' not in u: - return py.builtin._totext('"%s"') % u + return u'"%s"' % u else: - return py.builtin._totext("'%s'") % u.replace("'", r"\'") - s = repr(x[:self.maxstring]) + return u"'%s'" % u.replace("'", r"\'") + + s = repr(x[: self.maxstring]) if len(s) > self.maxstring: - i = max(0, (self.maxstring-3)//2) - j = max(0, self.maxstring-3-i) - s = repr(x[:i] + x[len(x)-j:]) - s = s[:i] + '...' + s[len(s)-j:] + i = max(0, (self.maxstring - 3) // 2) + j = max(0, self.maxstring - 3 - i) + s = repr(x[:i] + x[len(x) - j :]) + s = s[:i] + "..." + s[len(s) - j :] return s def repr_instance(self, x, level): - return self._callhelper(builtin_repr, x) + return self._callhelper(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 py.builtin._sysex: - raise - except: + except Exception: cls, e, tb = sys.exc_info() - exc_name = getattr(cls, '__name__', 'unknown') + exc_name = getattr(cls, "__name__", "unknown") try: exc_info = str(e) - except py.builtin._sysex: - raise - except: - exc_info = 'unknown' + except Exception: + exc_info = "unknown" return '<[%s("%s") raised in repr()] %s object at 0x%x>' % ( - exc_name, exc_info, x.__class__.__name__, id(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:] + 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. + """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 diff --git a/testing/io/test_saferepr.py b/testing/io/test_saferepr.py index 97be1416f..901203088 100644 --- a/testing/io/test_saferepr.py +++ b/testing/io/test_saferepr.py @@ -1,75 +1,66 @@ # -*- coding: utf-8 -*- +from _pytest._io.saferepr import saferepr -from __future__ import generators -import py -import sys -saferepr = py.io.saferepr +def test_simple_repr(): + assert saferepr(1) == "1" + assert saferepr(None) == "None" -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(): + 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_maxsize_error_on_instance(): + class A: + def __repr__(): + raise ValueError("...") - 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 - assert 'TypeError' in saferepr(BrokenRepr("string")) + s = saferepr(("*" * 50, A()), maxsize=25) + assert len(s) == 25 + assert s[0] == "(" and s[-1] == ")" - s2 = saferepr(BrokenRepr(BrokenReprException('omg even worse'))) - assert 'NameError' not in s2 - assert 'unknown' in s2 - def test_big_repr(self): - from py._io.saferepr import SafeRepr - assert len(saferepr(range(1000))) <= \ - len('[' + SafeRepr().maxlist * "1000" + ']') +def test_exceptions(): + class BrokenRepr: + def __init__(self, ex): + self.ex = ex - 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 __repr__(self): + raise self.ex - def test_unicode(self): - val = py.builtin._totext('£€', 'utf-8') - reprval = py.builtin._totext("'£€'", 'utf-8') - assert saferepr(val) == reprval + class BrokenReprException(Exception): + __str__ = None + __repr__ = None -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) + assert "Exception" in saferepr(BrokenRepr(Exception("broken"))) + s = saferepr(BrokenReprException("really broken")) + assert "TypeError" in s + assert "TypeError" in saferepr(BrokenRepr("string")) + s2 = saferepr(BrokenRepr(BrokenReprException("omg even worse"))) + assert "NameError" not in s2 + assert "unknown" in s2 + + +def test_big_repr(): + from _pytest._io.saferepr import SafeRepr + + assert len(saferepr(range(1000))) <= len("[" + SafeRepr().maxlist * "1000" + "]") + + +def test_repr_on_newstyle(): + class Function(object): + def __repr__(self): + return "<%s>" % (self.name) + + assert saferepr(Function()) + + +def test_unicode(): + val = u"£€" + reprval = u"'£€'" + assert saferepr(val) == reprval From 0c6ca0da62c2c48003de5237e52ab04fca3b11e2 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 20 Jan 2019 10:45:12 -0800 Subject: [PATCH 3/3] Fix usages of py.io.saferepr --- .pre-commit-config.yaml | 2 +- changelog/4657.trivial.rst | 1 + setup.cfg | 5 +++-- src/_pytest/_code/code.py | 7 ++++--- src/_pytest/assertion/rewrite.py | 9 +++++---- src/_pytest/assertion/util.py | 20 +++++++++----------- src/_pytest/compat.py | 3 ++- src/_pytest/pytester.py | 5 ++--- 8 files changed, 27 insertions(+), 25 deletions(-) create mode 100644 changelog/4657.trivial.rst diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 80e78ab50..fb0ab1c12 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -54,5 +54,5 @@ repos: - id: py-deprecated name: py library is deprecated language: pygrep - entry: '\bpy\.(builtin\.|code\.|std\.)' + entry: '\bpy\.(builtin\.|code\.|std\.|io\.saferepr)' types: [python] diff --git a/changelog/4657.trivial.rst b/changelog/4657.trivial.rst new file mode 100644 index 000000000..abdab08eb --- /dev/null +++ b/changelog/4657.trivial.rst @@ -0,0 +1 @@ +Copy saferepr from pylib diff --git a/setup.cfg b/setup.cfg index 8cd3858fd..9d0aa332e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -36,10 +36,11 @@ platforms = unix, linux, osx, cygwin, win32 zip_safe = no packages = _pytest - _pytest.assertion _pytest._code - _pytest.mark + _pytest._io + _pytest.assertion _pytest.config + _pytest.mark py_modules = pytest python_requires = >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.* diff --git a/src/_pytest/_code/code.py b/src/_pytest/_code/code.py index 1b49fe75b..fd99c6cdd 100644 --- a/src/_pytest/_code/code.py +++ b/src/_pytest/_code/code.py @@ -18,6 +18,7 @@ import six from six import text_type import _pytest +from _pytest._io.saferepr import saferepr from _pytest.compat import _PY2 from _pytest.compat import _PY3 from _pytest.compat import PY35 @@ -144,7 +145,7 @@ class Frame(object): def repr(self, object): """ return a 'safe' (non-recursive, one-line) string repr for 'object' """ - return py.io.saferepr(object) + return saferepr(object) def is_true(self, object): return object @@ -423,7 +424,7 @@ class ExceptionInfo(object): if exprinfo is None and isinstance(tup[1], AssertionError): exprinfo = getattr(tup[1], "msg", None) if exprinfo is None: - exprinfo = py.io.saferepr(tup[1]) + exprinfo = saferepr(tup[1]) if exprinfo and exprinfo.startswith(cls._assert_start_repr): _striptext = "AssertionError: " @@ -620,7 +621,7 @@ class FormattedExcinfo(object): return source def _saferepr(self, obj): - return py.io.saferepr(obj) + return saferepr(obj) def repr_args(self, entry): if self.funcargs: diff --git a/src/_pytest/assertion/rewrite.py b/src/_pytest/assertion/rewrite.py index 80f182723..52f5ebce7 100644 --- a/src/_pytest/assertion/rewrite.py +++ b/src/_pytest/assertion/rewrite.py @@ -19,6 +19,7 @@ import atomicwrites import py import six +from _pytest._io.saferepr import saferepr from _pytest.assertion import util from _pytest.compat import spec_from_file_location from _pytest.pathlib import fnmatch_ex @@ -484,7 +485,7 @@ def _saferepr(obj): JSON reprs. """ - r = py.io.saferepr(obj) + r = saferepr(obj) # only occurs in python2.x, repr must return text in python3+ if isinstance(r, bytes): # Represent unprintable bytes as `\x##` @@ -503,7 +504,7 @@ def _format_assertmsg(obj): For strings this simply replaces newlines with '\n~' so that util.format_explanation() will preserve them instead of escaping - newlines. For other objects py.io.saferepr() is used first. + newlines. For other objects saferepr() is used first. """ # reprlib appears to have a bug which means that if a string @@ -512,7 +513,7 @@ def _format_assertmsg(obj): # However in either case we want to preserve the newline. replaces = [(u"\n", u"\n~"), (u"%", u"%%")] if not isinstance(obj, six.string_types): - obj = py.io.saferepr(obj) + obj = saferepr(obj) replaces.append((u"\\n", u"\n~")) if isinstance(obj, bytes): @@ -753,7 +754,7 @@ class AssertionRewriter(ast.NodeVisitor): return ast.Name(name, ast.Load()) def display(self, expr): - """Call py.io.saferepr on the expression.""" + """Call saferepr on the expression.""" return self.helper("saferepr", expr) def helper(self, name, *args): diff --git a/src/_pytest/assertion/util.py b/src/_pytest/assertion/util.py index b35b6abc5..6326dddbd 100644 --- a/src/_pytest/assertion/util.py +++ b/src/_pytest/assertion/util.py @@ -5,11 +5,11 @@ from __future__ import print_function import pprint -import py import six import _pytest._code from ..compat import Sequence +from _pytest._io.saferepr import saferepr # The _reprcompare attribute on the util module is used by the new assertion # interpretation code and assertion rewriter to detect this plugin was @@ -105,8 +105,8 @@ except NameError: def assertrepr_compare(config, op, left, right): """Return specialised explanations for some operators/operands""" width = 80 - 15 - len(op) - 2 # 15 chars indentation, 1 space around op - left_repr = py.io.saferepr(left, maxsize=int(width // 2)) - right_repr = py.io.saferepr(right, maxsize=width - len(left_repr)) + left_repr = saferepr(left, maxsize=int(width // 2)) + right_repr = saferepr(right, maxsize=width - len(left_repr)) summary = u"%s %s %s" % (ecu(left_repr), op, ecu(right_repr)) @@ -282,12 +282,12 @@ def _compare_eq_sequence(left, right, verbose=False): if len(left) > len(right): explanation += [ u"Left contains more items, first extra item: %s" - % py.io.saferepr(left[len(right)]) + % saferepr(left[len(right)]) ] elif len(left) < len(right): explanation += [ u"Right contains more items, first extra item: %s" - % py.io.saferepr(right[len(left)]) + % saferepr(right[len(left)]) ] return explanation @@ -299,11 +299,11 @@ def _compare_eq_set(left, right, verbose=False): if diff_left: explanation.append(u"Extra items in the left set:") for item in diff_left: - explanation.append(py.io.saferepr(item)) + explanation.append(saferepr(item)) if diff_right: explanation.append(u"Extra items in the right set:") for item in diff_right: - explanation.append(py.io.saferepr(item)) + explanation.append(saferepr(item)) return explanation @@ -320,9 +320,7 @@ def _compare_eq_dict(left, right, verbose=False): if diff: explanation += [u"Differing items:"] for k in diff: - explanation += [ - py.io.saferepr({k: left[k]}) + " != " + py.io.saferepr({k: right[k]}) - ] + explanation += [saferepr({k: left[k]}) + " != " + saferepr({k: right[k]})] extra_left = set(left) - set(right) if extra_left: explanation.append(u"Left contains more items:") @@ -376,7 +374,7 @@ def _notin_text(term, text, verbose=False): tail = text[index + len(term) :] correct_text = head + tail diff = _diff_text(correct_text, text, verbose) - newdiff = [u"%s is contained here:" % py.io.saferepr(term, maxsize=42)] + newdiff = [u"%s is contained here:" % saferepr(term, maxsize=42)] for line in diff: if line.startswith(u"Skipping"): continue diff --git a/src/_pytest/compat.py b/src/_pytest/compat.py index ff027f308..fa878a485 100644 --- a/src/_pytest/compat.py +++ b/src/_pytest/compat.py @@ -17,6 +17,7 @@ import six from six import text_type import _pytest +from _pytest._io.saferepr import saferepr from _pytest.outcomes import fail from _pytest.outcomes import TEST_OUTCOME @@ -294,7 +295,7 @@ def get_real_func(obj): else: raise ValueError( ("could not find real function of {start}\nstopped at {current}").format( - start=py.io.saferepr(start_obj), current=py.io.saferepr(obj) + start=saferepr(start_obj), current=saferepr(obj) ) ) if isinstance(obj, functools.partial): diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index c59628948..7e255dc9c 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -20,6 +20,7 @@ import six import pytest from _pytest._code import Source +from _pytest._io.saferepr import saferepr from _pytest.assertion.rewrite import AssertionRewritingHook from _pytest.capture import MultiCapture from _pytest.capture import SysCapture @@ -1225,9 +1226,7 @@ def getdecoded(out): try: return out.decode("utf-8") except UnicodeDecodeError: - return "INTERNAL not-utf8-decodeable, truncated string:\n%s" % ( - py.io.saferepr(out), - ) + return "INTERNAL not-utf8-decodeable, truncated string:\n%s" % (saferepr(out),) class LineComp(object):