2017-03-17 09:21:30 +08:00
|
|
|
from __future__ import absolute_import, division, print_function
|
2017-12-27 11:47:26 +08:00
|
|
|
import inspect
|
2015-11-27 22:43:01 +08:00
|
|
|
import sys
|
2017-12-27 11:47:26 +08:00
|
|
|
import traceback
|
2015-11-27 22:43:01 +08:00
|
|
|
from inspect import CO_VARARGS, CO_VARKEYWORDS
|
2018-03-09 21:21:56 +08:00
|
|
|
|
|
|
|
import attr
|
2016-04-02 23:24:08 +08:00
|
|
|
import re
|
2016-10-18 06:56:44 +08:00
|
|
|
from weakref import ref
|
2017-06-06 23:41:15 +08:00
|
|
|
from _pytest.compat import _PY2, _PY3, PY35, safe_str
|
2015-11-27 22:43:01 +08:00
|
|
|
|
|
|
|
import py
|
|
|
|
builtin_repr = repr
|
|
|
|
|
2017-04-12 14:18:09 +08:00
|
|
|
if _PY3:
|
2015-11-27 22:43:01 +08:00
|
|
|
from traceback import format_exception_only
|
|
|
|
else:
|
|
|
|
from ._py2traceback import format_exception_only
|
|
|
|
|
2016-09-22 08:44:25 +08:00
|
|
|
|
2015-11-27 22:43:01 +08:00
|
|
|
class Code(object):
|
|
|
|
""" wrapper around Python code objects """
|
2017-07-17 07:25:09 +08:00
|
|
|
|
2015-11-27 22:43:01 +08:00
|
|
|
def __init__(self, rawcode):
|
|
|
|
if not hasattr(rawcode, "co_filename"):
|
|
|
|
rawcode = getrawcode(rawcode)
|
|
|
|
try:
|
|
|
|
self.filename = rawcode.co_filename
|
|
|
|
self.firstlineno = rawcode.co_firstlineno - 1
|
|
|
|
self.name = rawcode.co_name
|
|
|
|
except AttributeError:
|
2017-07-17 07:25:08 +08:00
|
|
|
raise TypeError("not a code object: %r" % (rawcode,))
|
2015-11-27 22:43:01 +08:00
|
|
|
self.raw = rawcode
|
|
|
|
|
|
|
|
def __eq__(self, other):
|
|
|
|
return self.raw == other.raw
|
|
|
|
|
2016-09-23 00:22:12 +08:00
|
|
|
__hash__ = None
|
2016-09-22 08:44:25 +08:00
|
|
|
|
2015-11-27 22:43:01 +08:00
|
|
|
def __ne__(self, other):
|
|
|
|
return not self == other
|
|
|
|
|
|
|
|
@property
|
|
|
|
def path(self):
|
|
|
|
""" return a path object pointing to source code (note that it
|
|
|
|
might not point to an actually existing file). """
|
2016-06-07 21:43:05 +08:00
|
|
|
try:
|
|
|
|
p = py.path.local(self.raw.co_filename)
|
|
|
|
# maybe don't try this checking
|
|
|
|
if not p.check():
|
|
|
|
raise OSError("py.path check failed.")
|
|
|
|
except OSError:
|
2015-11-27 22:43:01 +08:00
|
|
|
# XXX maybe try harder like the weird logic
|
|
|
|
# in the standard lib [linecache.updatecache] does?
|
|
|
|
p = self.raw.co_filename
|
2016-06-07 21:43:05 +08:00
|
|
|
|
2015-11-27 22:43:01 +08:00
|
|
|
return p
|
|
|
|
|
|
|
|
@property
|
|
|
|
def fullsource(self):
|
|
|
|
""" return a _pytest._code.Source object for the full source file of the code
|
|
|
|
"""
|
|
|
|
from _pytest._code import source
|
|
|
|
full, _ = source.findsource(self.raw)
|
|
|
|
return full
|
|
|
|
|
|
|
|
def source(self):
|
|
|
|
""" return a _pytest._code.Source object for the code object's source only
|
|
|
|
"""
|
|
|
|
# return source only for that part of code
|
|
|
|
import _pytest._code
|
|
|
|
return _pytest._code.Source(self.raw)
|
|
|
|
|
|
|
|
def getargs(self, var=False):
|
|
|
|
""" return a tuple with the argument names for the code object
|
|
|
|
|
|
|
|
if 'var' is set True also return the names of the variable and
|
|
|
|
keyword arguments when present
|
|
|
|
"""
|
|
|
|
# handfull shortcut for getting args
|
|
|
|
raw = self.raw
|
|
|
|
argcount = raw.co_argcount
|
|
|
|
if var:
|
|
|
|
argcount += raw.co_flags & CO_VARARGS
|
|
|
|
argcount += raw.co_flags & CO_VARKEYWORDS
|
|
|
|
return raw.co_varnames[:argcount]
|
|
|
|
|
2017-07-17 07:25:09 +08:00
|
|
|
|
2015-11-27 22:43:01 +08:00
|
|
|
class Frame(object):
|
|
|
|
"""Wrapper around a Python frame holding f_locals and f_globals
|
|
|
|
in which expressions can be evaluated."""
|
|
|
|
|
|
|
|
def __init__(self, frame):
|
|
|
|
self.lineno = frame.f_lineno - 1
|
|
|
|
self.f_globals = frame.f_globals
|
|
|
|
self.f_locals = frame.f_locals
|
|
|
|
self.raw = frame
|
|
|
|
self.code = Code(frame.f_code)
|
|
|
|
|
|
|
|
@property
|
|
|
|
def statement(self):
|
|
|
|
""" statement this frame is at """
|
|
|
|
import _pytest._code
|
|
|
|
if self.code.fullsource is None:
|
|
|
|
return _pytest._code.Source("")
|
|
|
|
return self.code.fullsource.getstatement(self.lineno)
|
|
|
|
|
|
|
|
def eval(self, code, **vars):
|
|
|
|
""" evaluate 'code' in the frame
|
|
|
|
|
|
|
|
'vars' are optional additional local variables
|
|
|
|
|
|
|
|
returns the result of the evaluation
|
|
|
|
"""
|
|
|
|
f_locals = self.f_locals.copy()
|
|
|
|
f_locals.update(vars)
|
|
|
|
return eval(code, self.f_globals, f_locals)
|
|
|
|
|
|
|
|
def exec_(self, code, **vars):
|
|
|
|
""" exec 'code' in the frame
|
|
|
|
|
|
|
|
'vars' are optiona; additional local variables
|
|
|
|
"""
|
|
|
|
f_locals = self.f_locals.copy()
|
|
|
|
f_locals.update(vars)
|
2017-07-17 07:25:08 +08:00
|
|
|
py.builtin.exec_(code, self.f_globals, f_locals)
|
2015-11-27 22:43:01 +08:00
|
|
|
|
|
|
|
def repr(self, object):
|
|
|
|
""" return a 'safe' (non-recursive, one-line) string repr for 'object'
|
|
|
|
"""
|
|
|
|
return py.io.saferepr(object)
|
|
|
|
|
|
|
|
def is_true(self, object):
|
|
|
|
return object
|
|
|
|
|
|
|
|
def getargs(self, var=False):
|
|
|
|
""" return a list of tuples (name, value) for all arguments
|
|
|
|
|
|
|
|
if 'var' is set True also include the variable and keyword
|
|
|
|
arguments when present
|
|
|
|
"""
|
|
|
|
retval = []
|
|
|
|
for arg in self.code.getargs(var):
|
|
|
|
try:
|
|
|
|
retval.append((arg, self.f_locals[arg]))
|
|
|
|
except KeyError:
|
|
|
|
pass # this can occur when using Psyco
|
|
|
|
return retval
|
|
|
|
|
2017-07-17 07:25:09 +08:00
|
|
|
|
2015-11-27 22:43:01 +08:00
|
|
|
class TracebackEntry(object):
|
|
|
|
""" a single entry in a traceback """
|
|
|
|
|
|
|
|
_repr_style = None
|
|
|
|
exprinfo = None
|
|
|
|
|
2016-04-20 17:07:34 +08:00
|
|
|
def __init__(self, rawentry, excinfo=None):
|
|
|
|
self._excinfo = excinfo
|
2015-11-27 22:43:01 +08:00
|
|
|
self._rawentry = rawentry
|
|
|
|
self.lineno = rawentry.tb_lineno - 1
|
|
|
|
|
|
|
|
def set_repr_style(self, mode):
|
|
|
|
assert mode in ("short", "long")
|
|
|
|
self._repr_style = mode
|
|
|
|
|
|
|
|
@property
|
|
|
|
def frame(self):
|
|
|
|
import _pytest._code
|
|
|
|
return _pytest._code.Frame(self._rawentry.tb_frame)
|
|
|
|
|
|
|
|
@property
|
|
|
|
def relline(self):
|
|
|
|
return self.lineno - self.frame.code.firstlineno
|
|
|
|
|
|
|
|
def __repr__(self):
|
2017-07-17 07:25:08 +08:00
|
|
|
return "<TracebackEntry %s:%d>" % (self.frame.code.path, self.lineno + 1)
|
2015-11-27 22:43:01 +08:00
|
|
|
|
|
|
|
@property
|
|
|
|
def statement(self):
|
|
|
|
""" _pytest._code.Source object for the current statement """
|
|
|
|
source = self.frame.code.fullsource
|
|
|
|
return source.getstatement(self.lineno)
|
|
|
|
|
|
|
|
@property
|
|
|
|
def path(self):
|
|
|
|
""" path to the source code """
|
|
|
|
return self.frame.code.path
|
|
|
|
|
|
|
|
def getlocals(self):
|
|
|
|
return self.frame.f_locals
|
|
|
|
locals = property(getlocals, None, None, "locals of underlaying frame")
|
|
|
|
|
|
|
|
def getfirstlinesource(self):
|
|
|
|
# on Jython this firstlineno can be -1 apparently
|
|
|
|
return max(self.frame.code.firstlineno, 0)
|
|
|
|
|
|
|
|
def getsource(self, astcache=None):
|
|
|
|
""" return failing source code. """
|
|
|
|
# we use the passed in astcache to not reparse asttrees
|
|
|
|
# within exception info printing
|
|
|
|
from _pytest._code.source import getstatementrange_ast
|
|
|
|
source = self.frame.code.fullsource
|
|
|
|
if source is None:
|
|
|
|
return None
|
|
|
|
key = astnode = None
|
|
|
|
if astcache is not None:
|
|
|
|
key = self.frame.code.path
|
|
|
|
if key is not None:
|
|
|
|
astnode = astcache.get(key, None)
|
|
|
|
start = self.getfirstlinesource()
|
|
|
|
try:
|
|
|
|
astnode, _, end = getstatementrange_ast(self.lineno, source,
|
|
|
|
astnode=astnode)
|
|
|
|
except SyntaxError:
|
|
|
|
end = self.lineno + 1
|
|
|
|
else:
|
|
|
|
if key is not None:
|
|
|
|
astcache[key] = astnode
|
|
|
|
return source[start:end]
|
|
|
|
|
|
|
|
source = property(getsource)
|
|
|
|
|
|
|
|
def ishidden(self):
|
|
|
|
""" return True if the current frame has a var __tracebackhide__
|
|
|
|
resolving to True
|
|
|
|
|
2016-04-20 17:07:34 +08:00
|
|
|
If __tracebackhide__ is a callable, it gets called with the
|
|
|
|
ExceptionInfo instance and can decide whether to hide the traceback.
|
2016-04-20 16:25:33 +08:00
|
|
|
|
2015-11-27 22:43:01 +08:00
|
|
|
mostly for internal use
|
|
|
|
"""
|
|
|
|
try:
|
2016-04-20 16:25:33 +08:00
|
|
|
tbh = self.frame.f_locals['__tracebackhide__']
|
2015-11-27 22:43:01 +08:00
|
|
|
except KeyError:
|
|
|
|
try:
|
2016-04-20 16:25:33 +08:00
|
|
|
tbh = self.frame.f_globals['__tracebackhide__']
|
2015-11-27 22:43:01 +08:00
|
|
|
except KeyError:
|
|
|
|
return False
|
|
|
|
|
2017-08-03 01:33:52 +08:00
|
|
|
if callable(tbh):
|
2016-10-18 06:56:44 +08:00
|
|
|
return tbh(None if self._excinfo is None else self._excinfo())
|
2016-04-20 16:25:33 +08:00
|
|
|
else:
|
|
|
|
return tbh
|
|
|
|
|
2015-11-27 22:43:01 +08:00
|
|
|
def __str__(self):
|
|
|
|
try:
|
|
|
|
fn = str(self.path)
|
|
|
|
except py.error.Error:
|
|
|
|
fn = '???'
|
|
|
|
name = self.frame.code.name
|
|
|
|
try:
|
|
|
|
line = str(self.statement).lstrip()
|
|
|
|
except KeyboardInterrupt:
|
|
|
|
raise
|
2017-11-04 23:21:34 +08:00
|
|
|
except: # noqa
|
2015-11-27 22:43:01 +08:00
|
|
|
line = "???"
|
2017-07-17 07:25:08 +08:00
|
|
|
return " File %r:%d in %s\n %s\n" % (fn, self.lineno + 1, name, line)
|
2015-11-27 22:43:01 +08:00
|
|
|
|
|
|
|
def name(self):
|
|
|
|
return self.frame.code.raw.co_name
|
|
|
|
name = property(name, None, None, "co_name of underlaying code")
|
|
|
|
|
2017-07-17 07:25:09 +08:00
|
|
|
|
2015-11-27 22:43:01 +08:00
|
|
|
class Traceback(list):
|
|
|
|
""" Traceback objects encapsulate and offer higher level
|
|
|
|
access to Traceback entries.
|
|
|
|
"""
|
|
|
|
Entry = TracebackEntry
|
2017-07-17 07:25:09 +08:00
|
|
|
|
2016-04-20 17:07:34 +08:00
|
|
|
def __init__(self, tb, excinfo=None):
|
|
|
|
""" initialize from given python traceback object and ExceptionInfo """
|
|
|
|
self._excinfo = excinfo
|
2015-11-27 22:43:01 +08:00
|
|
|
if hasattr(tb, 'tb_next'):
|
|
|
|
def f(cur):
|
|
|
|
while cur is not None:
|
2016-04-20 17:07:34 +08:00
|
|
|
yield self.Entry(cur, excinfo=excinfo)
|
2015-11-27 22:43:01 +08:00
|
|
|
cur = cur.tb_next
|
|
|
|
list.__init__(self, f(tb))
|
|
|
|
else:
|
|
|
|
list.__init__(self, tb)
|
|
|
|
|
|
|
|
def cut(self, path=None, lineno=None, firstlineno=None, excludepath=None):
|
|
|
|
""" return a Traceback instance wrapping part of this Traceback
|
|
|
|
|
|
|
|
by provding any combination of path, lineno and firstlineno, the
|
|
|
|
first frame to start the to-be-returned traceback is determined
|
|
|
|
|
|
|
|
this allows cutting the first part of a Traceback instance e.g.
|
|
|
|
for formatting reasons (removing some uninteresting bits that deal
|
|
|
|
with handling of the exception/traceback)
|
|
|
|
"""
|
|
|
|
for x in self:
|
|
|
|
code = x.frame.code
|
|
|
|
codepath = code.path
|
|
|
|
if ((path is None or codepath == path) and
|
|
|
|
(excludepath is None or not hasattr(codepath, 'relto') or
|
|
|
|
not codepath.relto(excludepath)) and
|
|
|
|
(lineno is None or x.lineno == lineno) and
|
2017-07-17 07:25:07 +08:00
|
|
|
(firstlineno is None or x.frame.code.firstlineno == firstlineno)):
|
2016-04-20 17:07:34 +08:00
|
|
|
return Traceback(x._rawentry, self._excinfo)
|
2015-11-27 22:43:01 +08:00
|
|
|
return self
|
|
|
|
|
|
|
|
def __getitem__(self, key):
|
|
|
|
val = super(Traceback, self).__getitem__(key)
|
|
|
|
if isinstance(key, type(slice(0))):
|
|
|
|
val = self.__class__(val)
|
|
|
|
return val
|
|
|
|
|
|
|
|
def filter(self, fn=lambda x: not x.ishidden()):
|
|
|
|
""" return a Traceback instance with certain items removed
|
|
|
|
|
2016-04-20 14:35:17 +08:00
|
|
|
fn is a function that gets a single argument, a TracebackEntry
|
2015-11-27 22:43:01 +08:00
|
|
|
instance, and should return True when the item should be added
|
|
|
|
to the Traceback, False when not
|
|
|
|
|
2016-04-20 14:35:17 +08:00
|
|
|
by default this removes all the TracebackEntries which are hidden
|
2015-11-27 22:43:01 +08:00
|
|
|
(see ishidden() above)
|
|
|
|
"""
|
2016-04-20 17:07:34 +08:00
|
|
|
return Traceback(filter(fn, self), self._excinfo)
|
2015-11-27 22:43:01 +08:00
|
|
|
|
|
|
|
def getcrashentry(self):
|
|
|
|
""" return last non-hidden traceback entry that lead
|
|
|
|
to the exception of a traceback.
|
|
|
|
"""
|
2017-07-17 07:25:08 +08:00
|
|
|
for i in range(-1, -len(self) - 1, -1):
|
2015-11-27 22:43:01 +08:00
|
|
|
entry = self[i]
|
|
|
|
if not entry.ishidden():
|
|
|
|
return entry
|
|
|
|
return self[-1]
|
|
|
|
|
|
|
|
def recursionindex(self):
|
2016-04-20 14:35:17 +08:00
|
|
|
""" return the index of the frame/TracebackEntry where recursion
|
2015-11-27 22:43:01 +08:00
|
|
|
originates if appropriate, None if no recursion occurred
|
|
|
|
"""
|
|
|
|
cache = {}
|
|
|
|
for i, entry in enumerate(self):
|
|
|
|
# id for the code.raw is needed to work around
|
|
|
|
# the strange metaprogramming in the decorator lib from pypi
|
|
|
|
# which generates code objects that have hash/value equality
|
2017-07-17 07:25:09 +08:00
|
|
|
# XXX needs a test
|
2015-11-27 22:43:01 +08:00
|
|
|
key = entry.frame.code.path, id(entry.frame.code.raw), entry.lineno
|
2017-07-17 07:25:09 +08:00
|
|
|
# print "checking for recursion at", key
|
2017-11-04 23:17:20 +08:00
|
|
|
values = cache.setdefault(key, [])
|
|
|
|
if values:
|
2015-11-27 22:43:01 +08:00
|
|
|
f = entry.frame
|
|
|
|
loc = f.f_locals
|
2017-11-04 23:17:20 +08:00
|
|
|
for otherloc in values:
|
2015-11-27 22:43:01 +08:00
|
|
|
if f.is_true(f.eval(co_equal,
|
2017-07-17 07:25:07 +08:00
|
|
|
__recursioncache_locals_1=loc,
|
|
|
|
__recursioncache_locals_2=otherloc)):
|
2015-11-27 22:43:01 +08:00
|
|
|
return i
|
2017-11-04 23:17:20 +08:00
|
|
|
values.append(entry.frame.f_locals)
|
2015-11-27 22:43:01 +08:00
|
|
|
return None
|
|
|
|
|
2016-11-21 04:59:15 +08:00
|
|
|
|
2015-11-27 22:43:01 +08:00
|
|
|
co_equal = compile('__recursioncache_locals_1 == __recursioncache_locals_2',
|
|
|
|
'?', 'eval')
|
|
|
|
|
2017-07-17 07:25:09 +08:00
|
|
|
|
2015-11-27 22:43:01 +08:00
|
|
|
class ExceptionInfo(object):
|
|
|
|
""" wraps sys.exc_info() objects and offers
|
|
|
|
help for navigating the traceback.
|
|
|
|
"""
|
|
|
|
_striptext = ''
|
2017-04-12 14:18:09 +08:00
|
|
|
_assert_start_repr = "AssertionError(u\'assert " if _PY2 else "AssertionError(\'assert "
|
2017-03-04 16:26:46 +08:00
|
|
|
|
2015-11-27 22:43:01 +08:00
|
|
|
def __init__(self, tup=None, exprinfo=None):
|
|
|
|
import _pytest._code
|
|
|
|
if tup is None:
|
|
|
|
tup = sys.exc_info()
|
|
|
|
if exprinfo is None and isinstance(tup[1], AssertionError):
|
|
|
|
exprinfo = getattr(tup[1], 'msg', None)
|
|
|
|
if exprinfo is None:
|
2017-03-04 16:26:46 +08:00
|
|
|
exprinfo = py.io.saferepr(tup[1])
|
|
|
|
if exprinfo and exprinfo.startswith(self._assert_start_repr):
|
2015-11-27 22:43:01 +08:00
|
|
|
self._striptext = 'AssertionError: '
|
|
|
|
self._excinfo = tup
|
|
|
|
#: the exception class
|
|
|
|
self.type = tup[0]
|
|
|
|
#: the exception instance
|
|
|
|
self.value = tup[1]
|
|
|
|
#: the exception raw traceback
|
|
|
|
self.tb = tup[2]
|
|
|
|
#: the exception type name
|
|
|
|
self.typename = self.type.__name__
|
|
|
|
#: the exception traceback (_pytest._code.Traceback instance)
|
2016-10-18 06:56:44 +08:00
|
|
|
self.traceback = _pytest._code.Traceback(self.tb, excinfo=ref(self))
|
2015-11-27 22:43:01 +08:00
|
|
|
|
|
|
|
def __repr__(self):
|
|
|
|
return "<ExceptionInfo %s tblen=%d>" % (self.typename, len(self.traceback))
|
|
|
|
|
|
|
|
def exconly(self, tryshort=False):
|
|
|
|
""" return the exception as a string
|
|
|
|
|
|
|
|
when 'tryshort' resolves to True, and the exception is a
|
|
|
|
_pytest._code._AssertionError, only the actual exception part of
|
|
|
|
the exception representation is returned (so 'AssertionError: ' is
|
|
|
|
removed from the beginning)
|
|
|
|
"""
|
|
|
|
lines = format_exception_only(self.type, self.value)
|
|
|
|
text = ''.join(lines)
|
|
|
|
text = text.rstrip()
|
|
|
|
if tryshort:
|
|
|
|
if text.startswith(self._striptext):
|
|
|
|
text = text[len(self._striptext):]
|
|
|
|
return text
|
|
|
|
|
|
|
|
def errisinstance(self, exc):
|
|
|
|
""" return True if the exception is an instance of exc """
|
|
|
|
return isinstance(self.value, exc)
|
|
|
|
|
|
|
|
def _getreprcrash(self):
|
|
|
|
exconly = self.exconly(tryshort=True)
|
|
|
|
entry = self.traceback.getcrashentry()
|
|
|
|
path, lineno = entry.frame.code.raw.co_filename, entry.lineno
|
2017-07-17 07:25:08 +08:00
|
|
|
return ReprFileLocation(path, lineno + 1, exconly)
|
2015-11-27 22:43:01 +08:00
|
|
|
|
|
|
|
def getrepr(self, showlocals=False, style="long",
|
2017-07-17 07:25:07 +08:00
|
|
|
abspath=False, tbfilter=True, funcargs=False):
|
2015-11-27 22:43:01 +08:00
|
|
|
""" return str()able representation of this exception info.
|
|
|
|
showlocals: show locals per traceback entry
|
|
|
|
style: long|short|no|native traceback style
|
|
|
|
tbfilter: hide entries (where __tracebackhide__ is true)
|
|
|
|
|
|
|
|
in case of style==native, tbfilter and showlocals is ignored.
|
|
|
|
"""
|
|
|
|
if style == 'native':
|
|
|
|
return ReprExceptionInfo(ReprTracebackNative(
|
2017-12-27 11:47:26 +08:00
|
|
|
traceback.format_exception(
|
2015-11-27 22:43:01 +08:00
|
|
|
self.type,
|
|
|
|
self.value,
|
|
|
|
self.traceback[0]._rawentry,
|
|
|
|
)), self._getreprcrash())
|
|
|
|
|
|
|
|
fmt = FormattedExcinfo(showlocals=showlocals, style=style,
|
2017-07-17 07:25:07 +08:00
|
|
|
abspath=abspath, tbfilter=tbfilter, funcargs=funcargs)
|
2015-11-27 22:43:01 +08:00
|
|
|
return fmt.repr_excinfo(self)
|
|
|
|
|
|
|
|
def __str__(self):
|
|
|
|
entry = self.traceback[-1]
|
|
|
|
loc = ReprFileLocation(entry.path, entry.lineno + 1, self.exconly())
|
|
|
|
return str(loc)
|
|
|
|
|
|
|
|
def __unicode__(self):
|
|
|
|
entry = self.traceback[-1]
|
|
|
|
loc = ReprFileLocation(entry.path, entry.lineno + 1, self.exconly())
|
|
|
|
return unicode(loc)
|
|
|
|
|
2016-04-02 23:24:08 +08:00
|
|
|
def match(self, regexp):
|
|
|
|
"""
|
|
|
|
Match the regular expression 'regexp' on the string representation of
|
|
|
|
the exception. If it matches then True is returned (so that it is
|
|
|
|
possible to write 'assert excinfo.match()'). If it doesn't match an
|
|
|
|
AssertionError is raised.
|
|
|
|
"""
|
|
|
|
__tracebackhide__ = True
|
|
|
|
if not re.search(regexp, str(self.value)):
|
2018-05-18 05:31:16 +08:00
|
|
|
assert 0, "Pattern '{!s}' not found in '{!s}'".format(
|
2016-04-02 23:24:08 +08:00
|
|
|
regexp, self.value)
|
|
|
|
return True
|
|
|
|
|
2015-11-27 22:43:01 +08:00
|
|
|
|
2018-03-09 21:21:56 +08:00
|
|
|
@attr.s
|
2015-11-27 22:43:01 +08:00
|
|
|
class FormattedExcinfo(object):
|
|
|
|
""" presenting information about failing Functions and Generators. """
|
|
|
|
# for traceback entries
|
|
|
|
flow_marker = ">"
|
|
|
|
fail_marker = "E"
|
|
|
|
|
2018-03-09 21:21:56 +08:00
|
|
|
showlocals = attr.ib(default=False)
|
|
|
|
style = attr.ib(default="long")
|
|
|
|
abspath = attr.ib(default=True)
|
|
|
|
tbfilter = attr.ib(default=True)
|
|
|
|
funcargs = attr.ib(default=False)
|
|
|
|
astcache = attr.ib(default=attr.Factory(dict), init=False, repr=False)
|
2015-11-27 22:43:01 +08:00
|
|
|
|
|
|
|
def _getindent(self, source):
|
|
|
|
# figure out indent for given source
|
|
|
|
try:
|
2017-07-17 07:25:08 +08:00
|
|
|
s = str(source.getstatement(len(source) - 1))
|
2015-11-27 22:43:01 +08:00
|
|
|
except KeyboardInterrupt:
|
|
|
|
raise
|
2017-11-04 23:21:34 +08:00
|
|
|
except: # noqa
|
2015-11-27 22:43:01 +08:00
|
|
|
try:
|
|
|
|
s = str(source[-1])
|
|
|
|
except KeyboardInterrupt:
|
|
|
|
raise
|
2017-11-04 23:21:34 +08:00
|
|
|
except: # noqa
|
2015-11-27 22:43:01 +08:00
|
|
|
return 0
|
|
|
|
return 4 + (len(s) - len(s.lstrip()))
|
|
|
|
|
|
|
|
def _getentrysource(self, entry):
|
|
|
|
source = entry.getsource(self.astcache)
|
|
|
|
if source is not None:
|
|
|
|
source = source.deindent()
|
|
|
|
return source
|
|
|
|
|
|
|
|
def _saferepr(self, obj):
|
|
|
|
return py.io.saferepr(obj)
|
|
|
|
|
|
|
|
def repr_args(self, entry):
|
|
|
|
if self.funcargs:
|
|
|
|
args = []
|
|
|
|
for argname, argvalue in entry.frame.getargs(var=True):
|
|
|
|
args.append((argname, self._saferepr(argvalue)))
|
|
|
|
return ReprFuncArgs(args)
|
|
|
|
|
|
|
|
def get_source(self, source, line_index=-1, excinfo=None, short=False):
|
|
|
|
""" return formatted and marked up source lines. """
|
|
|
|
import _pytest._code
|
|
|
|
lines = []
|
|
|
|
if source is None or line_index >= len(source.lines):
|
|
|
|
source = _pytest._code.Source("???")
|
|
|
|
line_index = 0
|
|
|
|
if line_index < 0:
|
|
|
|
line_index += len(source)
|
|
|
|
space_prefix = " "
|
|
|
|
if short:
|
|
|
|
lines.append(space_prefix + source.lines[line_index].strip())
|
|
|
|
else:
|
|
|
|
for line in source.lines[:line_index]:
|
|
|
|
lines.append(space_prefix + line)
|
|
|
|
lines.append(self.flow_marker + " " + source.lines[line_index])
|
2017-07-17 07:25:08 +08:00
|
|
|
for line in source.lines[line_index + 1:]:
|
2015-11-27 22:43:01 +08:00
|
|
|
lines.append(space_prefix + line)
|
|
|
|
if excinfo is not None:
|
|
|
|
indent = 4 if short else self._getindent(source)
|
|
|
|
lines.extend(self.get_exconly(excinfo, indent=indent, markall=True))
|
|
|
|
return lines
|
|
|
|
|
|
|
|
def get_exconly(self, excinfo, indent=4, markall=False):
|
|
|
|
lines = []
|
|
|
|
indent = " " * indent
|
|
|
|
# get the real exception information out
|
|
|
|
exlines = excinfo.exconly(tryshort=True).split('\n')
|
|
|
|
failindent = self.fail_marker + indent[1:]
|
|
|
|
for line in exlines:
|
|
|
|
lines.append(failindent + line)
|
|
|
|
if not markall:
|
|
|
|
failindent = indent
|
|
|
|
return lines
|
|
|
|
|
|
|
|
def repr_locals(self, locals):
|
|
|
|
if self.showlocals:
|
|
|
|
lines = []
|
|
|
|
keys = [loc for loc in locals if loc[0] != "@"]
|
|
|
|
keys.sort()
|
|
|
|
for name in keys:
|
|
|
|
value = locals[name]
|
|
|
|
if name == '__builtins__':
|
|
|
|
lines.append("__builtins__ = <builtins>")
|
|
|
|
else:
|
|
|
|
# This formatting could all be handled by the
|
|
|
|
# _repr() function, which is only reprlib.Repr in
|
|
|
|
# disguise, so is very configurable.
|
|
|
|
str_repr = self._saferepr(value)
|
2017-07-17 07:25:09 +08:00
|
|
|
# if len(str_repr) < 70 or not isinstance(value,
|
2015-11-27 22:43:01 +08:00
|
|
|
# (list, tuple, dict)):
|
2017-07-17 07:25:08 +08:00
|
|
|
lines.append("%-10s = %s" % (name, str_repr))
|
2017-07-17 07:25:09 +08:00
|
|
|
# else:
|
2015-11-27 22:43:01 +08:00
|
|
|
# self._line("%-10s =\\" % (name,))
|
|
|
|
# # XXX
|
2017-12-27 11:47:26 +08:00
|
|
|
# pprint.pprint(value, stream=self.excinfowriter)
|
2015-11-27 22:43:01 +08:00
|
|
|
return ReprLocals(lines)
|
|
|
|
|
|
|
|
def repr_traceback_entry(self, entry, excinfo=None):
|
|
|
|
import _pytest._code
|
|
|
|
source = self._getentrysource(entry)
|
|
|
|
if source is None:
|
|
|
|
source = _pytest._code.Source("???")
|
|
|
|
line_index = 0
|
|
|
|
else:
|
|
|
|
# entry.getfirstlinesource() can be -1, should be 0 on jython
|
|
|
|
line_index = entry.lineno - max(entry.getfirstlinesource(), 0)
|
|
|
|
|
|
|
|
lines = []
|
|
|
|
style = entry._repr_style
|
|
|
|
if style is None:
|
|
|
|
style = self.style
|
|
|
|
if style in ("short", "long"):
|
|
|
|
short = style == "short"
|
|
|
|
reprargs = self.repr_args(entry) if not short else None
|
|
|
|
s = self.get_source(source, line_index, excinfo, short=short)
|
|
|
|
lines.extend(s)
|
|
|
|
if short:
|
2017-07-17 07:25:08 +08:00
|
|
|
message = "in %s" % (entry.name)
|
2015-11-27 22:43:01 +08:00
|
|
|
else:
|
|
|
|
message = excinfo and excinfo.typename or ""
|
|
|
|
path = self._makepath(entry.path)
|
2017-07-17 07:25:08 +08:00
|
|
|
filelocrepr = ReprFileLocation(path, entry.lineno + 1, message)
|
2015-11-27 22:43:01 +08:00
|
|
|
localsrepr = None
|
|
|
|
if not short:
|
2017-07-17 07:25:08 +08:00
|
|
|
localsrepr = self.repr_locals(entry.locals)
|
2015-11-27 22:43:01 +08:00
|
|
|
return ReprEntry(lines, reprargs, localsrepr, filelocrepr, style)
|
|
|
|
if excinfo:
|
|
|
|
lines.extend(self.get_exconly(excinfo, indent=4))
|
|
|
|
return ReprEntry(lines, None, None, None, style)
|
|
|
|
|
|
|
|
def _makepath(self, path):
|
|
|
|
if not self.abspath:
|
|
|
|
try:
|
|
|
|
np = py.path.local().bestrelpath(path)
|
|
|
|
except OSError:
|
|
|
|
return path
|
|
|
|
if len(np) < len(str(path)):
|
|
|
|
path = np
|
|
|
|
return path
|
|
|
|
|
|
|
|
def repr_traceback(self, excinfo):
|
|
|
|
traceback = excinfo.traceback
|
|
|
|
if self.tbfilter:
|
|
|
|
traceback = traceback.filter()
|
2017-06-06 23:41:15 +08:00
|
|
|
|
2016-04-08 22:24:12 +08:00
|
|
|
if is_recursion_error(excinfo):
|
2017-06-06 23:41:15 +08:00
|
|
|
traceback, extraline = self._truncate_recursive_traceback(traceback)
|
|
|
|
else:
|
|
|
|
extraline = None
|
|
|
|
|
2015-11-27 22:43:01 +08:00
|
|
|
last = traceback[-1]
|
|
|
|
entries = []
|
|
|
|
for index, entry in enumerate(traceback):
|
|
|
|
einfo = (last == entry) and excinfo or None
|
|
|
|
reprentry = self.repr_traceback_entry(entry, einfo)
|
|
|
|
entries.append(reprentry)
|
|
|
|
return ReprTraceback(entries, extraline, style=self.style)
|
|
|
|
|
2017-06-06 23:41:15 +08:00
|
|
|
def _truncate_recursive_traceback(self, traceback):
|
|
|
|
"""
|
|
|
|
Truncate the given recursive traceback trying to find the starting point
|
|
|
|
of the recursion.
|
|
|
|
|
|
|
|
The detection is done by going through each traceback entry and finding the
|
|
|
|
point in which the locals of the frame are equal to the locals of a previous frame (see ``recursionindex()``.
|
|
|
|
|
|
|
|
Handle the situation where the recursion process might raise an exception (for example
|
|
|
|
comparing numpy arrays using equality raises a TypeError), in which case we do our best to
|
|
|
|
warn the user of the error and show a limited traceback.
|
|
|
|
"""
|
|
|
|
try:
|
|
|
|
recursionindex = traceback.recursionindex()
|
|
|
|
except Exception as e:
|
|
|
|
max_frames = 10
|
|
|
|
extraline = (
|
|
|
|
'!!! Recursion error detected, but an error occurred locating the origin of recursion.\n'
|
|
|
|
' The following exception happened when comparing locals in the stack frame:\n'
|
|
|
|
' {exc_type}: {exc_msg}\n'
|
|
|
|
' Displaying first and last {max_frames} stack frames out of {total}.'
|
|
|
|
).format(exc_type=type(e).__name__, exc_msg=safe_str(e), max_frames=max_frames, total=len(traceback))
|
|
|
|
traceback = traceback[:max_frames] + traceback[-max_frames:]
|
|
|
|
else:
|
2017-06-10 05:47:07 +08:00
|
|
|
if recursionindex is not None:
|
|
|
|
extraline = "!!! Recursion detected (same locals & position)"
|
|
|
|
traceback = traceback[:recursionindex + 1]
|
|
|
|
else:
|
|
|
|
extraline = None
|
2017-07-17 07:25:06 +08:00
|
|
|
|
2017-06-06 23:41:15 +08:00
|
|
|
return traceback, extraline
|
2016-03-20 06:02:17 +08:00
|
|
|
|
2015-11-27 22:43:01 +08:00
|
|
|
def repr_excinfo(self, excinfo):
|
2017-04-12 14:18:09 +08:00
|
|
|
if _PY2:
|
2016-03-20 06:02:17 +08:00
|
|
|
reprtraceback = self.repr_traceback(excinfo)
|
|
|
|
reprcrash = excinfo._getreprcrash()
|
|
|
|
|
|
|
|
return ReprExceptionInfo(reprtraceback, reprcrash)
|
|
|
|
else:
|
|
|
|
repr_chain = []
|
|
|
|
e = excinfo.value
|
|
|
|
descr = None
|
|
|
|
while e is not None:
|
2016-10-28 07:15:05 +08:00
|
|
|
if excinfo:
|
|
|
|
reprtraceback = self.repr_traceback(excinfo)
|
|
|
|
reprcrash = excinfo._getreprcrash()
|
|
|
|
else:
|
|
|
|
# fallback to native repr if the exception doesn't have a traceback:
|
|
|
|
# ExceptionInfo objects require a full traceback to work
|
2017-12-27 11:47:26 +08:00
|
|
|
reprtraceback = ReprTracebackNative(traceback.format_exception(type(e), e, None))
|
2016-10-28 07:15:05 +08:00
|
|
|
reprcrash = None
|
|
|
|
|
2016-03-20 06:02:17 +08:00
|
|
|
repr_chain += [(reprtraceback, reprcrash, descr)]
|
|
|
|
if e.__cause__ is not None:
|
|
|
|
e = e.__cause__
|
2016-10-28 07:15:05 +08:00
|
|
|
excinfo = ExceptionInfo((type(e), e, e.__traceback__)) if e.__traceback__ else None
|
2016-03-20 06:02:17 +08:00
|
|
|
descr = 'The above exception was the direct cause of the following exception:'
|
2017-07-29 17:39:17 +08:00
|
|
|
elif (e.__context__ is not None and not e.__suppress_context__):
|
2016-03-20 06:02:17 +08:00
|
|
|
e = e.__context__
|
2016-10-28 07:15:05 +08:00
|
|
|
excinfo = ExceptionInfo((type(e), e, e.__traceback__)) if e.__traceback__ else None
|
2016-03-20 06:02:17 +08:00
|
|
|
descr = 'During handling of the above exception, another exception occurred:'
|
|
|
|
else:
|
|
|
|
e = None
|
|
|
|
repr_chain.reverse()
|
|
|
|
return ExceptionChainRepr(repr_chain)
|
|
|
|
|
2015-11-27 22:43:01 +08:00
|
|
|
|
2016-03-20 06:02:17 +08:00
|
|
|
class TerminalRepr(object):
|
2015-11-27 22:43:01 +08:00
|
|
|
def __str__(self):
|
|
|
|
s = self.__unicode__()
|
2017-04-12 14:18:09 +08:00
|
|
|
if _PY2:
|
2015-11-27 22:43:01 +08:00
|
|
|
s = s.encode('utf-8')
|
|
|
|
return s
|
|
|
|
|
|
|
|
def __unicode__(self):
|
|
|
|
# FYI this is called from pytest-xdist's serialization of exception
|
|
|
|
# information.
|
|
|
|
io = py.io.TextIO()
|
|
|
|
tw = py.io.TerminalWriter(file=io)
|
|
|
|
self.toterminal(tw)
|
|
|
|
return io.getvalue().strip()
|
|
|
|
|
|
|
|
def __repr__(self):
|
2017-07-17 07:25:08 +08:00
|
|
|
return "<%s instance at %0x>" % (self.__class__, id(self))
|
2015-11-27 22:43:01 +08:00
|
|
|
|
|
|
|
|
2016-03-20 06:02:17 +08:00
|
|
|
class ExceptionRepr(TerminalRepr):
|
|
|
|
def __init__(self):
|
2015-11-27 22:43:01 +08:00
|
|
|
self.sections = []
|
|
|
|
|
|
|
|
def addsection(self, name, content, sep="-"):
|
|
|
|
self.sections.append((name, content, sep))
|
|
|
|
|
|
|
|
def toterminal(self, tw):
|
|
|
|
for name, content, sep in self.sections:
|
|
|
|
tw.sep(sep, name)
|
|
|
|
tw.line(content)
|
|
|
|
|
2016-03-20 06:02:17 +08:00
|
|
|
|
|
|
|
class ExceptionChainRepr(ExceptionRepr):
|
|
|
|
def __init__(self, chain):
|
|
|
|
super(ExceptionChainRepr, self).__init__()
|
|
|
|
self.chain = chain
|
|
|
|
# reprcrash and reprtraceback of the outermost (the newest) exception
|
|
|
|
# in the chain
|
|
|
|
self.reprtraceback = chain[-1][0]
|
|
|
|
self.reprcrash = chain[-1][1]
|
|
|
|
|
|
|
|
def toterminal(self, tw):
|
|
|
|
for element in self.chain:
|
|
|
|
element[0].toterminal(tw)
|
|
|
|
if element[2] is not None:
|
|
|
|
tw.line("")
|
|
|
|
tw.line(element[2], yellow=True)
|
|
|
|
super(ExceptionChainRepr, self).toterminal(tw)
|
|
|
|
|
|
|
|
|
|
|
|
class ReprExceptionInfo(ExceptionRepr):
|
|
|
|
def __init__(self, reprtraceback, reprcrash):
|
|
|
|
super(ReprExceptionInfo, self).__init__()
|
|
|
|
self.reprtraceback = reprtraceback
|
|
|
|
self.reprcrash = reprcrash
|
|
|
|
|
|
|
|
def toterminal(self, tw):
|
|
|
|
self.reprtraceback.toterminal(tw)
|
|
|
|
super(ReprExceptionInfo, self).toterminal(tw)
|
|
|
|
|
2017-07-17 07:25:09 +08:00
|
|
|
|
2015-11-27 22:43:01 +08:00
|
|
|
class ReprTraceback(TerminalRepr):
|
|
|
|
entrysep = "_ "
|
|
|
|
|
|
|
|
def __init__(self, reprentries, extraline, style):
|
|
|
|
self.reprentries = reprentries
|
|
|
|
self.extraline = extraline
|
|
|
|
self.style = style
|
|
|
|
|
|
|
|
def toterminal(self, tw):
|
|
|
|
# the entries might have different styles
|
|
|
|
for i, entry in enumerate(self.reprentries):
|
|
|
|
if entry.style == "long":
|
|
|
|
tw.line("")
|
|
|
|
entry.toterminal(tw)
|
|
|
|
if i < len(self.reprentries) - 1:
|
2017-07-17 07:25:08 +08:00
|
|
|
next_entry = self.reprentries[i + 1]
|
2015-11-27 22:43:01 +08:00
|
|
|
if entry.style == "long" or \
|
|
|
|
entry.style == "short" and next_entry.style == "long":
|
|
|
|
tw.sep(self.entrysep)
|
|
|
|
|
|
|
|
if self.extraline:
|
|
|
|
tw.line(self.extraline)
|
|
|
|
|
2017-07-17 07:25:09 +08:00
|
|
|
|
2015-11-27 22:43:01 +08:00
|
|
|
class ReprTracebackNative(ReprTraceback):
|
|
|
|
def __init__(self, tblines):
|
|
|
|
self.style = "native"
|
|
|
|
self.reprentries = [ReprEntryNative(tblines)]
|
|
|
|
self.extraline = None
|
|
|
|
|
2017-07-17 07:25:09 +08:00
|
|
|
|
2015-11-27 22:43:01 +08:00
|
|
|
class ReprEntryNative(TerminalRepr):
|
|
|
|
style = "native"
|
|
|
|
|
|
|
|
def __init__(self, tblines):
|
|
|
|
self.lines = tblines
|
|
|
|
|
|
|
|
def toterminal(self, tw):
|
|
|
|
tw.write("".join(self.lines))
|
|
|
|
|
2017-07-17 07:25:09 +08:00
|
|
|
|
2015-11-27 22:43:01 +08:00
|
|
|
class ReprEntry(TerminalRepr):
|
|
|
|
localssep = "_ "
|
|
|
|
|
|
|
|
def __init__(self, lines, reprfuncargs, reprlocals, filelocrepr, style):
|
|
|
|
self.lines = lines
|
|
|
|
self.reprfuncargs = reprfuncargs
|
|
|
|
self.reprlocals = reprlocals
|
|
|
|
self.reprfileloc = filelocrepr
|
|
|
|
self.style = style
|
|
|
|
|
|
|
|
def toterminal(self, tw):
|
|
|
|
if self.style == "short":
|
|
|
|
self.reprfileloc.toterminal(tw)
|
|
|
|
for line in self.lines:
|
|
|
|
red = line.startswith("E ")
|
|
|
|
tw.line(line, bold=True, red=red)
|
2017-07-17 07:25:09 +08:00
|
|
|
# tw.line("")
|
2015-11-27 22:43:01 +08:00
|
|
|
return
|
|
|
|
if self.reprfuncargs:
|
|
|
|
self.reprfuncargs.toterminal(tw)
|
|
|
|
for line in self.lines:
|
|
|
|
red = line.startswith("E ")
|
|
|
|
tw.line(line, bold=True, red=red)
|
|
|
|
if self.reprlocals:
|
2017-07-17 07:25:09 +08:00
|
|
|
# tw.sep(self.localssep, "Locals")
|
2015-11-27 22:43:01 +08:00
|
|
|
tw.line("")
|
|
|
|
self.reprlocals.toterminal(tw)
|
|
|
|
if self.reprfileloc:
|
|
|
|
if self.lines:
|
|
|
|
tw.line("")
|
|
|
|
self.reprfileloc.toterminal(tw)
|
|
|
|
|
|
|
|
def __str__(self):
|
|
|
|
return "%s\n%s\n%s" % ("\n".join(self.lines),
|
|
|
|
self.reprlocals,
|
|
|
|
self.reprfileloc)
|
|
|
|
|
2017-07-17 07:25:09 +08:00
|
|
|
|
2015-11-27 22:43:01 +08:00
|
|
|
class ReprFileLocation(TerminalRepr):
|
|
|
|
def __init__(self, path, lineno, message):
|
|
|
|
self.path = str(path)
|
|
|
|
self.lineno = lineno
|
|
|
|
self.message = message
|
|
|
|
|
|
|
|
def toterminal(self, tw):
|
|
|
|
# filename and lineno output for each entry,
|
|
|
|
# using an output format that most editors unterstand
|
|
|
|
msg = self.message
|
|
|
|
i = msg.find("\n")
|
|
|
|
if i != -1:
|
|
|
|
msg = msg[:i]
|
2016-07-30 17:12:35 +08:00
|
|
|
tw.write(self.path, bold=True, red=True)
|
|
|
|
tw.line(":%s: %s" % (self.lineno, msg))
|
2015-11-27 22:43:01 +08:00
|
|
|
|
2017-07-17 07:25:09 +08:00
|
|
|
|
2015-11-27 22:43:01 +08:00
|
|
|
class ReprLocals(TerminalRepr):
|
|
|
|
def __init__(self, lines):
|
|
|
|
self.lines = lines
|
|
|
|
|
|
|
|
def toterminal(self, tw):
|
|
|
|
for line in self.lines:
|
|
|
|
tw.line(line)
|
|
|
|
|
2017-07-17 07:25:09 +08:00
|
|
|
|
2015-11-27 22:43:01 +08:00
|
|
|
class ReprFuncArgs(TerminalRepr):
|
|
|
|
def __init__(self, args):
|
|
|
|
self.args = args
|
|
|
|
|
|
|
|
def toterminal(self, tw):
|
|
|
|
if self.args:
|
|
|
|
linesofar = ""
|
|
|
|
for name, value in self.args:
|
2017-08-31 03:06:12 +08:00
|
|
|
ns = "%s = %s" % (safe_str(name), safe_str(value))
|
2015-11-27 22:43:01 +08:00
|
|
|
if len(ns) + len(linesofar) + 2 > tw.fullwidth:
|
|
|
|
if linesofar:
|
|
|
|
tw.line(linesofar)
|
2017-07-17 07:25:08 +08:00
|
|
|
linesofar = ns
|
2015-11-27 22:43:01 +08:00
|
|
|
else:
|
|
|
|
if linesofar:
|
|
|
|
linesofar += ", " + ns
|
|
|
|
else:
|
|
|
|
linesofar = ns
|
|
|
|
if linesofar:
|
|
|
|
tw.line(linesofar)
|
|
|
|
tw.line("")
|
|
|
|
|
|
|
|
|
|
|
|
def getrawcode(obj, trycall=True):
|
|
|
|
""" return code object for given function. """
|
|
|
|
try:
|
|
|
|
return obj.__code__
|
|
|
|
except AttributeError:
|
|
|
|
obj = getattr(obj, 'im_func', obj)
|
|
|
|
obj = getattr(obj, 'func_code', obj)
|
|
|
|
obj = getattr(obj, 'f_code', obj)
|
|
|
|
obj = getattr(obj, '__code__', obj)
|
|
|
|
if trycall and not hasattr(obj, 'co_firstlineno'):
|
2017-12-27 11:47:26 +08:00
|
|
|
if hasattr(obj, '__call__') and not inspect.isclass(obj):
|
2015-11-27 22:43:01 +08:00
|
|
|
x = getrawcode(obj.__call__, trycall=False)
|
|
|
|
if hasattr(x, 'co_firstlineno'):
|
|
|
|
return x
|
|
|
|
return obj
|
|
|
|
|
2016-11-21 04:59:15 +08:00
|
|
|
|
2017-04-12 14:18:09 +08:00
|
|
|
if PY35: # RecursionError introduced in 3.5
|
2016-04-08 22:24:12 +08:00
|
|
|
def is_recursion_error(excinfo):
|
|
|
|
return excinfo.errisinstance(RecursionError) # noqa
|
|
|
|
else:
|
|
|
|
def is_recursion_error(excinfo):
|
|
|
|
if not excinfo.errisinstance(RuntimeError):
|
|
|
|
return False
|
|
|
|
try:
|
|
|
|
return "maximum recursion depth exceeded" in str(excinfo.value)
|
|
|
|
except UnicodeError:
|
|
|
|
return False
|