diff --git a/doc/changelog.txt b/doc/changelog.txt index 3c595a830..fa7089221 100644 --- a/doc/changelog.txt +++ b/doc/changelog.txt @@ -1,6 +1,8 @@ Changes between 1.0.x and 'trunk' ===================================== +* consolidate and cleanup py/code classes and files + * cleanup py/misc, move tests to bin-for-dist * introduce delattr/delitem/delenv methods to py.test's monkeypatch funcarg diff --git a/py/__init__.py b/py/__init__.py index 3058c23e5..44820715b 100644 --- a/py/__init__.py +++ b/py/__init__.py @@ -124,9 +124,9 @@ initpkg(__name__, 'code.compile' : ('./code/source.py', 'compile_'), 'code.Source' : ('./code/source.py', 'Source'), 'code.Code' : ('./code/code.py', 'Code'), - 'code.Frame' : ('./code/frame.py', 'Frame'), - 'code.ExceptionInfo' : ('./code/excinfo.py', 'ExceptionInfo'), - 'code.Traceback' : ('./code/traceback2.py', 'Traceback'), + 'code.Frame' : ('./code/code.py', 'Frame'), + 'code.ExceptionInfo' : ('./code/code.py', 'ExceptionInfo'), + 'code.Traceback' : ('./code/code.py', 'Traceback'), 'code.getfslineno' : ('./code/source.py', 'getfslineno'), # backports and additions of builtins diff --git a/py/code/code.py b/py/code/code.py index 56f8e4e23..c02ac81e9 100644 --- a/py/code/code.py +++ b/py/code/code.py @@ -1,5 +1,11 @@ import py -from py.__.code import source +import sys +try: + import repr +except ImportError: + import reprlib as repr + +from __builtin__ import repr as builtin_repr class Code(object): """ wrapper around Python code objects """ @@ -76,6 +82,7 @@ class Code(object): def fullsource(self): """ return a py.code.Source object for the full source file of the code """ + from py.__.code import source full, _ = source.findsource(self.raw) return full fullsource = property(fullsource, None, None, @@ -94,3 +101,629 @@ class Code(object): raw = self.raw return raw.co_varnames[:raw.co_argcount] +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.code = py.code.Code(frame.f_code) + self.lineno = frame.f_lineno - 1 + self.f_globals = frame.f_globals + self.f_locals = frame.f_locals + self.raw = frame + + def statement(self): + if self.code.fullsource is None: + return py.code.Source("") + return self.code.fullsource.getstatement(self.lineno) + statement = property(statement, None, None, + "statement this frame is at") + + 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) + exec code in self.f_globals, f_locals + + def repr(self, object): + """ return a 'safe' (non-recursive, one-line) string repr for 'object' + """ + return safe_repr(object) + + def is_true(self, object): + return object + + def getargs(self): + """ return a list of tuples (name, value) for all arguments + """ + retval = [] + for arg in self.code.getargs(): + try: + retval.append((arg, self.f_locals[arg])) + except KeyError: + pass # this can occur when using Psyco + return retval + +class TracebackEntry(object): + """ a single entry in a traceback """ + + exprinfo = None + + def __init__(self, rawentry): + self._rawentry = rawentry + self.frame = py.code.Frame(rawentry.tb_frame) + # Ugh. 2.4 and 2.5 differs here when encountering + # multi-line statements. Not sure about the solution, but + # should be portable + self.lineno = rawentry.tb_lineno - 1 + self.relline = self.lineno - self.frame.code.firstlineno + + def __repr__(self): + return "" %(self.frame.code.path, self.lineno+1) + + def statement(self): + """ return a py.code.Source object for the current statement """ + source = self.frame.code.fullsource + return source.getstatement(self.lineno) + statement = property(statement, None, None, + "statement of this traceback entry.") + + def path(self): + return self.frame.code.path + path = property(path, None, None, "path to the full source code") + + def getlocals(self): + return self.frame.f_locals + locals = property(getlocals, None, None, "locals of underlaying frame") + + def reinterpret(self): + """Reinterpret the failing statement and returns a detailed information + about what operations are performed.""" + if self.exprinfo is None: + from py.__.magic import exprinfo + source = str(self.statement).strip() + x = exprinfo.interpret(source, self.frame, should_fail=True) + if not isinstance(x, str): + raise TypeError, "interpret returned non-string %r" % (x,) + self.exprinfo = x + return self.exprinfo + + def getfirstlinesource(self): + return self.frame.code.firstlineno + + def getsource(self): + """ return failing source code. """ + source = self.frame.code.fullsource + if source is None: + return None + start = self.getfirstlinesource() + end = self.lineno + try: + _, end = source.getstatementrange(end) + except IndexError: + end = self.lineno + 1 + # heuristic to stop displaying source on e.g. + # if something: # assume this causes a NameError + # # _this_ lines and the one + # below we don't want from entry.getsource() + for i in range(self.lineno, end): + if source[i].rstrip().endswith(':'): + end = i + 1 + break + return source[start:end] + source = property(getsource) + + def ishidden(self): + """ return True if the current frame has a var __tracebackhide__ + resolving to True + + mostly for internal use + """ + try: + return self.frame.eval("__tracebackhide__") + except (SystemExit, KeyboardInterrupt): + raise + except: + return False + + 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 + except: + line = "???" + return " File %r:%d in %s\n %s\n" %(fn, self.lineno+1, name, line) + + def name(self): + return self.frame.code.raw.co_name + name = property(name, None, None, "co_name of underlaying code") + +class Traceback(list): + """ Traceback objects encapsulate and offer higher level + access to Traceback entries. + """ + Entry = TracebackEntry + def __init__(self, tb): + """ initialize from given python traceback object. """ + if hasattr(tb, 'tb_next'): + def f(cur): + while cur is not None: + yield self.Entry(cur) + 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 (hasattr(codepath, 'relto') and + not codepath.relto(excludepath))) and + (lineno is None or x.lineno == lineno) and + (firstlineno is None or x.frame.code.firstlineno == firstlineno)): + return Traceback(x._rawentry) + 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 + + fn is a function that gets a single argument, a TracebackItem + instance, and should return True when the item should be added + to the Traceback, False when not + + by default this removes all the TracebackItems which are hidden + (see ishidden() above) + """ + return Traceback(filter(fn, self)) + + def getcrashentry(self): + """ return last non-hidden traceback entry that lead + to the exception of a traceback. + """ + tb = self.filter() + if not tb: + tb = self + return tb[-1] + + def recursionindex(self): + """ return the index of the frame/TracebackItem where recursion + originates if appropriate, None if no recursion occurred + """ + cache = {} + for i, entry in py.builtin.enumerate(self): + key = entry.frame.code.path, entry.lineno + #print "checking for recursion at", key + l = cache.setdefault(key, []) + if l: + f = entry.frame + loc = f.f_locals + for otherloc in l: + if f.is_true(f.eval(co_equal, + __recursioncache_locals_1=loc, + __recursioncache_locals_2=otherloc)): + return i + l.append(entry.frame.f_locals) + return None + +co_equal = compile('__recursioncache_locals_1 == __recursioncache_locals_2', + '?', 'eval') + +class ExceptionInfo(object): + """ wraps sys.exc_info() objects and offers + help for navigating the traceback. + """ + _striptext = '' + def __init__(self, tup=None, exprinfo=None): + # NB. all attributes are private! Subclasses or other + # ExceptionInfo-like classes may have different attributes. + if tup is None: + tup = sys.exc_info() + if exprinfo is None and isinstance(tup[1], py.magic.AssertionError): + exprinfo = tup[1].msg + if exprinfo and exprinfo.startswith('assert '): + self._striptext = 'AssertionError: ' + self._excinfo = tup + self.type, self.value, tb = self._excinfo + self.typename = self.type.__name__ + self.traceback = py.code.Traceback(tb) + + def __repr__(self): + return "" % (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 + py.magic.AssertionError, only the actual exception part of + the exception representation is returned (so 'AssertionError: ' is + removed from the beginning) + """ + lines = py.std.traceback.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.path, entry.lineno + reprcrash = ReprFileLocation(path, lineno+1, exconly) + return reprcrash + + def getrepr(self, showlocals=False, style="long", + abspath=False, tbfilter=True, funcargs=False): + """ return str()able representation of this exception info. + showlocals: show locals per traceback entry + style: long|short|no traceback style + tbfilter: hide entries (where __tracebackhide__ is true) + """ + fmt = FormattedExcinfo(showlocals=showlocals, style=style, + abspath=abspath, tbfilter=tbfilter, funcargs=funcargs) + return fmt.repr_excinfo(self) + + def __str__(self): + entry = self.traceback[-1] + loc = ReprFileLocation(entry.path, entry.lineno + 1, self.exconly()) + return str(loc) + +class FormattedExcinfo(object): + """ presenting information about failing Functions and Generators. """ + # for traceback entries + flow_marker = ">" + fail_marker = "E" + + def __init__(self, showlocals=False, style="long", abspath=True, tbfilter=True, funcargs=False): + self.showlocals = showlocals + self.style = style + self.tbfilter = tbfilter + self.funcargs = funcargs + self.abspath = abspath + + def _getindent(self, source): + # figure out indent for given source + try: + s = str(source.getstatement(len(source)-1)) + except KeyboardInterrupt: + raise + except: + try: + s = str(source[-1]) + except KeyboardInterrupt: + raise + except: + return 0 + return 4 + (len(s) - len(s.lstrip())) + + def _getentrysource(self, entry): + source = entry.getsource() + if source is not None: + source = source.deindent() + return source + + def _saferepr(self, obj): + return safe_repr(obj) + + def repr_args(self, entry): + if self.funcargs: + args = [] + for argname, argvalue in entry.frame.getargs(): + args.append((argname, self._saferepr(argvalue))) + return ReprFuncArgs(args) + + def get_source(self, source, line_index=-1, excinfo=None): + """ return formatted and marked up source lines. """ + lines = [] + if source is None: + source = py.code.Source("???") + line_index = 0 + if line_index < 0: + line_index += len(source) + for i in range(len(source)): + if i == line_index: + prefix = self.flow_marker + " " + else: + prefix = " " + line = prefix + source[i] + lines.append(line) + if excinfo is not None: + indent = 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 = [] + items = locals.items() + items.sort() + for name, value in items: + if name == '__builtins__': + lines.append("__builtins__ = ") + else: + # This formatting could all be handled by the + # _repr() function, which is only repr.Repr in + # disguise, so is very configurable. + str_repr = self._saferepr(value) + #if len(str_repr) < 70 or not isinstance(value, + # (list, tuple, dict)): + lines.append("%-10s = %s" %(name, str_repr)) + #else: + # self._line("%-10s =\\" % (name,)) + # # XXX + # py.std.pprint.pprint(value, stream=self.excinfowriter) + return ReprLocals(lines) + + def repr_traceback_entry(self, entry, excinfo=None): + # excinfo is not None if this is the last tb entry + source = self._getentrysource(entry) + if source is None: + source = py.code.Source("???") + line_index = 0 + else: + line_index = entry.lineno - entry.getfirstlinesource() + + lines = [] + if self.style == "long": + reprargs = self.repr_args(entry) + lines.extend(self.get_source(source, line_index, excinfo)) + message = excinfo and excinfo.typename or "" + path = self._makepath(entry.path) + filelocrepr = ReprFileLocation(path, entry.lineno+1, message) + localsrepr = self.repr_locals(entry.locals) + return ReprEntry(lines, reprargs, localsrepr, filelocrepr) + else: + if self.style == "short": + line = source[line_index].lstrip() + lines.append(' File "%s", line %d, in %s' % ( + entry.path.basename, entry.lineno+1, entry.name)) + lines.append(" " + line) + if excinfo: + lines.extend(self.get_exconly(excinfo, indent=4)) + return ReprEntry(lines, None, None, None) + + def _makepath(self, path): + if not self.abspath: + np = py.path.local().bestrelpath(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() + recursionindex = None + if excinfo.errisinstance(RuntimeError): + recursionindex = traceback.recursionindex() + last = traceback[-1] + entries = [] + extraline = None + for index, entry in py.builtin.enumerate(traceback): + einfo = (last == entry) and excinfo or None + reprentry = self.repr_traceback_entry(entry, einfo) + entries.append(reprentry) + if index == recursionindex: + extraline = "!!! Recursion detected (same locals & position)" + break + return ReprTraceback(entries, extraline, style=self.style) + + def repr_excinfo(self, excinfo): + reprtraceback = self.repr_traceback(excinfo) + reprcrash = excinfo._getreprcrash() + return ReprExceptionInfo(reprtraceback, reprcrash) + +class TerminalRepr: + def __str__(self): + tw = py.io.TerminalWriter(stringio=True) + self.toterminal(tw) + return tw.stringio.getvalue().strip() + + def __repr__(self): + return "<%s instance at %0x>" %(self.__class__, id(self)) + +class ReprExceptionInfo(TerminalRepr): + def __init__(self, reprtraceback, reprcrash): + self.reprtraceback = reprtraceback + self.reprcrash = reprcrash + self.sections = [] + + def addsection(self, name, content, sep="-"): + self.sections.append((name, content, sep)) + + def toterminal(self, tw): + self.reprtraceback.toterminal(tw) + for name, content, sep in self.sections: + tw.sep(sep, name) + tw.line(content) + +class ReprTraceback(TerminalRepr): + entrysep = "_ " + + def __init__(self, reprentries, extraline, style): + self.reprentries = reprentries + self.extraline = extraline + self.style = style + + def toterminal(self, tw): + sepok = False + for entry in self.reprentries: + if self.style == "long": + if sepok: + tw.sep(self.entrysep) + tw.line("") + sepok = True + entry.toterminal(tw) + if self.extraline: + tw.line(self.extraline) + +class ReprEntry(TerminalRepr): + localssep = "_ " + + def __init__(self, lines, reprfuncargs, reprlocals, filelocrepr): + self.lines = lines + self.reprfuncargs = reprfuncargs + self.reprlocals = reprlocals + self.reprfileloc = filelocrepr + + def toterminal(self, tw): + 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: + #tw.sep(self.localssep, "Locals") + tw.line("") + self.reprlocals.toterminal(tw) + if self.reprfileloc: + tw.line("") + self.reprfileloc.toterminal(tw) + + def __str__(self): + return "%s\n%s\n%s" % ("\n".join(self.lines), + self.reprlocals, + self.reprfileloc) + +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] + tw.line("%s:%s: %s" %(self.path, self.lineno, msg)) + +class ReprLocals(TerminalRepr): + def __init__(self, lines): + self.lines = lines + + def toterminal(self, tw): + for line in self.lines: + tw.line(line) + +class ReprFuncArgs(TerminalRepr): + def __init__(self, args): + self.args = args + + def toterminal(self, tw): + if self.args: + linesofar = "" + for name, value in self.args: + ns = "%s = %s" %(name, value) + if len(ns) + len(linesofar) + 2 > tw.fullwidth: + if linesofar: + tw.line(linesofar) + linesofar = ns + else: + if linesofar: + linesofar += ", " + ns + else: + linesofar = ns + if linesofar: + tw.line(linesofar) + tw.line("") + + + +class SafeRepr(repr.Repr): + """ subclass of repr.Repr that limits the resulting size of repr() + and includes information on exceptions raised during the call. + """ + def __init__(self, *args, **kwargs): + repr.Repr.__init__(self, *args, **kwargs) + self.maxstring = 240 # 3 * 80 chars + self.maxother = 160 # 2 * 80 chars + + def repr(self, x): + return self._callhelper(repr.Repr.repr, self, x) + + def repr_instance(self, x, level): + return self._callhelper(builtin_repr, x) + + def _callhelper(self, call, x, *args): + try: + # Try the vanilla repr and make sure that the result is a string + s = call(x, *args) + except (KeyboardInterrupt, MemoryError, SystemExit): + raise + except: + cls, e, tb = sys.exc_info() + try: + exc_name = cls.__name__ + except: + exc_name = 'unknown' + try: + exc_info = str(e) + except: + exc_info = 'unknown' + return '<[%s("%s") raised in repr()] %s object at 0x%x>' % ( + exc_name, exc_info, x.__class__.__name__, id(x)) + else: + if len(s) > self.maxstring: + i = max(0, (self.maxstring-3)//2) + j = max(0, self.maxstring-3-i) + s = s[:i] + '...' + s[len(s)-j:] + return s + +safe_repr = SafeRepr().repr diff --git a/py/code/excinfo.py b/py/code/excinfo.py deleted file mode 100644 index 4245f7fe5..000000000 --- a/py/code/excinfo.py +++ /dev/null @@ -1,345 +0,0 @@ -""" - -Exception Info representation + formatting - -""" -import py -from py.__.code import safe_repr -from sys import exc_info - -class ExceptionInfo(object): - """ wraps sys.exc_info() objects and offers - help for navigating the traceback. - """ - _striptext = '' - def __init__(self, tup=None, exprinfo=None): - # NB. all attributes are private! Subclasses or other - # ExceptionInfo-like classes may have different attributes. - if tup is None: - tup = exc_info() - if exprinfo is None and isinstance(tup[1], py.magic.AssertionError): - exprinfo = tup[1].msg - if exprinfo and exprinfo.startswith('assert '): - self._striptext = 'AssertionError: ' - self._excinfo = tup - self.type, self.value, tb = self._excinfo - self.typename = self.type.__name__ - self.traceback = py.code.Traceback(tb) - - def __repr__(self): - return "" % (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 - py.magic.AssertionError, only the actual exception part of - the exception representation is returned (so 'AssertionError: ' is - removed from the beginning) - """ - lines = py.std.traceback.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.path, entry.lineno - reprcrash = ReprFileLocation(path, lineno+1, exconly) - return reprcrash - - def getrepr(self, showlocals=False, style="long", - abspath=False, tbfilter=True, funcargs=False): - """ return str()able representation of this exception info. - showlocals: show locals per traceback entry - style: long|short|no traceback style - tbfilter: hide entries (where __tracebackhide__ is true) - """ - fmt = FormattedExcinfo(showlocals=showlocals, style=style, - abspath=abspath, tbfilter=tbfilter, funcargs=funcargs) - return fmt.repr_excinfo(self) - - def __str__(self): - entry = self.traceback[-1] - loc = ReprFileLocation(entry.path, entry.lineno + 1, self.exconly()) - return str(loc) - -class FormattedExcinfo(object): - """ presenting information about failing Functions and Generators. """ - # for traceback entries - flow_marker = ">" - fail_marker = "E" - - def __init__(self, showlocals=False, style="long", abspath=True, tbfilter=True, funcargs=False): - self.showlocals = showlocals - self.style = style - self.tbfilter = tbfilter - self.funcargs = funcargs - self.abspath = abspath - - def _getindent(self, source): - # figure out indent for given source - try: - s = str(source.getstatement(len(source)-1)) - except KeyboardInterrupt: - raise - except: - try: - s = str(source[-1]) - except: - return 0 - return 4 + (len(s) - len(s.lstrip())) - - def _getentrysource(self, entry): - source = entry.getsource() - if source is not None: - source = source.deindent() - return source - - def _saferepr(self, obj): - return safe_repr._repr(obj) - - def repr_args(self, entry): - if self.funcargs: - args = [] - for argname, argvalue in entry.frame.getargs(): - args.append((argname, self._saferepr(argvalue))) - return ReprFuncArgs(args) - - def get_source(self, source, line_index=-1, excinfo=None): - """ return formatted and marked up source lines. """ - lines = [] - if source is None: - source = py.code.Source("???") - line_index = 0 - if line_index < 0: - line_index += len(source) - for i in range(len(source)): - if i == line_index: - prefix = self.flow_marker + " " - else: - prefix = " " - line = prefix + source[i] - lines.append(line) - if excinfo is not None: - indent = 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 = [] - items = locals.items() - items.sort() - for name, value in items: - if name == '__builtins__': - lines.append("__builtins__ = ") - else: - # This formatting could all be handled by the - # _repr() function, which is only repr.Repr in - # disguise, so is very configurable. - str_repr = self._saferepr(value) - #if len(str_repr) < 70 or not isinstance(value, - # (list, tuple, dict)): - lines.append("%-10s = %s" %(name, str_repr)) - #else: - # self._line("%-10s =\\" % (name,)) - # # XXX - # py.std.pprint.pprint(value, stream=self.excinfowriter) - return ReprLocals(lines) - - def repr_traceback_entry(self, entry, excinfo=None): - # excinfo is not None if this is the last tb entry - source = self._getentrysource(entry) - if source is None: - source = py.code.Source("???") - line_index = 0 - else: - line_index = entry.lineno - entry.getfirstlinesource() - - lines = [] - if self.style == "long": - reprargs = self.repr_args(entry) - lines.extend(self.get_source(source, line_index, excinfo)) - message = excinfo and excinfo.typename or "" - path = self._makepath(entry.path) - filelocrepr = ReprFileLocation(path, entry.lineno+1, message) - localsrepr = self.repr_locals(entry.locals) - return ReprEntry(lines, reprargs, localsrepr, filelocrepr) - else: - if self.style == "short": - line = source[line_index].lstrip() - lines.append(' File "%s", line %d, in %s' % ( - entry.path.basename, entry.lineno+1, entry.name)) - lines.append(" " + line) - if excinfo: - lines.extend(self.get_exconly(excinfo, indent=4)) - return ReprEntry(lines, None, None, None) - - def _makepath(self, path): - if not self.abspath: - np = py.path.local().bestrelpath(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() - recursionindex = None - if excinfo.errisinstance(RuntimeError): - recursionindex = traceback.recursionindex() - last = traceback[-1] - entries = [] - extraline = None - for index, entry in py.builtin.enumerate(traceback): - einfo = (last == entry) and excinfo or None - reprentry = self.repr_traceback_entry(entry, einfo) - entries.append(reprentry) - if index == recursionindex: - extraline = "!!! Recursion detected (same locals & position)" - break - return ReprTraceback(entries, extraline, style=self.style) - - def repr_excinfo(self, excinfo): - reprtraceback = self.repr_traceback(excinfo) - reprcrash = excinfo._getreprcrash() - return ReprExceptionInfo(reprtraceback, reprcrash) - -class Repr: - def __str__(self): - tw = py.io.TerminalWriter(stringio=True) - self.toterminal(tw) - return tw.stringio.getvalue().strip() - - def __repr__(self): - return "<%s instance at %0x>" %(self.__class__, id(self)) - -class ReprExceptionInfo(Repr): - def __init__(self, reprtraceback, reprcrash): - self.reprtraceback = reprtraceback - self.reprcrash = reprcrash - self.sections = [] - - def addsection(self, name, content, sep="-"): - self.sections.append((name, content, sep)) - - def toterminal(self, tw): - self.reprtraceback.toterminal(tw) - for name, content, sep in self.sections: - tw.sep(sep, name) - tw.line(content) - -class ReprTraceback(Repr): - entrysep = "_ " - - def __init__(self, reprentries, extraline, style): - self.reprentries = reprentries - self.extraline = extraline - self.style = style - - def toterminal(self, tw): - sepok = False - for entry in self.reprentries: - if self.style == "long": - if sepok: - tw.sep(self.entrysep) - tw.line("") - sepok = True - entry.toterminal(tw) - if self.extraline: - tw.line(self.extraline) - -class ReprEntry(Repr): - localssep = "_ " - - def __init__(self, lines, reprfuncargs, reprlocals, filelocrepr): - self.lines = lines - self.reprfuncargs = reprfuncargs - self.reprlocals = reprlocals - self.reprfileloc = filelocrepr - - def toterminal(self, tw): - 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: - #tw.sep(self.localssep, "Locals") - tw.line("") - self.reprlocals.toterminal(tw) - if self.reprfileloc: - tw.line("") - self.reprfileloc.toterminal(tw) - - def __str__(self): - return "%s\n%s\n%s" % ("\n".join(self.lines), - self.reprlocals, - self.reprfileloc) - -class ReprFileLocation(Repr): - 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] - tw.line("%s:%s: %s" %(self.path, self.lineno, msg)) - -class ReprLocals(Repr): - def __init__(self, lines): - self.lines = lines - - def toterminal(self, tw): - for line in self.lines: - tw.line(line) - -class ReprFuncArgs(Repr): - def __init__(self, args): - self.args = args - - def toterminal(self, tw): - if self.args: - linesofar = "" - for name, value in self.args: - ns = "%s = %s" %(name, value) - if len(ns) + len(linesofar) + 2 > tw.fullwidth: - if linesofar: - tw.line(linesofar) - linesofar = ns - else: - if linesofar: - linesofar += ", " + ns - else: - linesofar = ns - if linesofar: - tw.line(linesofar) - tw.line("") diff --git a/py/code/frame.py b/py/code/frame.py deleted file mode 100644 index e56f83b71..000000000 --- a/py/code/frame.py +++ /dev/null @@ -1,60 +0,0 @@ -import py -import py.__.code.safe_repr - -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.code = py.code.Code(frame.f_code) - self.lineno = frame.f_lineno - 1 - self.f_globals = frame.f_globals - self.f_locals = frame.f_locals - self.raw = frame - - def statement(self): - if self.code.fullsource is None: - return py.code.Source("") - return self.code.fullsource.getstatement(self.lineno) - statement = property(statement, None, None, - "statement this frame is at") - - 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) - exec code in self.f_globals, f_locals - - def repr(self, object): - """ return a 'safe' (non-recursive, one-line) string repr for 'object' - """ - return py.__.code.safe_repr._repr(object) - - def is_true(self, object): - return object - - def getargs(self): - """ return a list of tuples (name, value) for all arguments - """ - retval = [] - for arg in self.code.getargs(): - try: - retval.append((arg, self.f_locals[arg])) - except KeyError: - pass # this can occur when using Psyco - return retval - diff --git a/py/code/safe_repr.py b/py/code/safe_repr.py deleted file mode 100644 index cf5e3098b..000000000 --- a/py/code/safe_repr.py +++ /dev/null @@ -1,71 +0,0 @@ -"""Defines a safe repr function. This will always return a string of "reasonable" length -no matter what the object does in it's own repr function. Let's examine what can go wrong -in an arbitrary repr function. -The default repr will return something like (on Win32 anyway): -. Well behaved user-defined repr() methods will do similar. -The usual expectation is that repr will return a single line string. - -1. However, the repr method can raise an exception of an arbitrary type. - -Also, the return value may not be as expected: - 2. The return value may not be a string! - 3. The return value may not be a single line string, it may contain line breaks. - 4. The method may enter a loop and never return. - 5. The return value may be enormous, eg range(100000) - - The standard library has a nice implementation in the repr module that will do the job, - but the exception - handling is silent, so the the output contains no clue that repr() call raised an - exception. I would like to be told if repr raises an exception, it's a serious error, so - a sublass of repr overrides the method that does repr for class instances.""" - - -import repr -import __builtin__ - - -class SafeRepr(repr.Repr): - def __init__(self, *args, **kwargs): - repr.Repr.__init__(self, *args, **kwargs) - # Do we need a commandline switch for this? - self.maxstring = 240 # 3 * 80 chars - self.maxother = 160 # 2 * 80 chars - - def repr(self, x): - return self._callhelper(repr.Repr.repr, self, x) - - def repr_instance(self, x, level): - return self._callhelper(__builtin__.repr, x) - - def _callhelper(self, call, x, *args): - try: - # Try the vanilla repr and make sure that the result is a string - s = call(x, *args) - except (KeyboardInterrupt, MemoryError, SystemExit): - raise - except Exception ,e: - try: - exc_name = e.__class__.__name__ - except: - exc_name = 'unknown' - try: - exc_info = str(e) - except: - exc_info = 'unknown' - return '<[%s("%s") raised in repr()] %s object at 0x%x>' % \ - (exc_name, exc_info, x.__class__.__name__, id(x)) - except: - try: - name = x.__class__.__name__ - except: - name = 'unknown' - return '<[unknown exception raised in repr()] %s object at 0x%x>' % \ - (name, id(x)) - if len(s) > self.maxstring: - i = max(0, (self.maxstring-3)//2) - j = max(0, self.maxstring-3-i) - s = s[:i] + '...' + s[len(s)-j:] - return s - - -_repr = SafeRepr().repr diff --git a/py/code/source.py b/py/code/source.py index 515464a05..90527f78d 100644 --- a/py/code/source.py +++ b/py/code/source.py @@ -181,7 +181,8 @@ class Source(object): source = "\n".join(self.lines) + '\n' try: co = cpy_compile(source, filename, mode, flag) - except SyntaxError, ex: + except SyntaxError: + ex = sys.exc_info()[1] # re-represent syntax errors from parsing python strings msglines = self.lines[:ex.lineno] if ex.offset: diff --git a/py/code/testing/test_code.py b/py/code/testing/test_code.py index cc99d67cc..53e32ddfd 100644 --- a/py/code/testing/test_code.py +++ b/py/code/testing/test_code.py @@ -1,6 +1,7 @@ from __future__ import generators import py -import new +import sys, new +from py.__.code.code import safe_repr def test_newcode(): source = "i = 3" @@ -99,3 +100,69 @@ def test_code_source(): expected = """def x(): pass""" assert str(src) == expected + +def test_frame_getsourcelineno_myself(): + def func(): + return sys._getframe(0) + f = func() + f = py.code.Frame(f) + source, lineno = f.code.fullsource, f.lineno + assert source[lineno].startswith(" return sys._getframe(0)") + +def test_getstatement_empty_fullsource(): + def func(): + return sys._getframe(0) + f = func() + f = py.code.Frame(f) + prop = f.code.__class__.fullsource + try: + f.code.__class__.fullsource = None + assert f.statement == py.code.Source("") + finally: + f.code.__class__.fullsource = prop + +def test_code_from_func(): + co = py.code.Code(test_frame_getsourcelineno_myself) + assert co.firstlineno + assert co.path + + + +class TestSafeRepr: + def test_simple_repr(self): + assert safe_repr(1) == '1' + assert safe_repr(None) == 'None' + + def test_exceptions(self): + class BrokenRepr: + def __init__(self, ex): + self.ex = ex + foo = 0 + def __repr__(self): + raise self.ex + class BrokenReprException(Exception): + __str__ = None + __repr__ = None + assert 'Exception' in safe_repr(BrokenRepr(Exception("broken"))) + s = safe_repr(BrokenReprException("really broken")) + assert 'TypeError' in s + if py.std.sys.version_info < (2,6): + assert 'unknown' in safe_repr(BrokenRepr("string")) + else: + assert 'TypeError' in safe_repr(BrokenRepr("string")) + + def test_big_repr(self): + from py.__.code.code import SafeRepr + assert len(safe_repr(range(1000))) <= \ + len('[' + SafeRepr().maxlist * "1000" + ']') + + def test_repr_on_newstyle(self): + class Function(object): + def __repr__(self): + return "<%s>" %(self.name) + try: + s = safe_repr(Function()) + except Exception, e: + py.test.fail("saferepr failed for newstyle class") + + diff --git a/py/code/testing/test_excinfo.py b/py/code/testing/test_excinfo.py index 4d831c580..237607ed3 100644 --- a/py/code/testing/test_excinfo.py +++ b/py/code/testing/test_excinfo.py @@ -1,5 +1,5 @@ import py -from py.__.code.excinfo import FormattedExcinfo, ReprExceptionInfo +from py.__.code.code import FormattedExcinfo, ReprExceptionInfo class TWMock: def __init__(self): diff --git a/py/code/testing/test_frame.py b/py/code/testing/test_frame.py index e063a5a27..1307de543 100644 --- a/py/code/testing/test_frame.py +++ b/py/code/testing/test_frame.py @@ -1,27 +1,2 @@ import sys import py - -def test_frame_getsourcelineno_myself(): - def func(): - return sys._getframe(0) - f = func() - f = py.code.Frame(f) - source, lineno = f.code.fullsource, f.lineno - assert source[lineno].startswith(" return sys._getframe(0)") - -def test_getstatement_empty_fullsource(): - def func(): - return sys._getframe(0) - f = func() - f = py.code.Frame(f) - prop = f.code.__class__.fullsource - try: - f.code.__class__.fullsource = None - assert f.statement == py.code.Source("") - finally: - f.code.__class__.fullsource = prop - -def test_code_from_func(): - co = py.code.Code(test_frame_getsourcelineno_myself) - assert co.firstlineno - assert co.path diff --git a/py/code/testing/test_safe_repr.py b/py/code/testing/test_safe_repr.py deleted file mode 100644 index 955184601..000000000 --- a/py/code/testing/test_safe_repr.py +++ /dev/null @@ -1,48 +0,0 @@ - -import py -from py.__.code import safe_repr - -def test_simple_repr(): - assert safe_repr._repr(1) == '1' - assert safe_repr._repr(None) == 'None' - -class BrokenRepr: - def __init__(self, ex): - self.ex = ex - foo = 0 - def __repr__(self): - raise self.ex - -def test_exception(): - assert 'Exception' in safe_repr._repr(BrokenRepr(Exception("broken"))) - -class BrokenReprException(Exception): - __str__ = None - __repr__ = None - -def test_broken_exception(): - assert 'Exception' in safe_repr._repr(BrokenRepr(BrokenReprException("really broken"))) - -def test_string_exception(): - if py.std.sys.version_info < (2,6): - assert 'unknown' in safe_repr._repr(BrokenRepr("string")) - else: - assert 'TypeError' in safe_repr._repr(BrokenRepr("string")) - - - -def test_big_repr(): - assert len(safe_repr._repr(range(1000))) <= \ - len('[' + safe_repr.SafeRepr().maxlist * "1000" + ']') - -def test_repr_on_newstyle(): - class Function(object): - def __repr__(self): - return "<%s>" %(self.name) - try: - s = safe_repr._repr(Function()) - except Exception, e: - py.test.fail("saferepr failed for newstyle class") - - - diff --git a/py/code/traceback2.py b/py/code/traceback2.py deleted file mode 100644 index 0d715a4ed..000000000 --- a/py/code/traceback2.py +++ /dev/null @@ -1,199 +0,0 @@ -from __future__ import generators -import py -import sys - -class TracebackEntry(object): - """ a single entry in a traceback """ - - exprinfo = None - - def __init__(self, rawentry): - self._rawentry = rawentry - self.frame = py.code.Frame(rawentry.tb_frame) - # Ugh. 2.4 and 2.5 differs here when encountering - # multi-line statements. Not sure about the solution, but - # should be portable - self.lineno = rawentry.tb_lineno - 1 - self.relline = self.lineno - self.frame.code.firstlineno - - def __repr__(self): - return "" %(self.frame.code.path, self.lineno+1) - - def statement(self): - """ return a py.code.Source object for the current statement """ - source = self.frame.code.fullsource - return source.getstatement(self.lineno) - statement = property(statement, None, None, - "statement of this traceback entry.") - - def path(self): - return self.frame.code.path - path = property(path, None, None, "path to the full source code") - - def getlocals(self): - return self.frame.f_locals - locals = property(getlocals, None, None, "locals of underlaying frame") - - def reinterpret(self): - """Reinterpret the failing statement and returns a detailed information - about what operations are performed.""" - if self.exprinfo is None: - from py.__.magic import exprinfo - source = str(self.statement).strip() - x = exprinfo.interpret(source, self.frame, should_fail=True) - if not isinstance(x, str): - raise TypeError, "interpret returned non-string %r" % (x,) - self.exprinfo = x - return self.exprinfo - - def getfirstlinesource(self): - return self.frame.code.firstlineno - - def getsource(self): - """ return failing source code. """ - source = self.frame.code.fullsource - if source is None: - return None - start = self.getfirstlinesource() - end = self.lineno - try: - _, end = source.getstatementrange(end) - except IndexError: - end = self.lineno + 1 - # heuristic to stop displaying source on e.g. - # if something: # assume this causes a NameError - # # _this_ lines and the one - # below we don't want from entry.getsource() - for i in range(self.lineno, end): - if source[i].rstrip().endswith(':'): - end = i + 1 - break - return source[start:end] - source = property(getsource) - - def ishidden(self): - """ return True if the current frame has a var __tracebackhide__ - resolving to True - - mostly for internal use - """ - try: - return self.frame.eval("__tracebackhide__") - except (SystemExit, KeyboardInterrupt): - raise - except: - return False - - 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 - except: - line = "???" - return " File %r:%d in %s\n %s\n" %(fn, self.lineno+1, name, line) - - def name(self): - return self.frame.code.raw.co_name - name = property(name, None, None, "co_name of underlaying code") - -class Traceback(list): - """ Traceback objects encapsulate and offer higher level - access to Traceback entries. - """ - Entry = TracebackEntry - def __init__(self, tb): - """ initialize from given python traceback object. """ - if hasattr(tb, 'tb_next'): - def f(cur): - while cur is not None: - yield self.Entry(cur) - 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 (hasattr(codepath, 'relto') and - not codepath.relto(excludepath))) and - (lineno is None or x.lineno == lineno) and - (firstlineno is None or x.frame.code.firstlineno == firstlineno)): - return Traceback(x._rawentry) - 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 - - fn is a function that gets a single argument, a TracebackItem - instance, and should return True when the item should be added - to the Traceback, False when not - - by default this removes all the TracebackItems which are hidden - (see ishidden() above) - """ - return Traceback(filter(fn, self)) - - def getcrashentry(self): - """ return last non-hidden traceback entry that lead - to the exception of a traceback. - """ - tb = self.filter() - if not tb: - tb = self - return tb[-1] - - def recursionindex(self): - """ return the index of the frame/TracebackItem where recursion - originates if appropriate, None if no recursion occurred - """ - cache = {} - for i, entry in py.builtin.enumerate(self): - key = entry.frame.code.path, entry.lineno - #print "checking for recursion at", key - l = cache.setdefault(key, []) - if l: - f = entry.frame - loc = f.f_locals - for otherloc in l: - if f.is_true(f.eval(co_equal, - __recursioncache_locals_1=loc, - __recursioncache_locals_2=otherloc)): - return i - l.append(entry.frame.f_locals) - return None - -# def __str__(self): -# for x in self -# l = [] -## for func, entry in self._tblist: -# l.append(entry.display()) -# return "".join(l) - - -co_equal = compile('__recursioncache_locals_1 == __recursioncache_locals_2', - '?', 'eval') - diff --git a/py/test/plugin/pytest_doctest.py b/py/test/plugin/pytest_doctest.py index e973bf7a5..bcfe1baee 100644 --- a/py/test/plugin/pytest_doctest.py +++ b/py/test/plugin/pytest_doctest.py @@ -14,7 +14,7 @@ as well. """ import py -from py.__.code.excinfo import Repr, ReprFileLocation +from py.__.code.code import TerminalRepr, ReprFileLocation def pytest_addoption(parser): group = parser.addgroup("doctest options") @@ -30,7 +30,7 @@ def pytest_collect_file(path, parent): if path.check(fnmatch="test_*.txt"): return DoctestTextfile(path, parent) -class ReprFailDoctest(Repr): +class ReprFailDoctest(TerminalRepr): def __init__(self, reprlocation, lines): self.reprlocation = reprlocation self.lines = lines diff --git a/py/test/plugin/test_pytest_runner.py b/py/test/plugin/test_pytest_runner.py index e039d7e19..84501a8e7 100644 --- a/py/test/plugin/test_pytest_runner.py +++ b/py/test/plugin/test_pytest_runner.py @@ -1,6 +1,6 @@ import py from py.__.test.plugin import pytest_runner as runner -from py.__.code.excinfo import ReprExceptionInfo +from py.__.code.code import ReprExceptionInfo class TestSetupState: def test_setup(self, testdir):