Python2.xify the code base.

Skip a few tests that test the core interpreter and don't work on <3.
Unicodify the core.
Fix tests to deal with Python 2's behaviour (stdout not encoded at
all) when stdout is a pipe.
This commit is contained in:
Robert Collins 2014-11-24 21:49:37 +13:00
parent 43118d83bc
commit 9a9a29e95f
3 changed files with 57 additions and 30 deletions

View File

@ -3,3 +3,10 @@ A backport of traceback to older supported Pythons.
>>> import traceback2 as traceback >>> import traceback2 as traceback
Profit. Profit.
Things to be aware of!
In Python 2.x, unlike traceback, traceback2 creates unicode output.
Exception frame clearing silently does nothing if the interpreter in use does
not support it.

View File

@ -4,6 +4,8 @@ import linecache
import sys import sys
import operator import operator
from six import u
__all__ = ['extract_stack', 'extract_tb', 'format_exception', __all__ = ['extract_stack', 'extract_tb', 'format_exception',
'format_exception_only', 'format_list', 'format_stack', 'format_exception_only', 'format_list', 'format_stack',
'format_tb', 'print_exc', 'format_exc', 'print_exception', 'format_tb', 'print_exc', 'format_exc', 'print_exception',
@ -16,9 +18,9 @@ __all__ = ['extract_stack', 'extract_tb', 'format_exception',
def _format_list_iter(extracted_list): def _format_list_iter(extracted_list):
for filename, lineno, name, line in extracted_list: for filename, lineno, name, line in extracted_list:
item = ' File "{0}", line {1}, in {2}\n'.format(filename, lineno, name) item = u(' File "{0}", line {1}, in {2}\n').format(filename, lineno, name)
if line: if line:
item = item + ' {0}\n'.format(line.strip()) item = item + u(' {0}\n').format(line.strip())
yield item yield item
def print_list(extracted_list, file=None): def print_list(extracted_list, file=None):
@ -147,10 +149,10 @@ def _format_exception_iter(etype, value, tb, limit, chain):
for value, tb in values: for value, tb in values:
if isinstance(value, str): if isinstance(value, str):
# This is a cause/context message line # This is a cause/context message line
yield value + '\n' yield value + u('\n')
continue continue
if tb: if tb:
yield 'Traceback (most recent call last):\n' yield u('Traceback (most recent call last):\n')
for it in _format_list_iter(_extract_tb_iter(tb, limit=limit)): for it in _format_list_iter(_extract_tb_iter(tb, limit=limit)):
yield it yield it
for it in _format_exception_only_iter(type(value), value): for it in _format_exception_only_iter(type(value), value):
@ -209,32 +211,32 @@ def _format_exception_only_iter(etype, value):
return return
stype = getattr(etype, '__qualname__', etype.__name__) stype = getattr(etype, '__qualname__', etype.__name__)
smod = etype.__module__ smod = u(etype.__module__)
if smod not in ("__main__", "builtins"): if smod not in ("__main__", "builtins"):
stype = smod + '.' + stype stype = smod + u('.') + stype
if not issubclass(etype, SyntaxError): if not issubclass(etype, SyntaxError):
yield _format_final_exc_line(stype, value) yield _format_final_exc_line(stype, value)
return return
# It was a syntax error; show exactly where the problem was found. # It was a syntax error; show exactly where the problem was found.
filename = value.filename or "<string>" filename = value.filename or u("<string>")
lineno = str(value.lineno) or '?' lineno = str(value.lineno) or u('?')
yield ' File "{0}", line {1}\n'.format(filename, lineno) yield u(' File "{0}", line {1}\n').format(filename, lineno)
badline = value.text badline = value.text
offset = value.offset offset = value.offset
if badline is not None: if badline is not None:
yield ' {0}\n'.format(badline.strip()) yield u(' {0}\n').format(u(badline).strip())
if offset is not None: if offset is not None:
caretspace = badline.rstrip('\n') caretspace = u(badline).rstrip('\n')
offset = min(len(caretspace), offset) - 1 offset = min(len(caretspace), offset) - 1
caretspace = caretspace[:offset].lstrip() caretspace = caretspace[:offset].lstrip()
# non-space whitespace (likes tabs) must be kept for alignment # non-space whitespace (likes tabs) must be kept for alignment
caretspace = ((c.isspace() and c or ' ') for c in caretspace) caretspace = ((c.isspace() and c or ' ') for c in caretspace)
yield ' {0}^\n'.format(''.join(caretspace)) yield u(' {0}^\n'.format(''.join(caretspace)))
msg = value.msg or "<no detail available>" msg = value.msg or "<no detail available>"
yield "{0}: {1}\n".format(stype, msg) yield u("{0}: {1}\n").format(stype, msg)
def _format_final_exc_line(etype, value): def _format_final_exc_line(etype, value):
valuestr = _some_str(value) valuestr = _some_str(value)

View File

@ -1,6 +1,7 @@
"""Test cases for traceback module""" """Test cases for traceback module"""
import doctest import doctest
import io
from io import StringIO from io import StringIO
import sys import sys
import re import re
@ -8,6 +9,7 @@ import re
import contextlib2 as contextlib import contextlib2 as contextlib
import fixtures import fixtures
import six import six
from six import text_type, u
try: try:
from six import raise_from from six import raise_from
except ImportError: except ImportError:
@ -144,21 +146,23 @@ class SyntaxTracebackCases(testtools.TestCase):
stdout=subprocess.PIPE, stdout=subprocess.PIPE,
stderr=subprocess.STDOUT) stderr=subprocess.STDOUT)
stdout, stderr = process.communicate() stdout, stderr = process.communicate()
output_encoding = str(stdout, 'ascii').splitlines()[0] output_encoding = text_type(stdout, 'ascii').splitlines()[0]
def do_test(firstlines, message, charset, lineno): def do_test(firstlines, message, charset, lineno, output_encoding):
# Raise the message in a subprocess, and catch the output # Raise the message in a subprocess, and catch the output
with fixtures.TempDir() as d: with fixtures.TempDir() as d:
TESTFN = d.path + '/fname' TESTFN = d.path + '/fname'
output = open(TESTFN, "w", encoding=charset) output = io.open(TESTFN, "w", encoding=charset)
output.write("""{0}if 1: output.write(u("""{0}if 1:
import traceback; import traceback;
raise RuntimeError('{1}') raise RuntimeError('{1}')
""".format(firstlines, message)) """).format(firstlines, message))
output.close() output.close()
process = subprocess.Popen([sys.executable, TESTFN], process = subprocess.Popen([sys.executable, TESTFN],
stdout=subprocess.PIPE, stderr=subprocess.STDOUT) stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
stdout, stderr = process.communicate() stdout, stderr = process.communicate()
if output_encoding == 'None':
output_encoding = charset
stdout = stdout.decode(output_encoding).splitlines() stdout = stdout.decode(output_encoding).splitlines()
# The source lines are encoded with the 'backslashreplace' handler # The source lines are encoded with the 'backslashreplace' handler
@ -167,8 +171,8 @@ class SyntaxTracebackCases(testtools.TestCase):
# and we just decoded them with the output_encoding. # and we just decoded them with the output_encoding.
message_ascii = encoded_message.decode(output_encoding) message_ascii = encoded_message.decode(output_encoding)
err_line = "raise RuntimeError('{0}')".format(message_ascii) err_line = u("raise RuntimeError('{0}')").format(message_ascii)
err_msg = "RuntimeError: {0}".format(message_ascii) err_msg = u("RuntimeError: {0}").format(message_ascii)
self.assertIn(("line %s" % lineno), stdout[1], self.assertIn(("line %s" % lineno), stdout[1],
"Invalid line number: {0!r} instead of {1}".format( "Invalid line number: {0!r} instead of {1}".format(
@ -180,24 +184,26 @@ class SyntaxTracebackCases(testtools.TestCase):
"Invalid error message: {0!r} instead of {1!r}".format( "Invalid error message: {0!r} instead of {1!r}".format(
stdout[3], err_msg)) stdout[3], err_msg))
do_test("", "foo", "ascii", 3) do_test("", "foo", "ascii", 3, output_encoding)
for charset in ("ascii", "iso-8859-1", "utf-8", "GBK"): for charset in ("ascii", "iso-8859-1", "utf-8", "GBK"):
if charset == "ascii": if charset == "ascii":
text = "foo" text = u("foo")
elif charset == "GBK": elif charset == "GBK":
text = "\u4E02\u5100" text = u("\u4E02\u5100")
else: else:
text = "h\xe9 ho" text = u("h\xe9 ho")
do_test("# coding: {0}\n".format(charset), do_test("# coding: {0}\n".format(charset),
text, charset, 4) text, charset, 4, output_encoding)
do_test("#!shebang\n# coding: {0}\n".format(charset), do_test("#!shebang\n# coding: {0}\n".format(charset),
text, charset, 5) text, charset, 5, output_encoding)
do_test(" \t\f\n# coding: {0}\n".format(charset), do_test(" \t\f\n# coding: {0}\n".format(charset),
text, charset, 5) text, charset, 5, output_encoding)
# Issue #18960: coding spec should has no effect # Issue #18960: coding spec should has no effect
# (Fixed in 3.4) # (Fixed in 3.4)
if sys.version_info[:2] > (3, 3): if sys.version_info[:2] > (3, 3):
do_test("0\n# coding: GBK\n", "h\xe9 ho", 'utf-8', 5) do_test(
"0\n# coding: GBK\n", u("h\xe9 ho"), 'utf-8', 5,
output_encoding)
class TracebackFormatTests(unittest.TestCase): class TracebackFormatTests(unittest.TestCase):
@ -207,6 +213,14 @@ class TracebackFormatTests(unittest.TestCase):
def check_traceback_format(self, cleanup_func=None): def check_traceback_format(self, cleanup_func=None):
try: try:
if issubclass(six.binary_type, six.string_types):
# Python 2.6 or other platform where the interpreter
# is likely going to be spitting out bytes, which will
# then fail with io.StringIO(), so we skip the cross-
# checks with the C API there. Note that _testcapi
# is included in (at least) Ubuntu CPython packages, which
# makes the import check less effective than desired.
raise ImportError
from _testcapi import traceback_print from _testcapi import traceback_print
except ImportError: except ImportError:
traceback_print = None traceback_print = None
@ -217,8 +231,8 @@ class TracebackFormatTests(unittest.TestCase):
if cleanup_func is not None: if cleanup_func is not None:
# Clear the inner frames, not this one # Clear the inner frames, not this one
cleanup_func(tb.tb_next) cleanup_func(tb.tb_next)
traceback_fmt = 'Traceback (most recent call last):\n' + \ traceback_fmt = u('Traceback (most recent call last):\n') + \
''.join(traceback.format_tb(tb)) u('').join(traceback.format_tb(tb))
if traceback_print is not None: if traceback_print is not None:
file_ = StringIO() file_ = StringIO()
traceback_print(tb, file_) traceback_print(tb, file_)
@ -323,6 +337,7 @@ class BaseExceptionReportingTests:
# < 3 show as exceptions.ZeroDivisionError. # < 3 show as exceptions.ZeroDivisionError.
self.assertIn('ZeroDivisionError', lines[3]) self.assertIn('ZeroDivisionError', lines[3])
@unittest.skipIf(sys.version_info[:2] < (3, 2), "Only applies to 3.2+")
def test_cause(self): def test_cause(self):
def inner_raise(): def inner_raise():
try: try:
@ -337,6 +352,7 @@ class BaseExceptionReportingTests:
self.check_zero_div(blocks[0]) self.check_zero_div(blocks[0])
self.assertIn('inner_raise() # Marker', blocks[2]) self.assertIn('inner_raise() # Marker', blocks[2])
@unittest.skipIf(sys.version_info[:2] < (3, 2), "Only applies to 3.2+")
def test_context(self): def test_context(self):
def inner_raise(): def inner_raise():
try: try:
@ -369,6 +385,7 @@ Traceback (most recent call last):
ZeroDivisionError ZeroDivisionError
""", doctest.ELLIPSIS)) """, doctest.ELLIPSIS))
@unittest.skipIf(sys.version_info[:2] < (3, 2), "Only applies to 3.2+")
def test_cause_and_context(self): def test_cause_and_context(self):
# When both a cause and a context are set, only the cause should be # When both a cause and a context are set, only the cause should be
# displayed and the context should be muted. # displayed and the context should be muted.
@ -389,6 +406,7 @@ ZeroDivisionError
self.check_zero_div(blocks[0]) self.check_zero_div(blocks[0])
self.assertIn('inner_raise() # Marker', blocks[2]) self.assertIn('inner_raise() # Marker', blocks[2])
@unittest.skipIf(sys.version_info[:2] < (3, 2), "Only applies to 3.2+")
def test_cause_recursive(self): def test_cause_recursive(self):
def inner_raise(): def inner_raise():
try: try: