From e65282114e96efb9e7eee77c57244943b746f6fe Mon Sep 17 00:00:00 2001 From: Benjamin Peterson Date: Tue, 15 Jul 2008 15:32:09 +0000 Subject: [PATCH] implement chained exception tracebacks patch from Antoine Pitrou #3112 --- Include/traceback.h | 2 +- Lib/test/test_raise.py | 24 ++++ Lib/test/test_traceback.py | 136 ++++++++++++++++++- Lib/traceback.py | 82 +++++++++--- Misc/NEWS | 4 + Modules/_testcapimodule.c | 21 +++ Python/_warnings.c | 3 +- Python/errors.c | 22 +++- Python/pythonrun.c | 261 +++++++++++++++++++++++-------------- Python/traceback.c | 27 +++- 10 files changed, 449 insertions(+), 133 deletions(-) diff --git a/Include/traceback.h b/Include/traceback.h index b77cc9202b4..0be3ad53b60 100644 --- a/Include/traceback.h +++ b/Include/traceback.h @@ -19,7 +19,7 @@ typedef struct _traceback { PyAPI_FUNC(int) PyTraceBack_Here(struct _frame *); PyAPI_FUNC(int) PyTraceBack_Print(PyObject *, PyObject *); -PyAPI_FUNC(int) Py_DisplaySourceLine(PyObject *, const char *, int); +PyAPI_FUNC(int) Py_DisplaySourceLine(PyObject *, const char *, int, int); /* Reveal traceback type so we can typecheck traceback objects */ PyAPI_DATA(PyTypeObject) PyTraceBack_Type; diff --git a/Lib/test/test_raise.py b/Lib/test/test_raise.py index 3072c14c889..ba9cfc54d66 100644 --- a/Lib/test/test_raise.py +++ b/Lib/test/test_raise.py @@ -278,6 +278,30 @@ class TestContext(unittest.TestCase): else: self.fail("No exception raised") + def test_cycle_broken(self): + # Self-cycles (when re-raising a caught exception) are broken + try: + try: + 1/0 + except ZeroDivisionError as e: + raise e + except ZeroDivisionError as e: + self.failUnless(e.__context__ is None, e.__context__) + + def test_reraise_cycle_broken(self): + # Non-trivial context cycles (through re-raising a previous exception) + # are broken too. + try: + try: + xyzzy + except NameError as a: + try: + 1/0 + except ZeroDivisionError: + raise a + except NameError as e: + self.failUnless(e.__context__.__context__ is None) + class TestRemovedFunctionality(unittest.TestCase): def test_tuples(self): diff --git a/Lib/test/test_traceback.py b/Lib/test/test_traceback.py index 3f89e6a795b..3f69e5e8935 100644 --- a/Lib/test/test_traceback.py +++ b/Lib/test/test_traceback.py @@ -1,10 +1,11 @@ """Test cases for traceback module""" -from _testcapi import traceback_print +from _testcapi import traceback_print, exception_print from io import StringIO import sys import unittest -from test.support import run_unittest, is_jython, Error +import re +from test.support import run_unittest, is_jython, Error, captured_output import traceback @@ -19,7 +20,7 @@ else: raise Error("unable to create test traceback string") -class TracebackCases(unittest.TestCase): +class SyntaxTracebackCases(unittest.TestCase): # For now, a very minimal set of tests. I want to be sure that # formatting of SyntaxErrors works based on changes for 2.1. @@ -99,12 +100,135 @@ class TracebackFormatTests(unittest.TestCase): banner, location, source_line = tb_lines self.assert_(banner.startswith('Traceback')) self.assert_(location.startswith(' File')) - self.assert_(source_line.startswith('raise')) + self.assert_(source_line.startswith(' raise')) + + +cause_message = ( + "\nThe above exception was the direct cause " + "of the following exception:\n\n") + +context_message = ( + "\nDuring handling of the above exception, " + "another exception occurred:\n\n") + +boundaries = re.compile( + '(%s|%s)' % (re.escape(cause_message), re.escape(context_message))) + + +class BaseExceptionReportingTests: + + def get_exception(self, exception_or_callable): + if isinstance(exception_or_callable, Exception): + return exception_or_callable + try: + exception_or_callable() + except Exception as e: + return e + + def zero_div(self): + 1/0 # In zero_div + + def check_zero_div(self, msg): + lines = msg.splitlines() + self.assert_(lines[-3].startswith(' File')) + self.assert_('1/0 # In zero_div' in lines[-2], lines[-2]) + self.assert_(lines[-1].startswith('ZeroDivisionError'), lines[-1]) + + def test_simple(self): + try: + 1/0 # Marker + except ZeroDivisionError as _: + e = _ + lines = self.get_report(e).splitlines() + self.assertEquals(len(lines), 4) + self.assert_(lines[0].startswith('Traceback')) + self.assert_(lines[1].startswith(' File')) + self.assert_('1/0 # Marker' in lines[2]) + self.assert_(lines[3].startswith('ZeroDivisionError')) + + def test_cause(self): + def inner_raise(): + try: + self.zero_div() + except ZeroDivisionError as e: + raise KeyError from e + def outer_raise(): + inner_raise() # Marker + blocks = boundaries.split(self.get_report(outer_raise)) + self.assertEquals(len(blocks), 3) + self.assertEquals(blocks[1], cause_message) + self.check_zero_div(blocks[0]) + self.assert_('inner_raise() # Marker' in blocks[2]) + + def test_context(self): + def inner_raise(): + try: + self.zero_div() + except ZeroDivisionError: + raise KeyError + def outer_raise(): + inner_raise() # Marker + blocks = boundaries.split(self.get_report(outer_raise)) + self.assertEquals(len(blocks), 3) + self.assertEquals(blocks[1], context_message) + self.check_zero_div(blocks[0]) + self.assert_('inner_raise() # Marker' in blocks[2]) + + def test_cause_recursive(self): + def inner_raise(): + try: + try: + self.zero_div() + except ZeroDivisionError as e: + z = e + raise KeyError from e + except KeyError as e: + raise z from e + def outer_raise(): + inner_raise() # Marker + blocks = boundaries.split(self.get_report(outer_raise)) + self.assertEquals(len(blocks), 3) + self.assertEquals(blocks[1], cause_message) + # The first block is the KeyError raised from the ZeroDivisionError + self.assert_('raise KeyError from e' in blocks[0]) + self.assert_('1/0' not in blocks[0]) + # The second block (apart from the boundary) is the ZeroDivisionError + # re-raised from the KeyError + self.assert_('inner_raise() # Marker' in blocks[2]) + self.check_zero_div(blocks[2]) + + + +class PyExcReportingTests(BaseExceptionReportingTests, unittest.TestCase): + # + # This checks reporting through the 'traceback' module, with both + # format_exception() and print_exception(). + # + + def get_report(self, e): + e = self.get_exception(e) + s = ''.join( + traceback.format_exception(type(e), e, e.__traceback__)) + with captured_output("stderr") as sio: + traceback.print_exception(type(e), e, e.__traceback__) + self.assertEquals(sio.getvalue(), s) + return s + + +class CExcReportingTests(BaseExceptionReportingTests, unittest.TestCase): + # + # This checks built-in reporting by the interpreter. + # + + def get_report(self, e): + e = self.get_exception(e) + with captured_output("stderr") as s: + exception_print(e) + return s.getvalue() def test_main(): - run_unittest(TracebackCases, TracebackFormatTests) - + run_unittest(__name__) if __name__ == "__main__": test_main() diff --git a/Lib/traceback.py b/Lib/traceback.py index fb1c5addd88..b7130d899f7 100644 --- a/Lib/traceback.py +++ b/Lib/traceback.py @@ -3,6 +3,7 @@ import linecache import sys import types +import itertools __all__ = ['extract_stack', 'extract_tb', 'format_exception', 'format_exception_only', 'format_list', 'format_stack', @@ -107,7 +108,32 @@ def extract_tb(tb, limit = None): return list -def print_exception(etype, value, tb, limit=None, file=None): +_cause_message = ( + "\nThe above exception was the direct cause " + "of the following exception:\n") + +_context_message = ( + "\nDuring handling of the above exception, " + "another exception occurred:\n") + +def _iter_chain(exc, custom_tb=None, seen=None): + if seen is None: + seen = set() + seen.add(exc) + its = [] + cause = exc.__cause__ + context = exc.__context__ + if cause is not None and cause not in seen: + its.append(_iter_chain(cause, None, seen)) + its.append([(_cause_message, None)]) + if context is not None and context is not cause and context not in seen: + its.append(_iter_chain(context, None, seen)) + its.append([(_context_message, None)]) + its.append([(exc, custom_tb or exc.__traceback__)]) + return itertools.chain(*its) + + +def print_exception(etype, value, tb, limit=None, file=None, chain=True): """Print exception up to 'limit' stack trace entries from 'tb' to 'file'. This differs from print_tb() in the following ways: (1) if @@ -120,15 +146,23 @@ def print_exception(etype, value, tb, limit=None, file=None): """ if file is None: file = sys.stderr - if tb: - _print(file, 'Traceback (most recent call last):') - print_tb(tb, limit, file) - lines = format_exception_only(etype, value) - for line in lines[:-1]: - _print(file, line, ' ') - _print(file, lines[-1], '') + if chain: + values = _iter_chain(value, tb) + else: + values = [(value, tb)] + for value, tb in values: + if isinstance(value, str): + _print(file, value) + continue + if tb: + _print(file, 'Traceback (most recent call last):') + print_tb(tb, limit, file) + lines = format_exception_only(type(value), value) + for line in lines[:-1]: + _print(file, line, ' ') + _print(file, lines[-1], '') -def format_exception(etype, value, tb, limit = None): +def format_exception(etype, value, tb, limit=None, chain=True): """Format a stack trace and the exception information. The arguments have the same meaning as the corresponding arguments @@ -137,12 +171,19 @@ def format_exception(etype, value, tb, limit = None): these lines are concatenated and printed, exactly the same text is printed as does print_exception(). """ - if tb: - list = ['Traceback (most recent call last):\n'] - list = list + format_tb(tb, limit) + list = [] + if chain: + values = _iter_chain(value, tb) else: - list = [] - list = list + format_exception_only(etype, value) + values = [(value, tb)] + for value, tb in values: + if isinstance(value, str): + list.append(value + '\n') + continue + if tb: + list.append('Traceback (most recent call last):\n') + list.extend(format_tb(tb, limit)) + list.extend(format_exception_only(type(value), value)) return list def format_exception_only(etype, value): @@ -208,33 +249,34 @@ def _some_str(value): return '' % type(value).__name__ -def print_exc(limit=None, file=None): +def print_exc(limit=None, file=None, chain=True): """Shorthand for 'print_exception(*sys.exc_info(), limit, file)'.""" if file is None: file = sys.stderr try: etype, value, tb = sys.exc_info() - print_exception(etype, value, tb, limit, file) + print_exception(etype, value, tb, limit, file, chain) finally: etype = value = tb = None -def format_exc(limit=None): +def format_exc(limit=None, chain=True): """Like print_exc() but return a string.""" try: etype, value, tb = sys.exc_info() - return ''.join(format_exception(etype, value, tb, limit)) + return ''.join( + format_exception(etype, value, tb, limit, chain)) finally: etype = value = tb = None -def print_last(limit=None, file=None): +def print_last(limit=None, file=None, chain=True): """This is a shorthand for 'print_exception(sys.last_type, sys.last_value, sys.last_traceback, limit, file)'.""" if file is None: file = sys.stderr print_exception(sys.last_type, sys.last_value, sys.last_traceback, - limit, file) + limit, file, chain) def print_stack(f=None, limit=None, file=None): diff --git a/Misc/NEWS b/Misc/NEWS index e4178c098b0..a6ecc0eb020 100644 --- a/Misc/NEWS +++ b/Misc/NEWS @@ -22,6 +22,8 @@ Core and Builtins - Issue #3236: Return small longs from PyLong_FromString. +- Exception tracebacks now support exception chaining. + Library ------- @@ -35,6 +37,8 @@ Library - All the u* variant functions and methods in gettext have been renamed to their none u* siblings. +- The traceback module has been expanded to handle chained exceptions. + C API ----- diff --git a/Modules/_testcapimodule.c b/Modules/_testcapimodule.c index d8bb835b4ff..45494dd5c68 100644 --- a/Modules/_testcapimodule.c +++ b/Modules/_testcapimodule.c @@ -951,6 +951,26 @@ traceback_print(PyObject *self, PyObject *args) Py_RETURN_NONE; } +/* To test the format of exceptions as printed out. */ +static PyObject * +exception_print(PyObject *self, PyObject *args) +{ + PyObject *value; + PyObject *tb; + + if (!PyArg_ParseTuple(args, "O:exception_print", + &value)) + return NULL; + + tb = PyException_GetTraceback(value); + PyErr_Display((PyObject *) Py_TYPE(value), value, tb); + Py_XDECREF(tb); + + Py_RETURN_NONE; +} + + + static PyMethodDef TestMethods[] = { {"raise_exception", raise_exception, METH_VARARGS}, {"test_config", (PyCFunction)test_config, METH_NOARGS}, @@ -995,6 +1015,7 @@ static PyMethodDef TestMethods[] = { {"profile_int", profile_int, METH_NOARGS}, #endif {"traceback_print", traceback_print, METH_VARARGS}, + {"exception_print", exception_print, METH_VARARGS}, {NULL, NULL} /* sentinel */ }; diff --git a/Python/_warnings.c b/Python/_warnings.c index 23223faa1bd..e9384ca1f9c 100644 --- a/Python/_warnings.c +++ b/Python/_warnings.c @@ -256,7 +256,6 @@ show_warning(PyObject *filename, int lineno, PyObject *text, PyObject Py_XDECREF(name); /* Print " source_line\n" */ - PyFile_WriteString(" ", f_stderr); if (sourceline) { char *source_line_str = PyUnicode_AsString(sourceline); while (*source_line_str == ' ' || *source_line_str == '\t' || @@ -267,7 +266,7 @@ show_warning(PyObject *filename, int lineno, PyObject *text, PyObject PyFile_WriteString("\n", f_stderr); } else - Py_DisplaySourceLine(f_stderr, PyUnicode_AsString(filename), lineno); + Py_DisplaySourceLine(f_stderr, PyUnicode_AsString(filename), lineno, 2); PyErr_Clear(); } diff --git a/Python/errors.c b/Python/errors.c index ac64b6a2bc1..3b86c48bfbc 100644 --- a/Python/errors.c +++ b/Python/errors.c @@ -84,8 +84,23 @@ PyErr_SetObject(PyObject *exception, PyObject *value) return; value = fixed_value; } - Py_INCREF(tstate->exc_value); - PyException_SetContext(value, tstate->exc_value); + /* Avoid reference cycles through the context chain. + This is O(chain length) but context chains are + usually very short. Sensitive readers may try + to inline the call to PyException_GetContext. */ + if (tstate->exc_value != value) { + PyObject *o = tstate->exc_value, *context; + while ((context = PyException_GetContext(o))) { + Py_DECREF(context); + if (context == value) { + PyException_SetContext(o, NULL); + break; + } + o = context; + } + Py_INCREF(tstate->exc_value); + PyException_SetContext(value, tstate->exc_value); + } } if (value != NULL && PyExceptionInstance_Check(value)) tb = PyException_GetTraceback(value); @@ -160,6 +175,9 @@ PyErr_ExceptionMatches(PyObject *exc) /* Used in many places to normalize a raised exception, including in eval_code2(), do_raise(), and PyErr_Print() + + XXX: should PyErr_NormalizeException() also call + PyException_SetTraceback() with the resulting value and tb? */ void PyErr_NormalizeException(PyObject **exc, PyObject **val, PyObject **tb) diff --git a/Python/pythonrun.c b/Python/pythonrun.c index c46e9f4cc17..ad758a6fd97 100644 --- a/Python/pythonrun.c +++ b/Python/pythonrun.c @@ -1242,18 +1242,19 @@ PyErr_PrintEx(int set_sys_last_vars) if (exception == NULL) return; PyErr_NormalizeException(&exception, &v, &tb); + tb = tb ? tb : Py_None; + PyException_SetTraceback(v, tb); if (exception == NULL) return; /* Now we know v != NULL too */ if (set_sys_last_vars) { PySys_SetObject("last_type", exception); PySys_SetObject("last_value", v); - PySys_SetObject("last_traceback", tb ? tb : Py_None); + PySys_SetObject("last_traceback", tb); } hook = PySys_GetObject("excepthook"); if (hook) { - PyObject *args = PyTuple_Pack(3, - exception, v, tb ? tb : Py_None); + PyObject *args = PyTuple_Pack(3, exception, v, tb); PyObject *result = PyEval_CallObject(hook, args); if (result == NULL) { PyObject *exception2, *v2, *tb2; @@ -1293,12 +1294,164 @@ PyErr_PrintEx(int set_sys_last_vars) Py_XDECREF(tb); } +static void +print_exception(PyObject *f, PyObject *value) +{ + int err = 0; + PyObject *type, *tb; + + Py_INCREF(value); + fflush(stdout); + type = (PyObject *) Py_TYPE(value); + tb = PyException_GetTraceback(value); + if (tb && tb != Py_None) + err = PyTraceBack_Print(tb, f); + if (err == 0 && + PyObject_HasAttrString(value, "print_file_and_line")) + { + PyObject *message; + const char *filename, *text; + int lineno, offset; + if (!parse_syntax_error(value, &message, &filename, + &lineno, &offset, &text)) + PyErr_Clear(); + else { + char buf[10]; + PyFile_WriteString(" File \"", f); + if (filename == NULL) + PyFile_WriteString("", f); + else + PyFile_WriteString(filename, f); + PyFile_WriteString("\", line ", f); + PyOS_snprintf(buf, sizeof(buf), "%d", lineno); + PyFile_WriteString(buf, f); + PyFile_WriteString("\n", f); + if (text != NULL) + print_error_text(f, offset, text); + Py_DECREF(value); + value = message; + /* Can't be bothered to check all those + PyFile_WriteString() calls */ + if (PyErr_Occurred()) + err = -1; + } + } + if (err) { + /* Don't do anything else */ + } + else { + assert(PyExceptionClass_Check(type)); + PyObject* moduleName; + char* className = PyExceptionClass_Name(type); + if (className != NULL) { + char *dot = strrchr(className, '.'); + if (dot != NULL) + className = dot+1; + } + + moduleName = PyObject_GetAttrString(type, "__module__"); + if (moduleName == NULL || !PyUnicode_Check(moduleName)) + { + Py_DECREF(moduleName); + err = PyFile_WriteString("", f); + } + else { + char* modstr = PyUnicode_AsString(moduleName); + if (modstr && strcmp(modstr, "builtins")) + { + err = PyFile_WriteString(modstr, f); + err += PyFile_WriteString(".", f); + } + Py_DECREF(moduleName); + } + if (err == 0) { + if (className == NULL) + err = PyFile_WriteString("", f); + else + err = PyFile_WriteString(className, f); + } + } + if (err == 0 && (value != Py_None)) { + PyObject *s = PyObject_Str(value); + /* only print colon if the str() of the + object is not the empty string + */ + if (s == NULL) + err = -1; + else if (!PyUnicode_Check(s) || + PyUnicode_GetSize(s) != 0) + err = PyFile_WriteString(": ", f); + if (err == 0) + err = PyFile_WriteObject(s, f, Py_PRINT_RAW); + Py_XDECREF(s); + } + /* try to write a newline in any case */ + err += PyFile_WriteString("\n", f); + Py_XDECREF(tb); + Py_DECREF(value); + /* If an error happened here, don't show it. + XXX This is wrong, but too many callers rely on this behavior. */ + if (err != 0) + PyErr_Clear(); +} + +static const char *cause_message = + "\nThe above exception was the direct cause " + "of the following exception:\n\n"; + +static const char *context_message = + "\nDuring handling of the above exception, " + "another exception occurred:\n\n"; + +static void +print_exception_recursive(PyObject *f, PyObject *value, PyObject *seen) +{ + int err = 0, res; + PyObject *cause, *context; + + if (seen != NULL) { + /* Exception chaining */ + if (PySet_Add(seen, value) == -1) + PyErr_Clear(); + else if (PyExceptionInstance_Check(value)) { + cause = PyException_GetCause(value); + context = PyException_GetContext(value); + if (cause) { + res = PySet_Contains(seen, cause); + if (res == -1) + PyErr_Clear(); + if (res == 0) { + print_exception_recursive( + f, cause, seen); + err |= PyFile_WriteString( + cause_message, f); + } + } + if (context) { + res = PySet_Contains(seen, context); + if (res == -1) + PyErr_Clear(); + if (res == 0) { + print_exception_recursive( + f, context, seen); + err |= PyFile_WriteString( + context_message, f); + } + } + Py_XDECREF(context); + Py_XDECREF(cause); + } + } + print_exception(f, value); + if (err != 0) + PyErr_Clear(); +} + void PyErr_Display(PyObject *exception, PyObject *value, PyObject *tb) { - int err = 0; + PyObject *seen; PyObject *f = PySys_GetObject("stderr"); - Py_INCREF(value); if (f == Py_None) { /* pass */ } @@ -1307,97 +1460,15 @@ PyErr_Display(PyObject *exception, PyObject *value, PyObject *tb) fprintf(stderr, "lost sys.stderr\n"); } else { - fflush(stdout); - if (tb && tb != Py_None) - err = PyTraceBack_Print(tb, f); - if (err == 0 && - PyObject_HasAttrString(value, "print_file_and_line")) - { - PyObject *message; - const char *filename, *text; - int lineno, offset; - if (!parse_syntax_error(value, &message, &filename, - &lineno, &offset, &text)) - PyErr_Clear(); - else { - char buf[10]; - PyFile_WriteString(" File \"", f); - if (filename == NULL) - PyFile_WriteString("", f); - else - PyFile_WriteString(filename, f); - PyFile_WriteString("\", line ", f); - PyOS_snprintf(buf, sizeof(buf), "%d", lineno); - PyFile_WriteString(buf, f); - PyFile_WriteString("\n", f); - if (text != NULL) - print_error_text(f, offset, text); - Py_DECREF(value); - value = message; - /* Can't be bothered to check all those - PyFile_WriteString() calls */ - if (PyErr_Occurred()) - err = -1; - } - } - if (err) { - /* Don't do anything else */ - } - else if (PyExceptionClass_Check(exception)) { - PyObject* moduleName; - char* className = PyExceptionClass_Name(exception); - if (className != NULL) { - char *dot = strrchr(className, '.'); - if (dot != NULL) - className = dot+1; - } - - moduleName = PyObject_GetAttrString(exception, "__module__"); - if (moduleName == NULL || !PyUnicode_Check(moduleName)) - { - Py_DECREF(moduleName); - err = PyFile_WriteString("", f); - } - else { - char* modstr = PyUnicode_AsString(moduleName); - if (modstr && strcmp(modstr, "builtins")) - { - err = PyFile_WriteString(modstr, f); - err += PyFile_WriteString(".", f); - } - Py_DECREF(moduleName); - } - if (err == 0) { - if (className == NULL) - err = PyFile_WriteString("", f); - else - err = PyFile_WriteString(className, f); - } - } - else - err = PyFile_WriteObject(exception, f, Py_PRINT_RAW); - if (err == 0 && (value != Py_None)) { - PyObject *s = PyObject_Str(value); - /* only print colon if the str() of the - object is not the empty string - */ - if (s == NULL) - err = -1; - else if (!PyUnicode_Check(s) || - PyUnicode_GetSize(s) != 0) - err = PyFile_WriteString(": ", f); - if (err == 0) - err = PyFile_WriteObject(s, f, Py_PRINT_RAW); - Py_XDECREF(s); - } - /* try to write a newline in any case */ - err += PyFile_WriteString("\n", f); + /* We choose to ignore seen being possibly NULL, and report + at least the main exception (it could be a MemoryError). + */ + seen = PySet_New(NULL); + if (seen == NULL) + PyErr_Clear(); + print_exception_recursive(f, value, seen); + Py_XDECREF(seen); } - Py_DECREF(value); - /* If an error happened here, don't show it. - XXX This is wrong, but too many callers rely on this behavior. */ - if (err != 0) - PyErr_Clear(); } PyObject * diff --git a/Python/traceback.c b/Python/traceback.c index 76e22a188da..55300fc505e 100644 --- a/Python/traceback.c +++ b/Python/traceback.c @@ -129,7 +129,7 @@ PyTraceBack_Here(PyFrameObject *frame) } int -Py_DisplaySourceLine(PyObject *f, const char *filename, int lineno) +Py_DisplaySourceLine(PyObject *f, const char *filename, int lineno, int indent) { int err = 0; FILE *xfp = NULL; @@ -139,8 +139,6 @@ Py_DisplaySourceLine(PyObject *f, const char *filename, int lineno) if (filename == NULL) return -1; - /* This is needed by Emacs' compile command */ -#define FMT " File \"%.500s\", line %d, in %.500s\n" xfp = fopen(filename, "r" PY_STDIOTEXTMODE); if (xfp == NULL) { /* Search tail of filename in sys.path before giving up */ @@ -203,12 +201,27 @@ Py_DisplaySourceLine(PyObject *f, const char *filename, int lineno) } while (*pLastChar != '\0' && *pLastChar != '\n'); } if (i == lineno) { + char buf[11]; char *p = linebuf; while (*p == ' ' || *p == '\t' || *p == '\014') p++; - err = PyFile_WriteString(p, f); - if (err == 0 && strchr(p, '\n') == NULL) - err = PyFile_WriteString("\n", f); + + /* Write some spaces before the line */ + strcpy(buf, " "); + assert (strlen(buf) == 10); + while (indent > 0) { + if(indent < 10) + buf[indent] = '\0'; + err = PyFile_WriteString(buf, f); + if (err != 0) + break; + indent -= 10; + } + + if (err == 0) + err = PyFile_WriteString(p, f); + if (err == 0 && strchr(p, '\n') == NULL) + err = PyFile_WriteString("\n", f); } fclose(xfp); return err; @@ -228,7 +241,7 @@ tb_displayline(PyObject *f, const char *filename, int lineno, const char *name) err = PyFile_WriteString(linebuf, f); if (err != 0) return err; - return Py_DisplaySourceLine(f, filename, lineno); + return Py_DisplaySourceLine(f, filename, lineno, 4); } static int