From 488ca62c249bffd247701d99c62bfb2949f9fea2 Mon Sep 17 00:00:00 2001 From: DongHun Kwak Date: Mon, 10 Apr 2017 16:21:25 +0900 Subject: [PATCH] Imported Upstream version 1.4.0 Change-Id: I1dd9b757190fd564951cfbcfd27b7a2dc78138d7 Signed-off-by: DongHun Kwak --- .testr.conf | 4 + AUTHORS | 1 + ChangeLog | 52 ++ Makefile | 3 + PKG-INFO | 41 ++ README.rst | 17 + requirements.txt | 1 + setup.cfg | 40 + setup.py | 6 + test-requirements.txt | 4 + traceback2.egg-info/PKG-INFO | 41 ++ traceback2.egg-info/SOURCES.txt | 19 + traceback2.egg-info/dependency_links.txt | 1 + traceback2.egg-info/not-zip-safe | 1 + traceback2.egg-info/pbr.json | 1 + traceback2.egg-info/requires.txt | 1 + traceback2.egg-info/top_level.txt | 1 + traceback2/__init__.py | 597 +++++++++++++++ traceback2/tests/__init__.py | 0 traceback2/tests/test_traceback.py | 899 +++++++++++++++++++++++ 20 files changed, 1730 insertions(+) create mode 100644 .testr.conf create mode 100644 AUTHORS create mode 100644 ChangeLog create mode 100644 Makefile create mode 100644 PKG-INFO create mode 100644 README.rst create mode 100644 requirements.txt create mode 100644 setup.cfg create mode 100644 setup.py create mode 100644 test-requirements.txt create mode 100644 traceback2.egg-info/PKG-INFO create mode 100644 traceback2.egg-info/SOURCES.txt create mode 100644 traceback2.egg-info/dependency_links.txt create mode 100644 traceback2.egg-info/not-zip-safe create mode 100644 traceback2.egg-info/pbr.json create mode 100644 traceback2.egg-info/requires.txt create mode 100644 traceback2.egg-info/top_level.txt create mode 100644 traceback2/__init__.py create mode 100644 traceback2/tests/__init__.py create mode 100644 traceback2/tests/test_traceback.py diff --git a/.testr.conf b/.testr.conf new file mode 100644 index 0000000..a7167a7 --- /dev/null +++ b/.testr.conf @@ -0,0 +1,4 @@ +[DEFAULT] +test_command=${PYTHON:-python} -m subunit.run discover $LISTOPT $IDOPTION +test_id_option=--load-list $IDFILE +test_list_option=--list diff --git a/AUTHORS b/AUTHORS new file mode 100644 index 0000000..ad86f89 --- /dev/null +++ b/AUTHORS @@ -0,0 +1 @@ +Robert Collins diff --git a/ChangeLog b/ChangeLog new file mode 100644 index 0000000..2a3ef46 --- /dev/null +++ b/ChangeLog @@ -0,0 +1,52 @@ +CHANGES +======= + +1.4.0 +----- + +* Handle unicode paths more thoroughly + +1.3.0 +----- + +* Try harder to show unicode lines in SyntaxErrors + +1.2.1 +----- + +* Unbreak handling of syntax errors with no line + +1.2.0 +----- + +* Handle filenames that can't be decoded to unicode + +1.1.0 +----- + +* Handle objects with broken __unicode__ + +1.0.0 +----- + +* Time for 1.0.0 +* Fixe test when running with .pyc files +* Issue #22936: Make it possible to show local variables in tracebacks +* Remaining fallout from 17911 +* Fix brownbag in issue 17911 commit +* Issue #17911: traceback module overhaul +* Supporting boilerplate + +0.0.1 +----- + +* Fixes for the test suite on PyPy +* Python2.xify the code base +* Handle missing __traceback__ in tests for < 3.x +* Handle absence of __context__ and __cause__ on < 3.x +* Handle different type repr in < 3.2 +* Fixes for 3.2 - qualname and exception suppressing +* Fixes for 3.3 +* Port trunk traceback module to standalone w/3.5 +* Ignore .eggs as well +* Basic boilerplate diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..eb41afa --- /dev/null +++ b/Makefile @@ -0,0 +1,3 @@ +# tag it first. +release: + python setup.py sdist bdist_wheel upload -s diff --git a/PKG-INFO b/PKG-INFO new file mode 100644 index 0000000..4815752 --- /dev/null +++ b/PKG-INFO @@ -0,0 +1,41 @@ +Metadata-Version: 1.1 +Name: traceback2 +Version: 1.4.0 +Summary: Backports of the traceback module +Home-page: https://github.com/testing-cabal/traceback2 +Author: Testing-cabal +Author-email: testing-cabal@lists.launchpad.net +License: UNKNOWN +Description: A backport of traceback to older supported Pythons. + + >>> import traceback2 as traceback + + Profit. + + Things to be aware of! + + In Python 2.x, unlike traceback, traceback2 creates unicode output (because it + depends on the linecache2 module). + + Exception frame clearing silently does nothing if the interpreter in use does + not support it. + + traceback2._some_str, which while not an official API is so old its likely in + use behaves similarly to the Python3 version - objects where unicode(obj) fails + but str(object) works will be shown as b'thestrvaluerepr'. + + +Platform: UNKNOWN +Classifier: Development Status :: 6 - Mature +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: Python Software Foundation License +Classifier: Operating System :: OS Independent +Classifier: Programming Language :: Python +Classifier: Programming Language :: Python :: 2 +Classifier: Programming Language :: Python :: 2.6 +Classifier: Programming Language :: Python :: 2.7 +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3.2 +Classifier: Programming Language :: Python :: 3.3 +Classifier: Programming Language :: Python :: 3.4 +Classifier: Topic :: Software Development diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..3d279dd --- /dev/null +++ b/README.rst @@ -0,0 +1,17 @@ +A backport of traceback to older supported Pythons. + + >>> import traceback2 as traceback + +Profit. + +Things to be aware of! + +In Python 2.x, unlike traceback, traceback2 creates unicode output (because it +depends on the linecache2 module). + +Exception frame clearing silently does nothing if the interpreter in use does +not support it. + +traceback2._some_str, which while not an official API is so old its likely in +use behaves similarly to the Python3 version - objects where unicode(obj) fails +but str(object) works will be shown as b'thestrvaluerepr'. diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..2861de5 --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +linecache2 diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..9246706 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,40 @@ +[metadata] +name = traceback2 +version = 1.0.0 +summary = Backports of the traceback module +description-file = + README.rst +author = Testing-cabal +author-email = testing-cabal@lists.launchpad.net +home-page = https://github.com/testing-cabal/traceback2 +classifier = + Development Status :: 6 - Mature + Intended Audience :: Developers + License :: OSI Approved :: Python Software Foundation License + Operating System :: OS Independent + Programming Language :: Python + Programming Language :: Python :: 2 + Programming Language :: Python :: 2.6 + Programming Language :: Python :: 2.7 + Programming Language :: Python :: 3 + Programming Language :: Python :: 3.2 + Programming Language :: Python :: 3.3 + Programming Language :: Python :: 3.4 + Topic :: Software Development + +[files] +packages = + traceback2 + +[wheel] +universal = 1 + +[pbr] +autodoc_index_modules = 0 +warnerrors = true + +[egg_info] +tag_date = 0 +tag_svn_revision = 0 +tag_build = + diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..31b64ee --- /dev/null +++ b/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['pbr'], + pbr=True) + diff --git a/test-requirements.txt b/test-requirements.txt new file mode 100644 index 0000000..14e4a45 --- /dev/null +++ b/test-requirements.txt @@ -0,0 +1,4 @@ +contextlib2 +fixtures +testtools +unittest2 diff --git a/traceback2.egg-info/PKG-INFO b/traceback2.egg-info/PKG-INFO new file mode 100644 index 0000000..4815752 --- /dev/null +++ b/traceback2.egg-info/PKG-INFO @@ -0,0 +1,41 @@ +Metadata-Version: 1.1 +Name: traceback2 +Version: 1.4.0 +Summary: Backports of the traceback module +Home-page: https://github.com/testing-cabal/traceback2 +Author: Testing-cabal +Author-email: testing-cabal@lists.launchpad.net +License: UNKNOWN +Description: A backport of traceback to older supported Pythons. + + >>> import traceback2 as traceback + + Profit. + + Things to be aware of! + + In Python 2.x, unlike traceback, traceback2 creates unicode output (because it + depends on the linecache2 module). + + Exception frame clearing silently does nothing if the interpreter in use does + not support it. + + traceback2._some_str, which while not an official API is so old its likely in + use behaves similarly to the Python3 version - objects where unicode(obj) fails + but str(object) works will be shown as b'thestrvaluerepr'. + + +Platform: UNKNOWN +Classifier: Development Status :: 6 - Mature +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: Python Software Foundation License +Classifier: Operating System :: OS Independent +Classifier: Programming Language :: Python +Classifier: Programming Language :: Python :: 2 +Classifier: Programming Language :: Python :: 2.6 +Classifier: Programming Language :: Python :: 2.7 +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3.2 +Classifier: Programming Language :: Python :: 3.3 +Classifier: Programming Language :: Python :: 3.4 +Classifier: Topic :: Software Development diff --git a/traceback2.egg-info/SOURCES.txt b/traceback2.egg-info/SOURCES.txt new file mode 100644 index 0000000..7b1f471 --- /dev/null +++ b/traceback2.egg-info/SOURCES.txt @@ -0,0 +1,19 @@ +.testr.conf +AUTHORS +ChangeLog +Makefile +README.rst +requirements.txt +setup.cfg +setup.py +test-requirements.txt +traceback2/__init__.py +traceback2.egg-info/PKG-INFO +traceback2.egg-info/SOURCES.txt +traceback2.egg-info/dependency_links.txt +traceback2.egg-info/not-zip-safe +traceback2.egg-info/pbr.json +traceback2.egg-info/requires.txt +traceback2.egg-info/top_level.txt +traceback2/tests/__init__.py +traceback2/tests/test_traceback.py \ No newline at end of file diff --git a/traceback2.egg-info/dependency_links.txt b/traceback2.egg-info/dependency_links.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/traceback2.egg-info/dependency_links.txt @@ -0,0 +1 @@ + diff --git a/traceback2.egg-info/not-zip-safe b/traceback2.egg-info/not-zip-safe new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/traceback2.egg-info/not-zip-safe @@ -0,0 +1 @@ + diff --git a/traceback2.egg-info/pbr.json b/traceback2.egg-info/pbr.json new file mode 100644 index 0000000..e71fd07 --- /dev/null +++ b/traceback2.egg-info/pbr.json @@ -0,0 +1 @@ +{"git_version": "37c5f15", "is_release": true} \ No newline at end of file diff --git a/traceback2.egg-info/requires.txt b/traceback2.egg-info/requires.txt new file mode 100644 index 0000000..816ba28 --- /dev/null +++ b/traceback2.egg-info/requires.txt @@ -0,0 +1 @@ +linecache2 \ No newline at end of file diff --git a/traceback2.egg-info/top_level.txt b/traceback2.egg-info/top_level.txt new file mode 100644 index 0000000..ac5f6de --- /dev/null +++ b/traceback2.egg-info/top_level.txt @@ -0,0 +1 @@ +traceback2 diff --git a/traceback2/__init__.py b/traceback2/__init__.py new file mode 100644 index 0000000..88fcaaf --- /dev/null +++ b/traceback2/__init__.py @@ -0,0 +1,597 @@ +"""Extract, format and print information about Python stack traces.""" + +import sys +import operator + +import linecache2 as linecache +from six import u, PY2 + +__all__ = ['extract_stack', 'extract_tb', 'format_exception', + 'format_exception_only', 'format_list', 'format_stack', + 'format_tb', 'print_exc', 'format_exc', 'print_exception', + 'print_last', 'print_stack', 'print_tb', + 'clear_frames'] + +# +# Formatting and printing lists of traceback lines. +# + +def print_list(extracted_list, file=None): + """Print the list of tuples as returned by extract_tb() or + extract_stack() as a formatted stack trace to the given file.""" + if file is None: + file = sys.stderr + for item in StackSummary.from_list(extracted_list).format(): + file.write(item) + +def format_list(extracted_list): + """Format a list of traceback entry tuples for printing. + + Given a list of tuples as returned by extract_tb() or + extract_stack(), return a list of strings ready for printing. + Each string in the resulting list corresponds to the item with the + same index in the argument list. Each string ends in a newline; + the strings may contain internal newlines as well, for those items + whose source text line is not None. + """ + return StackSummary.from_list(extracted_list).format() + +# +# Printing and Extracting Tracebacks. +# + +def print_tb(tb, limit=None, file=None): + """Print up to 'limit' stack trace entries from the traceback 'tb'. + + If 'limit' is omitted or None, all entries are printed. If 'file' + is omitted or None, the output goes to sys.stderr; otherwise + 'file' should be an open file or file-like object with a write() + method. + """ + print_list(extract_tb(tb, limit=limit), file=file) + +def format_tb(tb, limit=None): + """A shorthand for 'format_list(extract_tb(tb, limit))'.""" + return extract_tb(tb, limit=limit).format() + +def extract_tb(tb, limit=None): + """Return list of up to limit pre-processed entries from traceback. + + This is useful for alternate formatting of stack traces. If + 'limit' is omitted or None, all entries are extracted. A + pre-processed stack trace entry is a quadruple (filename, line + number, function name, text) representing the information that is + usually printed for a stack trace. The text is a string with + leading and trailing whitespace stripped; if the source is not + available it is None. + """ + return StackSummary.extract(walk_tb(tb), limit=limit) + +# +# Exception formatting and output. +# + +_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") + + +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 + traceback is not None, it prints a header "Traceback (most recent + call last):"; (2) it prints the exception type and value after the + stack trace; (3) if type is SyntaxError and value has the + appropriate format, it prints the line where the syntax error + occurred with a caret on the next line indicating the approximate + position of the error. + """ + # format_exception has ignored etype for some time, and code such as cgitb + # passes in bogus values as a result. For compatibility with such code we + # ignore it here (rather than in the new TracebackException API). + if file is None: + file = sys.stderr + for line in TracebackException( + type(value), value, tb, limit=limit).format(chain=chain): + file.write(line) + + +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 + to print_exception(). The return value is a list of strings, each + ending in a newline and some containing internal newlines. When + these lines are concatenated and printed, exactly the same text is + printed as does print_exception(). + """ + # format_exception has ignored etype for some time, and code such as cgitb + # passes in bogus values as a result. For compatibility with such code we + # ignore it here (rather than in the new TracebackException API). + return list(TracebackException( + type(value), value, tb, limit=limit).format(chain=chain)) + + +def format_exception_only(etype, value): + """Format the exception part of a traceback. + + The arguments are the exception type and value such as given by + sys.last_type and sys.last_value. The return value is a list of + strings, each ending in a newline. + + Normally, the list contains a single string; however, for + SyntaxError exceptions, it contains several lines that (when + printed) display detailed information about where the syntax + error occurred. + + The message indicating which exception occurred is always the last + string in the list. + + """ + return list(TracebackException(etype, value, None).format_exception_only()) + + +# -- not offical API but folk probably use these two functions. + +def _format_final_exc_line(etype, value): + valuestr = _some_str(value) + if value == 'None' or value is None or not valuestr: + line = u("%s\n") % etype + else: + line = u("%s: %s\n") % (etype, valuestr) + return line + +def _some_str(value): + try: + if PY2: + # If there is a working __unicode__, great. + # Otherwise see if we can get a bytestring... + # Otherwise we fallback to unprintable. + try: + return unicode(value) + except: + return "b%s" % repr(str(value)) + else: + # For Python3, bytestrings don't implicit decode, so its trivial. + return str(value) + except: + return '' % type(value).__name__ + +# -- + +def _some_fs_str(value): + """_some_str, but for filesystem paths.""" + if value is None: + return None + try: + if type(value) is bytes: + return value.decode(sys.getfilesystemencoding()) + except: + pass + return _some_str(value) + + +def print_exc(limit=None, file=None, chain=True): + """Shorthand for 'print_exception(*sys.exc_info(), limit, file)'.""" + print_exception(*sys.exc_info(), limit=limit, file=file, chain=chain) + +def format_exc(limit=None, chain=True): + """Like print_exc() but return a string.""" + return "".join(format_exception(*sys.exc_info(), limit=limit, chain=chain)) + +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 not hasattr(sys, "last_type"): + raise ValueError("no last exception") + print_exception(sys.last_type, sys.last_value, sys.last_traceback, + limit, file, chain) + +# +# Printing and Extracting Stacks. +# + +def print_stack(f=None, limit=None, file=None): + """Print a stack trace from its invocation point. + + The optional 'f' argument can be used to specify an alternate + stack frame at which to start. The optional 'limit' and 'file' + arguments have the same meaning as for print_exception(). + """ + print_list(extract_stack(f, limit=limit), file=file) + + +def format_stack(f=None, limit=None): + """Shorthand for 'format_list(extract_stack(f, limit))'.""" + return format_list(extract_stack(f, limit=limit)) + + +def extract_stack(f=None, limit=None): + """Extract the raw traceback from the current stack frame. + + The return value has the same format as for extract_tb(). The + optional 'f' and 'limit' arguments have the same meaning as for + print_stack(). Each item in the list is a quadruple (filename, + line number, function name, text), and the entries are in order + from oldest to newest stack frame. + """ + stack = StackSummary.extract(walk_stack(f), limit=limit) + stack.reverse() + return stack + + +_identity = lambda:None +def clear_frames(tb): + "Clear all references to local variables in the frames of a traceback." + while tb is not None: + try: + getattr(tb.tb_frame, 'clear', _identity)() + except RuntimeError: + # Ignore the exception raised if the frame is still executing. + pass + tb = tb.tb_next + + +class FrameSummary: + """A single frame from a traceback. + + - :attr:`filename` The filename for the frame. + - :attr:`lineno` The line within filename for the frame that was + active when the frame was captured. + - :attr:`name` The name of the function or method that was executing + when the frame was captured. + - :attr:`line` The text from the linecache module for the + of code that was running when the frame was captured. + - :attr:`locals` Either None if locals were not supplied, or a dict + mapping the name to the repr() of the variable. + """ + + __slots__ = ('filename', 'lineno', 'name', '_line', 'locals') + + def __init__(self, filename, lineno, name, lookup_line=True, + locals=None, line=None): + """Construct a FrameSummary. + + :param lookup_line: If True, `linecache` is consulted for the source + code line. Otherwise, the line will be looked up when first needed. + :param locals: If supplied the frame locals, which will be captured as + object representations. + :param line: If provided, use this instead of looking up the line in + the linecache. + """ + self.filename = filename + self.lineno = lineno + self.name = name + self._line = line + if lookup_line: + self.line + self.locals = \ + dict((k, repr(v)) for k, v in locals.items()) if locals else None + + def __eq__(self, other): + return (self.filename == other.filename and + self.lineno == other.lineno and + self.name == other.name and + self.locals == other.locals) + + def __getitem__(self, pos): + return (self.filename, self.lineno, self.name, self.line)[pos] + + def __iter__(self): + return iter([self.filename, self.lineno, self.name, self.line]) + + def __repr__(self): + return "".format( + filename=self.filename, lineno=self.lineno, name=self.name) + + @property + def line(self): + if self._line is None: + self._line = linecache.getline(self.filename, self.lineno).strip() + return self._line + + +def walk_stack(f): + """Walk a stack yielding the frame and line number for each frame. + + This will follow f.f_back from the given frame. If no frame is given, the + current stack is used. Usually used with StackSummary.extract. + """ + if f is None: + f = sys._getframe().f_back.f_back + while f is not None: + yield f, f.f_lineno + f = f.f_back + + +def walk_tb(tb): + """Walk a traceback yielding the frame and line number for each frame. + + This will follow tb.tb_next (and thus is in the opposite order to + walk_stack). Usually used with StackSummary.extract. + """ + while tb is not None: + yield tb.tb_frame, tb.tb_lineno + tb = tb.tb_next + + +class StackSummary(list): + """A stack of frames.""" + + @classmethod + def extract(klass, frame_gen, limit=None, lookup_lines=True, + capture_locals=False): + """Create a StackSummary from a traceback or stack object. + + :param frame_gen: A generator that yields (frame, lineno) tuples to + include in the stack. + :param limit: None to include all frames or the number of frames to + include. + :param lookup_lines: If True, lookup lines for each frame immediately, + otherwise lookup is deferred until the frame is rendered. + :param capture_locals: If True, the local variables from each frame will + be captured as object representations into the FrameSummary. + """ + if limit is None: + limit = getattr(sys, 'tracebacklimit', None) + + result = klass() + fnames = set() + for pos, (f, lineno) in enumerate(frame_gen): + if limit is not None and pos >= limit: + break + co = f.f_code + filename = co.co_filename + name = co.co_name + + fnames.add(filename) + linecache.lazycache(filename, f.f_globals) + # Must defer line lookups until we have called checkcache. + if capture_locals: + f_locals = f.f_locals + else: + f_locals = None + result.append(FrameSummary( + filename, lineno, name, lookup_line=False, locals=f_locals)) + for filename in fnames: + linecache.checkcache(filename) + # If immediate lookup was desired, trigger lookups now. + if lookup_lines: + for f in result: + f.line + return result + + @classmethod + def from_list(klass, a_list): + """Create a StackSummary from a simple list of tuples. + + This method supports the older Python API. Each tuple should be a + 4-tuple with (filename, lineno, name, line) elements. + """ + if isinstance(a_list, StackSummary): + return StackSummary(a_list) + result = StackSummary() + for filename, lineno, name, line in a_list: + result.append(FrameSummary(filename, lineno, name, line=line)) + return result + + def format(self): + """Format the stack ready for printing. + + Returns a list of strings ready for printing. Each string in the + resulting list corresponds to a single frame from the stack. + Each string ends in a newline; the strings may contain internal + newlines as well, for those items with source text lines. + """ + result = [] + for frame in self: + row = [] + row.append(u(' File "{0}", line {1}, in {2}\n').format( + _some_fs_str(frame.filename), frame.lineno, frame.name)) + if frame.line: + row.append(u(' {0}\n').format(frame.line.strip())) + if frame.locals: + for name, value in sorted(frame.locals.items()): + row.append(u(' {name} = {value}\n').format(name=name, value=value)) + result.append(u('').join(row)) + return result + + +class TracebackException: + """An exception ready for rendering. + + The traceback module captures enough attributes from the original exception + to this intermediary form to ensure that no references are held, while + still being able to fully print or format it. + + Use `from_exception` to create TracebackException instances from exception + objects, or the constructor to create TracebackException instances from + individual components. + + - :attr:`__cause__` A TracebackException of the original *__cause__*. + - :attr:`__context__` A TracebackException of the original *__context__*. + - :attr:`__suppress_context__` The *__suppress_context__* value from the + original exception. + - :attr:`stack` A `StackSummary` representing the traceback. + - :attr:`exc_type` The class of the original traceback. + - :attr:`filename` For syntax errors - the filename where the error + occured. + - :attr:`lineno` For syntax errors - the linenumber where the error + occured. + - :attr:`text` For syntax errors - the text where the error + occured. + - :attr:`offset` For syntax errors - the offset into the text where the + error occured. + - :attr:`msg` For syntax errors - the compiler error message. + """ + + def __init__(self, exc_type, exc_value, exc_traceback, limit=None, + lookup_lines=True, capture_locals=False, _seen=None): + # NB: we need to accept exc_traceback, exc_value, exc_traceback to + # permit backwards compat with the existing API, otherwise we + # need stub thunk objects just to glue it together. + # Handle loops in __cause__ or __context__. + if _seen is None: + _seen = set() + _seen.add(exc_value) + # Gracefully handle (the way Python 2.4 and earlier did) the case of + # being called with no type or value (None, None, None). + if (exc_value and getattr(exc_value, '__cause__', None) is not None + and exc_value.__cause__ not in _seen): + cause = TracebackException( + type(exc_value.__cause__), + exc_value.__cause__, + exc_value.__cause__.__traceback__, + limit=limit, + lookup_lines=False, + capture_locals=capture_locals, + _seen=_seen) + else: + cause = None + if (exc_value and getattr(exc_value, '__context__', None) is not None + and exc_value.__context__ not in _seen): + context = TracebackException( + type(exc_value.__context__), + exc_value.__context__, + exc_value.__context__.__traceback__, + limit=limit, + lookup_lines=False, + capture_locals=capture_locals, + _seen=_seen) + else: + context = None + self.__cause__ = cause + self.__context__ = context + self.__suppress_context__ = \ + getattr(exc_value, '__suppress_context__', False) if exc_value else False + # TODO: locals. + self.stack = StackSummary.extract( + walk_tb(exc_traceback), limit=limit, lookup_lines=lookup_lines, + capture_locals=capture_locals) + self.exc_type = exc_type + # Capture now to permit freeing resources: only complication is in the + # unofficial API _format_final_exc_line + self._str = _some_str(exc_value) + if exc_type and issubclass(exc_type, SyntaxError): + # Handle SyntaxError's specially + self.filename = exc_value.filename + self.lineno = str(exc_value.lineno) + self.text = exc_value.text + self.offset = exc_value.offset + self.msg = exc_value.msg + if lookup_lines: + self._load_lines() + + @classmethod + def from_exception(self, exc, *args, **kwargs): + """Create a TracebackException from an exception. + + Only useful in Python 3 specific code. + """ + return TracebackException( + type(exc), exc, exc.__traceback__, *args, **kwargs) + + def _load_lines(self): + """Private API. force all lines in the stack to be loaded.""" + for frame in self.stack: + frame.line + if self.__context__: + self.__context__._load_lines() + if self.__cause__: + self.__cause__._load_lines() + + def __eq__(self, other): + return self.__dict__ == other.__dict__ + + def __str__(self): + return self._str + + def format_exception_only(self): + """Format the exception part of the traceback. + + The return value is a generator of strings, each ending in a newline. + + Normally, the generator emits a single string; however, for + SyntaxError exceptions, it emites several lines that (when + printed) display detailed information about where the syntax + error occurred. + + The message indicating which exception occurred is always the last + string in the output. + """ + if self.exc_type is None: + yield _format_final_exc_line(None, self._str) + return + + stype = getattr(self.exc_type, '__qualname__', self.exc_type.__name__) + smod = u(self.exc_type.__module__) + if smod not in ("__main__", "builtins", "exceptions"): + stype = smod + u('.') + stype + + if not issubclass(self.exc_type, SyntaxError): + yield _format_final_exc_line(stype, self._str) + return + + # It was a syntax error; show exactly where the problem was found. + filename = _some_fs_str(self.filename) or u("") + lineno = str(self.lineno) or u('?') + yield u(' File "{0}", line {1}\n').format(filename, lineno) + + badline = None + if self.text is not None: + if type(self.text) is bytes: + # Not decoded - get the line via linecache which will decode + # for us. + if self.lineno: + badline = linecache.getline(filename, int(lineno)) + if not badline: + # But we can't for some reason, so fallback to attempting a + # u cast. + badline = u(self.text) + else: + badline = self.text + offset = self.offset + if badline is not None: + yield u(' {0}\n').format(badline.strip()) + if offset is not None: + caretspace = badline.rstrip('\n') + offset = min(len(caretspace), offset) - 1 + caretspace = caretspace[:offset].lstrip() + # non-space whitespace (likes tabs) must be kept for alignment + caretspace = ((c.isspace() and c or ' ') for c in caretspace) + yield u(' {0}^\n').format(''.join(caretspace)) + msg = self.msg or u("") + yield u("{0}: {1}\n").format(stype, msg) + + def format(self, chain=True): + """Format the exception. + + If chain is not *True*, *__cause__* and *__context__* will not be formatted. + + The return value is a generator of strings, each ending in a newline and + some containing internal newlines. `print_exception` is a wrapper around + this method which just prints the lines to a file. + + The message indicating which exception occurred is always the last + string in the output. + """ + if chain: + if self.__cause__ is not None: + for line in self.__cause__.format(chain=chain): + yield line + yield _cause_message + elif (self.__context__ is not None and + not self.__suppress_context__): + for line in self.__context__.format(chain=chain): + yield line + yield _context_message + yield u('Traceback (most recent call last):\n') + for line in self.stack.format(): + yield line + for line in self.format_exception_only(): + yield line diff --git a/traceback2/tests/__init__.py b/traceback2/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/traceback2/tests/test_traceback.py b/traceback2/tests/test_traceback.py new file mode 100644 index 0000000..2a933b5 --- /dev/null +++ b/traceback2/tests/test_traceback.py @@ -0,0 +1,899 @@ +"""Test cases for traceback module""" + +from collections import namedtuple +import doctest +import io +from io import StringIO +import platform +import sys +import re + +import contextlib2 as contextlib +import fixtures +import linecache2 as linecache +import six +from six import b, text_type, u +try: + from six import raise_from +except ImportError: +# support raise_from on 3.x: +# submitted to six: https://bitbucket.org/gutworth/six/issue/102/raise-foo-from-bar-is-a-syntax-error-on-27 + if sys.version_info[:2] > (3, 1): + six.exec_("""def raise_from(value, from_value): + raise value from from_value + """) + else: + def raise_from(value, from_value): + raise value +import unittest2 as unittest +import testtools +from testtools.matchers import DocTestMatches, Equals, MatchesAny + +import traceback2 as traceback + + +@contextlib.contextmanager +def captured_output(streamname): + stream = StringIO() + patch = fixtures.MonkeyPatch('sys.%s' % streamname, stream) + with patch: + yield stream + + +FNAME = __file__ +if FNAME.endswith('.pyc'): + FNAME = FNAME[:-1] +class FakeLoader: + def __init__(self, lines): + self._lines = lines + def get_source(self, name): + return self._lines +fake_module = dict( + __name__='fred', + __loader__=FakeLoader(''.join(linecache.getlines(FNAME))) + ) + + +test_code = namedtuple('code', ['co_filename', 'co_name']) +test_frame = namedtuple('frame', ['f_code', 'f_globals', 'f_locals']) +test_tb = namedtuple('tb', ['tb_frame', 'tb_lineno', 'tb_next']) + + +class SyntaxTracebackCases(testtools.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. + + def get_exception_format(self, func, exc): + try: + func() + except exc as value: + return traceback.format_exception_only(exc, value) + else: + raise ValueError("call did not raise exception") + + def syntax_error_with_caret(self): + compile("def fact(x):\n\treturn x!\n", "?", "exec") + + def syntax_error_with_caret_2(self): + compile("1 +\n", "?", "exec") + + def syntax_error_bad_indentation(self): + compile("def spam():\n print(1)\n print(2)", "?", "exec") + + def syntax_error_with_caret_non_ascii(self): + compile('Python = "\u1e54\xfd\u0163\u0125\xf2\xf1" +', "?", "exec") + + def syntax_error_bad_indentation2(self): + compile(" print(2)", "?", "exec") + + def test_caret(self): + err = self.get_exception_format(self.syntax_error_with_caret, + SyntaxError) + self.assertEqual(len(err), 4) + self.assertTrue(err[1].strip() == "return x!") + self.assertIn("^", err[2]) # third line has caret + self.assertEqual(err[1].find("!"), err[2].find("^")) # in the right place + + err = self.get_exception_format(self.syntax_error_with_caret_2, + SyntaxError) + self.assertIn("^", err[2]) # third line has caret + self.assertEqual(err[2].count('\n'), 1) # and no additional newline + self.assertEqual(err[1].find("+"), err[2].find("^")) # in the right place + + err = self.get_exception_format(self.syntax_error_with_caret_non_ascii, + SyntaxError) + self.assertIn("^", err[2]) # third line has caret + self.assertEqual(err[2].count('\n'), 1) # and no additional newline + self.assertEqual(err[1].find("+"), err[2].find("^")) # in the right place + + def test_nocaret(self): + exc = SyntaxError("error", ("x.py", 23, None, "bad syntax")) + err = traceback.format_exception_only(SyntaxError, exc) + self.assertEqual(len(err), 3) + self.assertEqual(err[1].strip(), "bad syntax") + + def test_bad_indentation(self): + err = self.get_exception_format(self.syntax_error_bad_indentation, + IndentationError) + self.assertEqual(len(err), 4) + self.assertEqual(err[1].strip(), "print(2)") + self.assertIn("^", err[2]) + self.assertEqual(err[1].find(")"), err[2].find("^")) + + err = self.get_exception_format(self.syntax_error_bad_indentation2, + IndentationError) + self.assertEqual(len(err), 4) + self.assertEqual(err[1].strip(), "print(2)") + self.assertIn("^", err[2]) + # pypy has a different offset for its errors. + pos_cpython = err[1].find("p") + pos_pypy = err[1].find(")") + self.assertThat( + err[2].find("^"), + MatchesAny(Equals(pos_cpython), Equals(pos_pypy))) + + def test_base_exception(self): + # Test that exceptions derived from BaseException are formatted right + e = KeyboardInterrupt() + lst = traceback.format_exception_only(e.__class__, e) + self.assertThat(lst, + MatchesAny(Equals(['KeyboardInterrupt\n']), + Equals(['exceptions.KeyboardInterrupt\n']))) + + def test_format_exception_only_bad__str__(self): + def qualname(X): + return getattr(X, '__qualname__', X.__name__) + class X(Exception): + def __str__(self): + 1/0 + err = traceback.format_exception_only(X, X()) + self.assertEqual(len(err), 1) + str_value = '' % X.__name__ + if X.__module__ in ('__main__', 'builtins'): + str_name = qualname(X) + else: + str_name = '.'.join([X.__module__, qualname(X)]) + self.assertEqual(err[0], "%s: %s\n" % (str_name, str_value)) + + def test_format_exception_only_undecodable__str__(self): + # This won't decode via the ascii codec. + X = Exception(u('\u5341').encode('shift-jis')) + err = traceback.format_exception_only(type(X), X) + self.assertEqual(len(err), 1) + str_value = "b'\\x8f\\\\'" + self.assertEqual(err[0], "Exception: %s\n" % str_value) + + def test_without_exception(self): + err = traceback.format_exception_only(None, None) + self.assertEqual(err, ['None\n']) + + def test_encoded_file(self): + # Test that tracebacks are correctly printed for encoded source files: + # - correct line number (Issue2384) + # - respect file encoding (Issue3975) + import tempfile, sys, subprocess, os + + # The spawned subprocess has its stdout redirected to a PIPE, and its + # encoding may be different from the current interpreter, on Windows + # at least. + process = subprocess.Popen([sys.executable, "-c", + "import sys; print(sys.stdout.encoding)"], + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT) + stdout, stderr = process.communicate() + output_encoding = text_type(stdout, 'ascii').splitlines()[0] + + def do_test(firstlines, message, charset, lineno, output_encoding): + # Raise the message in a subprocess, and catch the output + with fixtures.TempDir() as d: + TESTFN = d.path + '/fname' + output = io.open(TESTFN, "w", encoding=charset) + output.write(u("""{0}if 1: + import traceback; + raise RuntimeError('{1}') + """).format(firstlines, message)) + output.close() + process = subprocess.Popen([sys.executable, TESTFN], + stdout=subprocess.PIPE, stderr=subprocess.STDOUT) + stdout, stderr = process.communicate() + if output_encoding == 'None': + output_encoding = charset + stdout = stdout.decode(output_encoding).splitlines() + + # The source lines are encoded with the 'backslashreplace' handler + encoded_message = message.encode(output_encoding, + 'backslashreplace') + # and we just decoded them with the output_encoding. + message_ascii = encoded_message.decode(output_encoding) + + err_line = u("raise RuntimeError('{0}')").format(message_ascii) + err_msg = u("RuntimeError: {0}").format(message_ascii) + + if platform.python_implementation() == 'PyPy': + # PyPy includes its own top level app_main.py in the traceback. + del stdout[1] + self.assertIn(("line %s" % lineno), stdout[1], + "Invalid line number: {0!r} instead of {1}".format( + stdout[1], lineno)) + self.assertTrue(stdout[2].endswith(err_line), + "Invalid traceback line: {0!r} instead of {1!r}".format( + stdout[2], err_line)) + self.assertTrue(stdout[3] == err_msg, + "Invalid error message: {0!r} instead of {1!r}".format( + stdout[3], err_msg)) + + do_test("", "foo", "ascii", 3, output_encoding) + for charset in ("ascii", "iso-8859-1", "utf-8", "GBK"): + if charset == "ascii": + text = u("foo") + elif charset == "GBK": + text = u("\u4E02\u5100") + else: + text = u("h\xe9 ho") + do_test("# coding: {0}\n".format(charset), + text, charset, 4, output_encoding) + do_test("#!shebang\n# coding: {0}\n".format(charset), + text, charset, 5, output_encoding) + do_test(" \t\f\n# coding: {0}\n".format(charset), + text, charset, 5, output_encoding) + # Issue #18960: coding spec should has no effect + # (Fixed in 3.4) + if sys.version_info[:2] > (3, 3): + do_test( + "0\n# coding: GBK\n", u("h\xe9 ho"), 'utf-8', 5, + output_encoding) + + +class TracebackFormatTests(unittest.TestCase): + + def some_exception(self): + raise KeyError('blah') + + def check_traceback_format(self, cleanup_func=None): + 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 + except ImportError: + traceback_print = None + try: + self.some_exception() + except KeyError: + type_, value, tb = sys.exc_info() + if cleanup_func is not None: + # Clear the inner frames, not this one + cleanup_func(tb.tb_next) + traceback_fmt = u('Traceback (most recent call last):\n') + \ + u('').join(traceback.format_tb(tb)) + if traceback_print is not None: + file_ = StringIO() + traceback_print(tb, file_) + python_fmt = file_.getvalue() + # Call all _tb and _exc functions + with captured_output("stderr") as tbstderr: + traceback.print_tb(tb) + tbfile = StringIO() + traceback.print_tb(tb, file=tbfile) + with captured_output("stderr") as excstderr: + traceback.print_exc() + excfmt = traceback.format_exc() + excfile = StringIO() + traceback.print_exc(file=excfile) + else: + self.fail("unable to create test traceback string") + + # Make sure that Python and the traceback module format the same thing + if traceback_print is not None: + self.assertEqual(traceback_fmt, python_fmt) + # Now verify the _tb func output + self.assertEqual(tbstderr.getvalue(), tbfile.getvalue()) + # Now verify the _exc func output + self.assertEqual(excstderr.getvalue(), excfile.getvalue()) + self.assertEqual(excfmt, excfile.getvalue()) + + # Make sure that the traceback is properly indented. + tb_lines = traceback_fmt.splitlines() + self.assertEqual(len(tb_lines), 5) + banner = tb_lines[0] + location, source_line = tb_lines[-2:] + self.assertTrue(banner.startswith('Traceback')) + self.assertTrue(location.startswith(' File')) + self.assertTrue(source_line.startswith(' raise')) + + def test_traceback_format(self): + self.check_traceback_format() + + def test_traceback_format_with_cleared_frames(self): + # Check that traceback formatting also works with a clear()ed frame + def cleanup_tb(tb): + if getattr(tb.tb_frame, 'clear_frames', None): + tb.tb_frame.clear() + self.check_traceback_format(cleanup_tb) + + def test_stack_format(self): + # Verify _stack functions. Note we have to use _getframe(1) to + # compare them without this frame appearing in the output + with captured_output("stderr") as ststderr: + traceback.print_stack(sys._getframe(1)) + stfile = StringIO() + traceback.print_stack(sys._getframe(1), file=stfile) + self.assertEqual(ststderr.getvalue(), stfile.getvalue()) + + stfmt = traceback.format_stack(sys._getframe(1)) + + self.assertEqual(ststderr.getvalue(), "".join(stfmt)) + + +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, tb=None): + if isinstance(exception_or_callable, Exception): + return exception_or_callable, tb + try: + exception_or_callable() + except Exception as e: + return e, sys.exc_info()[2] + + def zero_div(self): + 1/0 # In zero_div + + def check_zero_div(self, msg): + lines = msg.splitlines() + self.assertTrue(lines[-3].startswith(' File')) + self.assertIn('1/0 # In zero_div', lines[-2]) + self.assertTrue(lines[-1].startswith('ZeroDivisionError'), lines[-1]) + + def test_simple(self): + try: + 1/0 # Marker + except ZeroDivisionError as _: + e = _ + tb = sys.exc_info()[2] + lines = self.get_report(e, tb=tb).splitlines() + self.assertEqual(len(lines), 4) + self.assertTrue(lines[0].startswith('Traceback')) + self.assertTrue(lines[1].startswith(' File')) + self.assertIn('1/0 # Marker', lines[2]) + # < 3 show as exceptions.ZeroDivisionError. + self.assertIn('ZeroDivisionError', lines[3]) + + @unittest.skipIf(sys.version_info[:2] < (3, 2), "Only applies to 3.2+") + def test_cause(self): + def inner_raise(): + try: + self.zero_div() + except ZeroDivisionError as e: + raise_from(KeyError, e) + def outer_raise(): + inner_raise() # Marker + blocks = boundaries.split(self.get_report(outer_raise)) + self.assertEqual(len(blocks), 3) + self.assertEqual(blocks[1], cause_message) + self.check_zero_div(blocks[0]) + 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 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.assertEqual(len(blocks), 3) + self.assertEqual(blocks[1], context_message) + self.check_zero_div(blocks[0]) + self.assertIn('inner_raise() # Marker', blocks[2]) + + @unittest.skipIf(sys.version_info[:2] < (3, 3), "Only applies to 3.3+") + def test_context_suppression(self): + try: + try: + raise Exception + except: + raise_from(ZeroDivisionError, None) + except ZeroDivisionError as _: + e = _ + tb = sys.exc_info()[2] + lines = self.get_report(e, tb) + self.assertThat(lines, DocTestMatches("""\ +Traceback (most recent call last): + File "...traceback2/tests/test_traceback.py", line ..., in test_context_suppression + raise_from(ZeroDivisionError, None) + File "", line 2, in raise_from +ZeroDivisionError +""", doctest.ELLIPSIS)) + + @unittest.skipIf(sys.version_info[:2] < (3, 2), "Only applies to 3.2+") + def test_cause_and_context(self): + # When both a cause and a context are set, only the cause should be + # displayed and the context should be muted. + def inner_raise(): + try: + self.zero_div() + except ZeroDivisionError as _e: + e = _e + try: + xyzzy + except NameError: + raise_from(KeyError, e) + def outer_raise(): + inner_raise() # Marker + blocks = boundaries.split(self.get_report(outer_raise)) + self.assertEqual(len(blocks), 3) + self.assertEqual(blocks[1], cause_message) + self.check_zero_div(blocks[0]) + 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 inner_raise(): + try: + try: + self.zero_div() + except ZeroDivisionError as e: + z = e + raise_from(KeyError, e) + except KeyError as e: + raise_from(z, e) + def outer_raise(): + inner_raise() # Marker + blocks = boundaries.split(self.get_report(outer_raise)) + self.assertEqual(len(blocks), 3) + self.assertEqual(blocks[1], cause_message) + # The first block is the KeyError raised from the ZeroDivisionError + self.assertIn('raise_from(KeyError, e)', blocks[0]) + self.assertNotIn('1/0', blocks[0]) + # The second block (apart from the boundary) is the ZeroDivisionError + # re-raised from the KeyError + self.assertIn('inner_raise() # Marker', blocks[2]) + self.check_zero_div(blocks[2]) + + def test_syntax_error_offset_at_eol(self): + # See #10186. + def e(): + raise SyntaxError('', ('', 0, 5, u('hello'))) + msg = self.get_report(e).splitlines() + self.assertEqual(msg[-2], " ^") + def e(): + exec("x = 5 | 4 |") + msg = self.get_report(e).splitlines() + self.assertEqual(msg[-2], ' ^') + + +class PyExcReportingTests(BaseExceptionReportingTests, testtools.TestCase): + # + # This checks reporting through the 'traceback' module, with both + # format_exception() and print_exception(). + # + + def get_report(self, e, tb=None): + e, tb = self.get_exception(e, tb) + s = ''.join( + traceback.format_exception(type(e), e, tb)) + with captured_output("stderr") as sio: + traceback.print_exception(type(e), e, tb) + self.assertEqual(sio.getvalue(), s) + return s + + +class MiscTracebackCases(unittest.TestCase): + # + # Check non-printing functions in traceback module + # + + def test_clear(self): + def outer(): + middle() + def middle(): + inner() + def inner(): + i = 1 + 1/0 + + try: + outer() + except: + type_, value, tb = sys.exc_info() + + # Initial assertion: there's one local in the inner frame. + inner_frame = tb.tb_next.tb_next.tb_next.tb_frame + self.assertEqual(len(inner_frame.f_locals), 1) + + # Clear traceback frames + traceback.clear_frames(tb) + + # Local variable dict should now be empty (on Python 3.4+) + if sys.version_info[:2] > (3, 3): + self.assertEqual({}, inner_frame.f_locals) + + +class TestFrame(unittest.TestCase): + + def test_basics(self): + linecache.clearcache() + linecache.lazycache("f", fake_module) + f = traceback.FrameSummary("f", 1, "dummy") + self.assertEqual( + ("f", 1, "dummy", '"""Test cases for traceback module"""'), + tuple(f)) + self.assertEqual(None, f.locals) + + def test_lazy_lines(self): + linecache.clearcache() + f = traceback.FrameSummary("f", 1, "dummy", lookup_line=False) + self.assertEqual(None, f._line) + linecache.lazycache("f", fake_module) + self.assertEqual( + '"""Test cases for traceback module"""', + f.line) + + def test_explicit_line(self): + f = traceback.FrameSummary("f", 1, "dummy", line="line") + self.assertEqual("line", f.line) + + +class TestStack(unittest.TestCase): + + def test_walk_stack(self): + s = list(traceback.walk_stack(None)) + self.assertGreater(len(s), 10) + + def test_walk_tb(self): + try: + 1/0 + except Exception: + _, _, tb = sys.exc_info() + s = list(traceback.walk_tb(tb)) + self.assertEqual(len(s), 1) + + def test_extract_stack(self): + s = traceback.StackSummary.extract(traceback.walk_stack(None)) + self.assertIsInstance(s, traceback.StackSummary) + + def test_extract_stack_limit(self): + s = traceback.StackSummary.extract(traceback.walk_stack(None), limit=5) + self.assertEqual(len(s), 5) + + def test_extract_stack_lookup_lines(self): + linecache.clearcache() + linecache.updatecache('/foo.py', fake_module) + c = test_code('/foo.py', 'method') + f = test_frame(c, None, None) + s = traceback.StackSummary.extract(iter([(f, 8)]), lookup_lines=True) + linecache.clearcache() + self.assertEqual(s[0].line, "import sys") + + def test_extract_stackup_deferred_lookup_lines(self): + linecache.clearcache() + c = test_code('/foo.py', 'method') + f = test_frame(c, None, None) + s = traceback.StackSummary.extract(iter([(f, 8)]), lookup_lines=False) + self.assertEqual({}, linecache.cache) + linecache.updatecache('/foo.py', fake_module) + self.assertEqual(s[0].line, "import sys") + + def test_from_list(self): + s = traceback.StackSummary.from_list([('foo.py', 1, 'fred', 'line')]) + self.assertEqual( + [' File "foo.py", line 1, in fred\n line\n'], + s.format()) + + def test_format_smoke(self): + # For detailed tests see the format_list tests, which consume the same + # code. + s = traceback.StackSummary.from_list([('foo.py', 1, 'fred', 'line')]) + self.assertEqual( + [' File "foo.py", line 1, in fred\n line\n'], + s.format()) + + @unittest.skipIf(sys.getfilesystemencoding()=='ANSI_X3.4-1968', + 'Requires non-ascii fs encoding') + def test_format_unicode_filename(self): + # Filenames in Python2 may be bytestrings that will fail to implicit + # decode. + fname = u('\u5341').encode(sys.getfilesystemencoding()) + s = traceback.StackSummary.from_list([(fname, 1, 'fred', 'line')]) + self.assertEqual( + [u(' File "\u5341", line 1, in fred\n line\n')], + s.format()) + + def test_format_bad_filename(self): + # Filenames in Python2 may be bytestrings that will fail to implicit + # decode. + # This won't decode via the implicit(ascii) codec or the default + # fs encoding (unless the encoding is a wildcard encoding). + fname = b('\x8b') + s = traceback.StackSummary.from_list([(fname, 1, 'fred', 'line')]) + self.assertEqual( + [' File "b\'\\x8b\'", line 1, in fred\n line\n'], + s.format()) + + def test_locals(self): + linecache.updatecache('/foo.py', globals()) + c = test_code('/foo.py', 'method') + f = test_frame(c, globals(), {'something': 1}) + s = traceback.StackSummary.extract(iter([(f, 6)]), capture_locals=True) + self.assertEqual(s[0].locals, {'something': '1'}) + + def test_no_locals(self): + linecache.updatecache('/foo.py', globals()) + c = test_code('/foo.py', 'method') + f = test_frame(c, globals(), {'something': 1}) + s = traceback.StackSummary.extract(iter([(f, 6)])) + self.assertEqual(s[0].locals, None) + + def test_format_locals(self): + def some_inner(k, v): + a = 1 + b = 2 + return traceback.StackSummary.extract( + traceback.walk_stack(None), capture_locals=True, limit=1) + s = some_inner(3, 4) + self.assertEqual( + [' File "' + FNAME + '", line 651, ' + 'in some_inner\n' + ' traceback.walk_stack(None), capture_locals=True, limit=1)\n' + ' a = 1\n' + ' b = 2\n' + ' k = 3\n' + ' v = 4\n' + ], s.format()) + + + +class TestTracebackException(unittest.TestCase): + + def test_smoke(self): + try: + 1/0 + except Exception: + exc_info = sys.exc_info() + exc = traceback.TracebackException(*exc_info) + expected_stack = traceback.StackSummary.extract( + traceback.walk_tb(exc_info[2])) + self.assertEqual(None, exc.__cause__) + self.assertEqual(None, exc.__context__) + self.assertEqual(False, exc.__suppress_context__) + self.assertEqual(expected_stack, exc.stack) + self.assertEqual(exc_info[0], exc.exc_type) + self.assertEqual(str(exc_info[1]), str(exc)) + + @unittest.skipIf(sys.version_info[:2] < (3, 0), "Only applies to 3+") + def test_from_exception(self): + # Check all the parameters are accepted. + def foo(): + 1/0 + try: + foo() + except Exception as e: + exc_info = sys.exc_info() + self.expected_stack = traceback.StackSummary.extract( + traceback.walk_tb(exc_info[2]), limit=1, lookup_lines=False, + capture_locals=True) + self.exc = traceback.TracebackException.from_exception( + e, limit=1, lookup_lines=False, capture_locals=True) + expected_stack = self.expected_stack + exc = self.exc + self.assertEqual(None, exc.__cause__) + self.assertEqual(None, exc.__context__) + self.assertEqual(False, exc.__suppress_context__) + self.assertEqual(expected_stack, exc.stack) + self.assertEqual(exc_info[0], exc.exc_type) + self.assertEqual(str(exc_info[1]), str(exc)) + + @unittest.skipIf(sys.version_info[:2] < (3, 2), "Only applies to 3.2+") + def test_cause(self): + try: + try: + 1/0 + finally: + exc_info_context = sys.exc_info() + exc_context = traceback.TracebackException(*exc_info_context) + cause = Exception("cause") + raise_from(Exception("uh ok"), cause) + except Exception: + exc_info = sys.exc_info() + exc = traceback.TracebackException(*exc_info) + expected_stack = traceback.StackSummary.extract( + traceback.walk_tb(exc_info[2])) + exc_cause = traceback.TracebackException(Exception, cause, None) + self.assertEqual(exc_cause, exc.__cause__) + self.assertEqual(exc_context, exc.__context__) + if hasattr(exc_info[1], '__suppress_context__'): + self.assertEqual(True, exc.__suppress_context__) + self.assertEqual(expected_stack, exc.stack) + self.assertEqual(exc_info[0], exc.exc_type) + self.assertEqual(str(exc_info[1]), str(exc)) + + @unittest.skipIf(sys.version_info[:2] < (3, 2), "Only applies to 3.2+") + def test_context(self): + try: + try: + 1/0 + finally: + exc_info_context = sys.exc_info() + exc_context = traceback.TracebackException(*exc_info_context) + raise Exception("uh oh") + except Exception: + exc_info = sys.exc_info() + exc = traceback.TracebackException(*exc_info) + expected_stack = traceback.StackSummary.extract( + traceback.walk_tb(exc_info[2])) + self.assertEqual(None, exc.__cause__) + self.assertEqual(exc_context, exc.__context__) + self.assertEqual(False, exc.__suppress_context__) + self.assertEqual(expected_stack, exc.stack) + self.assertEqual(exc_info[0], exc.exc_type) + self.assertEqual(str(exc_info[1]), str(exc)) + + def test_limit(self): + def recurse(n): + if n: + recurse(n-1) + else: + 1/0 + try: + recurse(10) + except Exception: + exc_info = sys.exc_info() + exc = traceback.TracebackException(*exc_info, limit=5) + expected_stack = traceback.StackSummary.extract( + traceback.walk_tb(exc_info[2]), limit=5) + self.assertEqual(expected_stack, exc.stack) + + def test_lookup_lines(self): + linecache.clearcache() + e = Exception("uh oh") + c = test_code('/foo.py', 'method') + f = test_frame(c, None, None) + tb = test_tb(f, 8, None) + exc = traceback.TracebackException(Exception, e, tb, lookup_lines=False) + self.assertEqual({}, linecache.cache) + linecache.updatecache('/foo.py', fake_module) + self.assertEqual(exc.stack[0].line, "import sys") + + def test_locals(self): + linecache.updatecache('/foo.py', fake_module) + e = Exception("uh oh") + c = test_code('/foo.py', 'method') + f = test_frame(c, globals(), {'something': 1, 'other': 'string'}) + tb = test_tb(f, 6, None) + exc = traceback.TracebackException( + Exception, e, tb, capture_locals=True) + self.assertEqual( + exc.stack[0].locals, {'something': '1', 'other': "'string'"}) + + def test_no_locals(self): + linecache.updatecache('/foo.py', fake_module) + e = Exception("uh oh") + c = test_code('/foo.py', 'method') + f = test_frame(c, fake_module, {'something': 1}) + tb = test_tb(f, 6, None) + exc = traceback.TracebackException(Exception, e, tb) + self.assertEqual(exc.stack[0].locals, None) + + def test_syntax_no_extras(self): + linecache.updatecache('/foo.py', fake_module) + e = SyntaxError("uh oh") + c = test_code('/foo.py', 'method') + f = test_frame(c, fake_module, {'something': 1}) + tb = test_tb(f, 6, None) + exc = traceback.TracebackException(SyntaxError, e, tb) + self.assertEqual([ + u('Traceback (most recent call last):\n'), + u(' File "/foo.py", line 6, in method\n from io import StringIO\n'), + u(' File "", line None\n'), + u('SyntaxError: uh oh\n')], + list(exc.format())) + + def test_syntax_undecoded_lines(self): + # If the interpreter returns bytestrings, we have to decode ourselves. + lines = u("1\n\u5341\n3\n") + fake_module = dict( + __name__='fred', + __loader__=FakeLoader(lines) + ) + linecache.updatecache('/foo.py', fake_module) + e = SyntaxError("uh oh") + e.filename = '/foo.py' + e.lineno = 2 + e.text = b('something wrong') + e.offset = 1 + c = test_code('/foo.py', 'method') + f = test_frame(c, fake_module, {'something': 1}) + tb = test_tb(f, 2, None) + exc = traceback.TracebackException(SyntaxError, e, tb) + list(exc.format_exception_only()) + self.assertEqual([ + u('Traceback (most recent call last):\n'), + u(' File "/foo.py", line 2, in method\n \u5341\n'), + u(' File "/foo.py", line 2\n'), + u(' \u5341\n'), + u(' ^\n'), + u('SyntaxError: uh oh\n')], + list(exc.format())) + + @unittest.skipUnless(sys.version_info[0] < 3, "Applies to 2.x only.") + @unittest.skipIf(sys.getfilesystemencoding()=='ANSI_X3.4-1968', + 'Requires non-ascii fs encoding') + def test_format_unicode_filename(self): + # Filenames in Python2 may be bytestrings that will fail to implicit + # decode. + fname = u('\u5341').encode(sys.getfilesystemencoding()) + lines = u("1\n2\n3\n") + fake_module = dict( + __name__='fred', + __loader__=FakeLoader(lines) + ) + linecache.updatecache(fname, fake_module) + e = SyntaxError("uh oh") + e.filename = fname + e.lineno = 2 + e.text = b('something wrong') + e.offset = 1 + c = test_code(fname, 'method') + f = test_frame(c, fake_module, {'something': 1}) + tb = test_tb(f, 2, None) + exc = traceback.TracebackException(SyntaxError, e, tb) + list(exc.format_exception_only()) + self.assertEqual([ + u('Traceback (most recent call last):\n'), + u(' File "\u5341", line 2, in method\n 2\n'), + u(' File "\u5341", line 2\n'), + u(' something wrong\n'), + u(' ^\n'), + u('SyntaxError: uh oh\n')], + list(exc.format())) + + @unittest.skipUnless(sys.version_info[0] < 3, "Applies to 2.x only.") + def test_format_bad_filename(self): + # Filenames in Python2 may be bytestrings that will fail to implicit + # decode. + # This won't decode via the implicit(ascii) codec or the default + # fs encoding (unless the encoding is a wildcard encoding). + fname = b('\x8b') + lines = u("1\n2\n3\n") + fake_module = dict( + __name__='fred', + __loader__=FakeLoader(lines) + ) + linecache.updatecache(fname, fake_module) + e = SyntaxError("uh oh") + e.filename = fname + e.lineno = 2 + e.text = b('something wrong') + e.offset = 1 + c = test_code(fname, 'method') + f = test_frame(c, fake_module, {'something': 1}) + tb = test_tb(f, 2, None) + exc = traceback.TracebackException(SyntaxError, e, tb) + list(exc.format_exception_only()) + self.assertEqual([ + u('Traceback (most recent call last):\n'), + b(' File "b\'\\x8b\'", line 2, in method\n 2\n').decode(), + b(' File "b\'\\x8b\'", line 2\n').decode(), + u(' something wrong\n'), + u(' ^\n'), + u('SyntaxError: uh oh\n')], + list(exc.format())) -- 2.34.1