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:
		
						commit
						fa80b8ad17
					
				|  | @ -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.   | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,2 +1,2 @@ | ||||||
| # | # | ||||||
| __version__ = '2.4.3.dev2' | __version__ = '2.5.0.dev1' | ||||||
|  |  | ||||||
|  | @ -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 | ||||||
|  |  | ||||||
|  | @ -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: | ||||||
|  |  | ||||||
|  | @ -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()), | ||||||
|  |  | ||||||
|  | @ -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 | ||||||
|  |  | ||||||
							
								
								
									
										4
									
								
								setup.py
								
								
								
								
							
							
						
						
									
										4
									
								
								setup.py
								
								
								
								
							|  | @ -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'], | ||||||
|  |  | ||||||
|  | @ -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(): | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue