add changelog: fix issue319 - correctly show unicode in assertion errors. Many

thanks to Floris Bruynooghe for the complete PR.  Also means
we depend on py>=1.4.19 now.
This commit is contained in:
holger krekel 2013-12-11 11:28:06 +01:00
commit fa80b8ad17
8 changed files with 97 additions and 50 deletions

View File

@ -13,6 +13,10 @@ Unreleased
- PR90: add --color=yes|no|auto option to force terminal coloring - PR90: add --color=yes|no|auto option to force terminal coloring
mode ("auto" is default). Thanks Marc Abramowitz. mode ("auto" is default). Thanks Marc Abramowitz.
- fix issue319 - correctly show unicode in assertion errors. Many
thanks to Floris Bruynooghe for the complete PR. Also means
we depend on py>=1.4.19 now.
- fix issue396 - correctly sort and finalize class-scoped parametrized - fix issue396 - correctly sort and finalize class-scoped parametrized
tests independently from number of methods on the class. tests independently from number of methods on the class.

View File

@ -1,2 +1,2 @@
# #
__version__ = '2.4.3.dev2' __version__ = '2.5.0.dev1'

View File

@ -78,10 +78,13 @@ def pytest_runtest_setup(item):
for new_expl in hook_result: for new_expl in hook_result:
if new_expl: if new_expl:
# Don't include pageloads of data unless we are very verbose (-vv) # Don't include pageloads of data unless we are very
if len(''.join(new_expl[1:])) > 80*8 and item.config.option.verbose < 2: # verbose (-vv)
new_expl[1:] = ['Detailed information truncated, use "-vv" to see'] if (len(py.builtin._totext('').join(new_expl[1:])) > 80*8
res = '\n~'.join(new_expl) and item.config.option.verbose < 2):
new_expl[1:] = [py.builtin._totext(
'Detailed information truncated, use "-vv" to see')]
res = py.builtin._totext('\n~').join(new_expl)
if item.config.getvalue("assertmode") == "rewrite": if item.config.getvalue("assertmode") == "rewrite":
# The result will be fed back a python % formatting # The result will be fed back a python % formatting
# operation, which will fail if there are extraneous # operation, which will fail if there are extraneous

View File

@ -2,17 +2,17 @@ import sys
import py import py
from _pytest.assertion.util import BuiltinAssertionError from _pytest.assertion.util import BuiltinAssertionError
class AssertionError(BuiltinAssertionError): class AssertionError(BuiltinAssertionError):
def __init__(self, *args): def __init__(self, *args):
BuiltinAssertionError.__init__(self, *args) BuiltinAssertionError.__init__(self, *args)
if args: if args:
try: try:
self.msg = str(args[0]) self.msg = py.builtin._totext(args[0])
except py.builtin._sysex: except Exception:
raise self.msg = py.builtin._totext(
except: "<[broken __repr__] %s at %0xd>"
self.msg = "<[broken __repr__] %s at %0xd>" %( % (args[0].__class__, id(args[0])))
args[0].__class__, id(args[0]))
else: else:
f = py.code.Frame(sys._getframe(1)) f = py.code.Frame(sys._getframe(1))
try: try:

View File

@ -655,7 +655,7 @@ class AssertionRewriter(ast.NodeVisitor):
res_expr = ast.Compare(left_res, [op], [next_res]) res_expr = ast.Compare(left_res, [op], [next_res])
self.statements.append(ast.Assign([store_names[i]], res_expr)) self.statements.append(ast.Assign([store_names[i]], res_expr))
left_res, left_expl = next_res, next_expl left_res, left_expl = next_res, next_expl
# Use py.code._reprcompare if that's available. # Use pytest.assertion.util._reprcompare if that's available.
expl_call = self.helper("call_reprcompare", expl_call = self.helper("call_reprcompare",
ast.Tuple(syms, ast.Load()), ast.Tuple(syms, ast.Load()),
ast.Tuple(load_names, ast.Load()), ast.Tuple(load_names, ast.Load()),

View File

@ -11,6 +11,7 @@ except ImportError:
BuiltinAssertionError = py.builtin.builtins.AssertionError BuiltinAssertionError = py.builtin.builtins.AssertionError
u = py.builtin._totext
# The _reprcompare attribute on the util module is used by the new assertion # The _reprcompare attribute on the util module is used by the new assertion
# interpretation code and assertion rewriter to detect this plugin was # interpretation code and assertion rewriter to detect this plugin was
@ -29,7 +30,18 @@ def format_explanation(explanation):
for when one explanation needs to span multiple lines, e.g. when for when one explanation needs to span multiple lines, e.g. when
displaying diffs. displaying diffs.
""" """
# simplify 'assert False where False = ...' explanation = _collapse_false(explanation)
lines = _split_explanation(explanation)
result = _format_lines(lines)
return u('\n').join(result)
def _collapse_false(explanation):
"""Collapse expansions of False
So this strips out any "assert False\n{where False = ...\n}"
blocks.
"""
where = 0 where = 0
while True: while True:
start = where = explanation.find("False\n{False = ", where) start = where = explanation.find("False\n{False = ", where)
@ -51,28 +63,48 @@ def format_explanation(explanation):
explanation = (explanation[:start] + explanation[start+15:end-1] + explanation = (explanation[:start] + explanation[start+15:end-1] +
explanation[end+1:]) explanation[end+1:])
where -= 17 where -= 17
raw_lines = (explanation or '').split('\n') return explanation
# escape newlines not followed by {, } and ~
def _split_explanation(explanation):
"""Return a list of individual lines in the explanation
This will return a list of lines split on '\n{', '\n}' and '\n~'.
Any other newlines will be escaped and appear in the line as the
literal '\n' characters.
"""
raw_lines = (explanation or u('')).split('\n')
lines = [raw_lines[0]] lines = [raw_lines[0]]
for l in raw_lines[1:]: for l in raw_lines[1:]:
if l.startswith('{') or l.startswith('}') or l.startswith('~'): if l.startswith('{') or l.startswith('}') or l.startswith('~'):
lines.append(l) lines.append(l)
else: else:
lines[-1] += '\\n' + l lines[-1] += '\\n' + l
return lines
def _format_lines(lines):
"""Format the individual lines
This will replace the '{', '}' and '~' characters of our mini
formatting language with the proper 'where ...', 'and ...' and ' +
...' text, taking care of indentation along the way.
Return a list of formatted lines.
"""
result = lines[:1] result = lines[:1]
stack = [0] stack = [0]
stackcnt = [0] stackcnt = [0]
for line in lines[1:]: for line in lines[1:]:
if line.startswith('{'): if line.startswith('{'):
if stackcnt[-1]: if stackcnt[-1]:
s = 'and ' s = u('and ')
else: else:
s = 'where ' s = u('where ')
stack.append(len(result)) stack.append(len(result))
stackcnt[-1] += 1 stackcnt[-1] += 1
stackcnt.append(0) stackcnt.append(0)
result.append(' +' + ' '*(len(stack)-1) + s + line[1:]) result.append(u(' +') + u(' ')*(len(stack)-1) + s + line[1:])
elif line.startswith('}'): elif line.startswith('}'):
assert line.startswith('}') assert line.startswith('}')
stack.pop() stack.pop()
@ -80,9 +112,9 @@ def format_explanation(explanation):
result[stack[-1]] += line[1:] result[stack[-1]] += line[1:]
else: else:
assert line.startswith('~') assert line.startswith('~')
result.append(' '*len(stack) + line[1:]) result.append(u(' ')*len(stack) + line[1:])
assert len(stack) == 1 assert len(stack) == 1
return '\n'.join(result) return result
# Provide basestring in python3 # Provide basestring in python3
@ -97,7 +129,7 @@ def assertrepr_compare(config, op, left, right):
width = 80 - 15 - len(op) - 2 # 15 chars indentation, 1 space around op width = 80 - 15 - len(op) - 2 # 15 chars indentation, 1 space around op
left_repr = py.io.saferepr(left, maxsize=int(width/2)) left_repr = py.io.saferepr(left, maxsize=int(width/2))
right_repr = py.io.saferepr(right, maxsize=width-len(left_repr)) right_repr = py.io.saferepr(right, maxsize=width-len(left_repr))
summary = '%s %s %s' % (left_repr, op, right_repr) summary = u('%s %s %s') % (left_repr, op, right_repr)
issequence = lambda x: (isinstance(x, (list, tuple, Sequence)) issequence = lambda x: (isinstance(x, (list, tuple, Sequence))
and not isinstance(x, basestring)) and not isinstance(x, basestring))
@ -120,13 +152,12 @@ def assertrepr_compare(config, op, left, right):
elif op == 'not in': elif op == 'not in':
if istext(left) and istext(right): if istext(left) and istext(right):
explanation = _notin_text(left, right, verbose) explanation = _notin_text(left, right, verbose)
except py.builtin._sysex: except Exception:
raise
except:
excinfo = py.code.ExceptionInfo() excinfo = py.code.ExceptionInfo()
explanation = [ explanation = [
'(pytest_assertion plugin: representation of details failed. ' u('(pytest_assertion plugin: representation of details failed. '
'Probably an object has a faulty __repr__.)', str(excinfo)] 'Probably an object has a faulty __repr__.)'),
u(excinfo)]
if not explanation: if not explanation:
return None return None
@ -148,8 +179,8 @@ def _diff_text(left, right, verbose=False):
break break
if i > 42: if i > 42:
i -= 10 # Provide some context i -= 10 # Provide some context
explanation = ['Skipping %s identical leading ' explanation = [u('Skipping %s identical leading '
'characters in diff, use -v to show' % i] 'characters in diff, use -v to show') % i]
left = left[i:] left = left[i:]
right = right[i:] right = right[i:]
if len(left) == len(right): if len(left) == len(right):
@ -158,8 +189,8 @@ def _diff_text(left, right, verbose=False):
break break
if i > 42: if i > 42:
i -= 10 # Provide some context i -= 10 # Provide some context
explanation += ['Skipping %s identical trailing ' explanation += [u('Skipping %s identical trailing '
'characters in diff, use -v to show' % i] 'characters in diff, use -v to show') % i]
left = left[:-i] left = left[:-i]
right = right[:-i] right = right[:-i]
explanation += [line.strip('\n') explanation += [line.strip('\n')
@ -172,16 +203,15 @@ def _compare_eq_sequence(left, right, verbose=False):
explanation = [] explanation = []
for i in range(min(len(left), len(right))): for i in range(min(len(left), len(right))):
if left[i] != right[i]: if left[i] != right[i]:
explanation += ['At index %s diff: %r != %r' % explanation += [u('At index %s diff: %r != %r')
(i, left[i], right[i])] % (i, left[i], right[i])]
break break
if len(left) > len(right): if len(left) > len(right):
explanation += [ explanation += [u('Left contains more items, first extra item: %s')
'Left contains more items, first extra item: %s' % % py.io.saferepr(left[len(right)],)]
py.io.saferepr(left[len(right)],)]
elif len(left) < len(right): elif len(left) < len(right):
explanation += [ explanation += [
'Right contains more items, first extra item: %s' % u('Right contains more items, first extra item: %s') %
py.io.saferepr(right[len(left)],)] py.io.saferepr(right[len(left)],)]
return explanation # + _diff_text(py.std.pprint.pformat(left), return explanation # + _diff_text(py.std.pprint.pformat(left),
# py.std.pprint.pformat(right)) # py.std.pprint.pformat(right))
@ -192,11 +222,11 @@ def _compare_eq_set(left, right, verbose=False):
diff_left = left - right diff_left = left - right
diff_right = right - left diff_right = right - left
if diff_left: if diff_left:
explanation.append('Extra items in the left set:') explanation.append(u('Extra items in the left set:'))
for item in diff_left: for item in diff_left:
explanation.append(py.io.saferepr(item)) explanation.append(py.io.saferepr(item))
if diff_right: if diff_right:
explanation.append('Extra items in the right set:') explanation.append(u('Extra items in the right set:'))
for item in diff_right: for item in diff_right:
explanation.append(py.io.saferepr(item)) explanation.append(py.io.saferepr(item))
return explanation return explanation
@ -207,25 +237,25 @@ def _compare_eq_dict(left, right, verbose=False):
common = set(left).intersection(set(right)) common = set(left).intersection(set(right))
same = dict((k, left[k]) for k in common if left[k] == right[k]) same = dict((k, left[k]) for k in common if left[k] == right[k])
if same and not verbose: if same and not verbose:
explanation += ['Omitting %s identical items, use -v to show' % explanation += [u('Omitting %s identical items, use -v to show') %
len(same)] len(same)]
elif same: elif same:
explanation += ['Common items:'] explanation += [u('Common items:')]
explanation += py.std.pprint.pformat(same).splitlines() explanation += py.std.pprint.pformat(same).splitlines()
diff = set(k for k in common if left[k] != right[k]) diff = set(k for k in common if left[k] != right[k])
if diff: if diff:
explanation += ['Differing items:'] explanation += [u('Differing items:')]
for k in diff: for k in diff:
explanation += [py.io.saferepr({k: left[k]}) + ' != ' + explanation += [py.io.saferepr({k: left[k]}) + ' != ' +
py.io.saferepr({k: right[k]})] py.io.saferepr({k: right[k]})]
extra_left = set(left) - set(right) extra_left = set(left) - set(right)
if extra_left: if extra_left:
explanation.append('Left contains more items:') explanation.append(u('Left contains more items:'))
explanation.extend(py.std.pprint.pformat( explanation.extend(py.std.pprint.pformat(
dict((k, left[k]) for k in extra_left)).splitlines()) dict((k, left[k]) for k in extra_left)).splitlines())
extra_right = set(right) - set(left) extra_right = set(right) - set(left)
if extra_right: if extra_right:
explanation.append('Right contains more items:') explanation.append(u('Right contains more items:'))
explanation.extend(py.std.pprint.pformat( explanation.extend(py.std.pprint.pformat(
dict((k, right[k]) for k in extra_right)).splitlines()) dict((k, right[k]) for k in extra_right)).splitlines())
return explanation return explanation
@ -237,14 +267,14 @@ def _notin_text(term, text, verbose=False):
tail = text[index+len(term):] tail = text[index+len(term):]
correct_text = head + tail correct_text = head + tail
diff = _diff_text(correct_text, text, verbose) diff = _diff_text(correct_text, text, verbose)
newdiff = ['%s is contained here:' % py.io.saferepr(term, maxsize=42)] newdiff = [u('%s is contained here:') % py.io.saferepr(term, maxsize=42)]
for line in diff: for line in diff:
if line.startswith('Skipping'): if line.startswith(u('Skipping')):
continue continue
if line.startswith('- '): if line.startswith(u('- ')):
continue continue
if line.startswith('+ '): if line.startswith(u('+ ')):
newdiff.append(' ' + line[2:]) newdiff.append(u(' ') + line[2:])
else: else:
newdiff.append(line) newdiff.append(line)
return newdiff return newdiff

View File

@ -17,7 +17,7 @@ classifiers=['Development Status :: 6 - Mature',
long_description = open("README.rst").read() long_description = open("README.rst").read()
def main(): def main():
install_requires = ["py>=1.4.17"] install_requires = ["py>=1.4.19"]
if sys.version_info < (2,7): if sys.version_info < (2,7):
install_requires.append("argparse") install_requires.append("argparse")
if sys.platform == "win32": if sys.platform == "win32":
@ -27,7 +27,7 @@ def main():
name='pytest', name='pytest',
description='py.test: simple powerful testing with Python', description='py.test: simple powerful testing with Python',
long_description = long_description, long_description = long_description,
version='2.4.3.dev2', version='2.5.0.dev1',
url='http://pytest.org', url='http://pytest.org',
license='MIT license', license='MIT license',
platforms=['unix', 'linux', 'osx', 'cygwin', 'win32'], platforms=['unix', 'linux', 'osx', 'cygwin', 'win32'],

View File

@ -1,3 +1,4 @@
# -*- coding: utf-8 -*-
import sys import sys
import py, pytest import py, pytest
@ -176,6 +177,15 @@ class TestAssert_reprcompare:
expl = ' '.join(callequal('foo', 'bar')) expl = ' '.join(callequal('foo', 'bar'))
assert 'raised in repr()' not in expl assert 'raised in repr()' not in expl
def test_unicode(self):
left = py.builtin._totext('£€', 'utf-8')
right = py.builtin._totext('£', 'utf-8')
expl = callequal(left, right)
assert expl[0] == py.builtin._totext("'£€' == '£'", 'utf-8')
assert expl[1] == py.builtin._totext('- £€', 'utf-8')
assert expl[2] == py.builtin._totext('+ £', 'utf-8')
def test_python25_compile_issue257(testdir): def test_python25_compile_issue257(testdir):
testdir.makepyfile(""" testdir.makepyfile("""
def test_rewritten(): def test_rewritten():