diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 4ec058121..e1a8b1588 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -19,6 +19,21 @@ **Changes** +* **Important**: `py.code `_ has been + merged into the ``pytest`` repository as ``pytest._code``. This decision + was made because ``py.code`` had very few uses outside ``pytest`` and the + fact that it was in a different repository made it difficult to fix bugs on + its code in a timely manner. The team hopes with this to be able to better + refactor out and improve that code. + This change shouldn't affect users, but it is useful to let users aware + if they encounter any strange behavior. + + Keep in mind that the code for ``pytest._code`` is **private** and + **experimental**, so you definitely should not import it explicitly! + + Please note that the original ``py.code`` is still available in + `pylib `_. + * ``pytest_enter_pdb`` now optionally receives the pytest config object. Thanks `@nicoddemus`_ for the PR. diff --git a/_pytest/_code/__init__.py b/_pytest/_code/__init__.py new file mode 100644 index 000000000..c046b9716 --- /dev/null +++ b/_pytest/_code/__init__.py @@ -0,0 +1,12 @@ +""" python inspection/code generation API """ +from .code import Code # noqa +from .code import ExceptionInfo # noqa +from .code import Frame # noqa +from .code import Traceback # noqa +from .code import getrawcode # noqa +from .code import patch_builtins # noqa +from .code import unpatch_builtins # noqa +from .source import Source # noqa +from .source import compile_ as compile # noqa +from .source import getfslineno # noqa + diff --git a/_pytest/_code/_py2traceback.py b/_pytest/_code/_py2traceback.py new file mode 100644 index 000000000..d65e27cb7 --- /dev/null +++ b/_pytest/_code/_py2traceback.py @@ -0,0 +1,79 @@ +# copied from python-2.7.3's traceback.py +# CHANGES: +# - some_str is replaced, trying to create unicode strings +# +import types + +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. + + """ + + # An instance should not have a meaningful value parameter, but + # sometimes does, particularly for string exceptions, such as + # >>> raise string1, string2 # deprecated + # + # Clear these out first because issubtype(string1, SyntaxError) + # would throw another exception and mask the original problem. + if (isinstance(etype, BaseException) or + isinstance(etype, types.InstanceType) or + etype is None or type(etype) is str): + return [_format_final_exc_line(etype, value)] + + stype = etype.__name__ + + if not issubclass(etype, SyntaxError): + return [_format_final_exc_line(stype, value)] + + # It was a syntax error; show exactly where the problem was found. + lines = [] + try: + msg, (filename, lineno, offset, badline) = value.args + except Exception: + pass + else: + filename = filename or "" + lines.append(' File "%s", line %d\n' % (filename, lineno)) + if badline is not None: + lines.append(' %s\n' % badline.strip()) + if offset is not None: + caretspace = badline.rstrip('\n')[:offset].lstrip() + # non-space whitespace (likes tabs) must be kept for alignment + caretspace = ((c.isspace() and c or ' ') for c in caretspace) + # only three spaces to account for offset1 == pos 0 + lines.append(' %s^\n' % ''.join(caretspace)) + value = msg + + lines.append(_format_final_exc_line(stype, value)) + return lines + +def _format_final_exc_line(etype, value): + """Return a list of a single line -- normal case for format_exception_only""" + valuestr = _some_str(value) + if value is None or not valuestr: + line = "%s\n" % etype + else: + line = "%s: %s\n" % (etype, valuestr) + return line + +def _some_str(value): + try: + return unicode(value) + except Exception: + try: + return str(value) + except Exception: + pass + return '' % type(value).__name__ diff --git a/_pytest/_code/code.py b/_pytest/_code/code.py new file mode 100644 index 000000000..bc68aac55 --- /dev/null +++ b/_pytest/_code/code.py @@ -0,0 +1,795 @@ +import sys +from inspect import CO_VARARGS, CO_VARKEYWORDS + +import py + +builtin_repr = repr + +reprlib = py.builtin._tryimport('repr', 'reprlib') + +if sys.version_info[0] >= 3: + from traceback import format_exception_only +else: + from ._py2traceback import format_exception_only + +class Code(object): + """ wrapper around Python code objects """ + 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: + raise TypeError("not a code object: %r" %(rawcode,)) + self.raw = rawcode + + def __eq__(self, other): + return self.raw == other.raw + + 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). """ + p = py.path.local(self.raw.co_filename) + # maybe don't try this checking + if not p.check(): + # XXX maybe try harder like the weird logic + # in the standard lib [linecache.updatecache] does? + p = self.raw.co_filename + 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] + +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) + py.builtin.exec_(code, self.f_globals, f_locals ) + + 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 + +class TracebackEntry(object): + """ a single entry in a traceback """ + + _repr_style = None + exprinfo = None + + def __init__(self, rawentry): + 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): + return "" %(self.frame.code.path, self.lineno+1) + + @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 reinterpret(self): + """Reinterpret the failing statement and returns a detailed information + about what operations are performed.""" + from _pytest.assertion.reinterpret import reinterpret + if self.exprinfo is None: + source = py.builtin._totext(self.statement).strip() + x = reinterpret(source, self.frame, should_fail=True) + if not py.builtin._istext(x): + raise TypeError("interpret returned non-string %r" % (x,)) + self.exprinfo = x + return self.exprinfo + + 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 + + mostly for internal use + """ + try: + return self.frame.f_locals['__tracebackhide__'] + except KeyError: + try: + return self.frame.f_globals['__tracebackhide__'] + except KeyError: + 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 not hasattr(codepath, 'relto') or + 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. + """ + for i in range(-1, -len(self)-1, -1): + entry = self[i] + if not entry.ishidden(): + return entry + return self[-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 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 + #XXX needs a test + key = entry.frame.code.path, id(entry.frame.code.raw), 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): + 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: + exprinfo = str(tup[1]) + if exprinfo and exprinfo.startswith('assert '): + 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) + self.traceback = _pytest._code.Traceback(self.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 + _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 + return ReprFileLocation(path, lineno+1, exconly) + + 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|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( + py.std.traceback.format_exception( + self.type, + self.value, + self.traceback[0]._rawentry, + )), self._getreprcrash()) + + 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) + + def __unicode__(self): + entry = self.traceback[-1] + loc = ReprFileLocation(entry.path, entry.lineno + 1, self.exconly()) + return unicode(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 + self.astcache = {} + + 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(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]) + for line in source.lines[line_index+1:]: + 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__ = ") + 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) + #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): + 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: + message = "in %s" %(entry.name) + else: + message = excinfo and excinfo.typename or "" + path = self._makepath(entry.path) + filelocrepr = ReprFileLocation(path, entry.lineno+1, message) + localsrepr = None + if not short: + localsrepr = self.repr_locals(entry.locals) + 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() + recursionindex = None + if excinfo.errisinstance(RuntimeError): + if "maximum recursion depth exceeded" in str(excinfo.value): + recursionindex = traceback.recursionindex() + last = traceback[-1] + entries = [] + extraline = None + for index, entry in 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): + s = self.__unicode__() + if sys.version_info[0] < 3: + 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): + 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): + # 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: + next_entry = self.reprentries[i+1] + if entry.style == "long" or \ + entry.style == "short" and next_entry.style == "long": + tw.sep(self.entrysep) + + if self.extraline: + tw.line(self.extraline) + +class ReprTracebackNative(ReprTraceback): + def __init__(self, tblines): + self.style = "native" + self.reprentries = [ReprEntryNative(tblines)] + self.extraline = None + +class ReprEntryNative(TerminalRepr): + style = "native" + + def __init__(self, tblines): + self.lines = tblines + + def toterminal(self, tw): + tw.write("".join(self.lines)) + +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) + #tw.line("") + 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: + #tw.sep(self.localssep, "Locals") + 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) + +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("") + + + +oldbuiltins = {} + +def patch_builtins(assertion=True, compile=True): + """ put compile and AssertionError builtins to Python's builtins. """ + if assertion: + from _pytest.assertion import reinterpret + l = oldbuiltins.setdefault('AssertionError', []) + l.append(py.builtin.builtins.AssertionError) + py.builtin.builtins.AssertionError = reinterpret.AssertionError + if compile: + import _pytest._code + l = oldbuiltins.setdefault('compile', []) + l.append(py.builtin.builtins.compile) + py.builtin.builtins.compile = _pytest._code.compile + +def unpatch_builtins(assertion=True, compile=True): + """ remove compile and AssertionError builtins from Python builtins. """ + if assertion: + py.builtin.builtins.AssertionError = oldbuiltins['AssertionError'].pop() + if compile: + py.builtin.builtins.compile = oldbuiltins['compile'].pop() + +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'): + if hasattr(obj, '__call__') and not py.std.inspect.isclass(obj): + x = getrawcode(obj.__call__, trycall=False) + if hasattr(x, 'co_firstlineno'): + return x + return obj + diff --git a/_pytest/_code/source.py b/_pytest/_code/source.py new file mode 100644 index 000000000..a1521f8a2 --- /dev/null +++ b/_pytest/_code/source.py @@ -0,0 +1,421 @@ +from __future__ import generators + +from bisect import bisect_right +import sys +import inspect, tokenize +import py +from types import ModuleType +cpy_compile = compile + +try: + import _ast + from _ast import PyCF_ONLY_AST as _AST_FLAG +except ImportError: + _AST_FLAG = 0 + _ast = None + + +class Source(object): + """ a immutable object holding a source code fragment, + possibly deindenting it. + """ + _compilecounter = 0 + def __init__(self, *parts, **kwargs): + self.lines = lines = [] + de = kwargs.get('deindent', True) + rstrip = kwargs.get('rstrip', True) + for part in parts: + if not part: + partlines = [] + if isinstance(part, Source): + partlines = part.lines + elif isinstance(part, (tuple, list)): + partlines = [x.rstrip("\n") for x in part] + elif isinstance(part, py.builtin._basestring): + partlines = part.split('\n') + if rstrip: + while partlines: + if partlines[-1].strip(): + break + partlines.pop() + else: + partlines = getsource(part, deindent=de).lines + if de: + partlines = deindent(partlines) + lines.extend(partlines) + + def __eq__(self, other): + try: + return self.lines == other.lines + except AttributeError: + if isinstance(other, str): + return str(self) == other + return False + + def __getitem__(self, key): + if isinstance(key, int): + return self.lines[key] + else: + if key.step not in (None, 1): + raise IndexError("cannot slice a Source with a step") + return self.__getslice__(key.start, key.stop) + + def __len__(self): + return len(self.lines) + + def __getslice__(self, start, end): + newsource = Source() + newsource.lines = self.lines[start:end] + return newsource + + def strip(self): + """ return new source object with trailing + and leading blank lines removed. + """ + start, end = 0, len(self) + while start < end and not self.lines[start].strip(): + start += 1 + while end > start and not self.lines[end-1].strip(): + end -= 1 + source = Source() + source.lines[:] = self.lines[start:end] + return source + + def putaround(self, before='', after='', indent=' ' * 4): + """ return a copy of the source object with + 'before' and 'after' wrapped around it. + """ + before = Source(before) + after = Source(after) + newsource = Source() + lines = [ (indent + line) for line in self.lines] + newsource.lines = before.lines + lines + after.lines + return newsource + + def indent(self, indent=' ' * 4): + """ return a copy of the source object with + all lines indented by the given indent-string. + """ + newsource = Source() + newsource.lines = [(indent+line) for line in self.lines] + return newsource + + def getstatement(self, lineno, assertion=False): + """ return Source statement which contains the + given linenumber (counted from 0). + """ + start, end = self.getstatementrange(lineno, assertion) + return self[start:end] + + def getstatementrange(self, lineno, assertion=False): + """ return (start, end) tuple which spans the minimal + statement region which containing the given lineno. + """ + if not (0 <= lineno < len(self)): + raise IndexError("lineno out of range") + ast, start, end = getstatementrange_ast(lineno, self) + return start, end + + def deindent(self, offset=None): + """ return a new source object deindented by offset. + If offset is None then guess an indentation offset from + the first non-blank line. Subsequent lines which have a + lower indentation offset will be copied verbatim as + they are assumed to be part of multilines. + """ + # XXX maybe use the tokenizer to properly handle multiline + # strings etc.pp? + newsource = Source() + newsource.lines[:] = deindent(self.lines, offset) + return newsource + + def isparseable(self, deindent=True): + """ return True if source is parseable, heuristically + deindenting it by default. + """ + try: + import parser + except ImportError: + syntax_checker = lambda x: compile(x, 'asd', 'exec') + else: + syntax_checker = parser.suite + + if deindent: + source = str(self.deindent()) + else: + source = str(self) + try: + #compile(source+'\n', "x", "exec") + syntax_checker(source+'\n') + except KeyboardInterrupt: + raise + except Exception: + return False + else: + return True + + def __str__(self): + return "\n".join(self.lines) + + def compile(self, filename=None, mode='exec', + flag=generators.compiler_flag, + dont_inherit=0, _genframe=None): + """ return compiled code object. if filename is None + invent an artificial filename which displays + the source/line position of the caller frame. + """ + if not filename or py.path.local(filename).check(file=0): + if _genframe is None: + _genframe = sys._getframe(1) # the caller + fn,lineno = _genframe.f_code.co_filename, _genframe.f_lineno + base = "<%d-codegen " % self._compilecounter + self.__class__._compilecounter += 1 + if not filename: + filename = base + '%s:%d>' % (fn, lineno) + else: + filename = base + '%r %s:%d>' % (filename, fn, lineno) + source = "\n".join(self.lines) + '\n' + try: + co = cpy_compile(source, filename, mode, flag) + except SyntaxError: + ex = sys.exc_info()[1] + # re-represent syntax errors from parsing python strings + msglines = self.lines[:ex.lineno] + if ex.offset: + msglines.append(" "*ex.offset + '^') + msglines.append("(code was compiled probably from here: %s)" % filename) + newex = SyntaxError('\n'.join(msglines)) + newex.offset = ex.offset + newex.lineno = ex.lineno + newex.text = ex.text + raise newex + else: + if flag & _AST_FLAG: + return co + lines = [(x + "\n") for x in self.lines] + if sys.version_info[0] >= 3: + # XXX py3's inspect.getsourcefile() checks for a module + # and a pep302 __loader__ ... we don't have a module + # at code compile-time so we need to fake it here + m = ModuleType("_pycodecompile_pseudo_module") + py.std.inspect.modulesbyfile[filename] = None + py.std.sys.modules[None] = m + m.__loader__ = 1 + py.std.linecache.cache[filename] = (1, None, lines, filename) + return co + +# +# public API shortcut functions +# + +def compile_(source, filename=None, mode='exec', flags= + generators.compiler_flag, dont_inherit=0): + """ compile the given source to a raw code object, + and maintain an internal cache which allows later + retrieval of the source code for the code object + and any recursively created code objects. + """ + if _ast is not None and isinstance(source, _ast.AST): + # XXX should Source support having AST? + return cpy_compile(source, filename, mode, flags, dont_inherit) + _genframe = sys._getframe(1) # the caller + s = Source(source) + co = s.compile(filename, mode, flags, _genframe=_genframe) + return co + + +def getfslineno(obj): + """ Return source location (path, lineno) for the given object. + If the source cannot be determined return ("", -1) + """ + import _pytest._code + try: + code = _pytest._code.Code(obj) + except TypeError: + try: + fn = (py.std.inspect.getsourcefile(obj) or + py.std.inspect.getfile(obj)) + except TypeError: + return "", -1 + + fspath = fn and py.path.local(fn) or None + lineno = -1 + if fspath: + try: + _, lineno = findsource(obj) + except IOError: + pass + else: + fspath = code.path + lineno = code.firstlineno + assert isinstance(lineno, int) + return fspath, lineno + +# +# helper functions +# + +def findsource(obj): + try: + sourcelines, lineno = py.std.inspect.findsource(obj) + except py.builtin._sysex: + raise + except: + return None, -1 + source = Source() + source.lines = [line.rstrip() for line in sourcelines] + return source, lineno + +def getsource(obj, **kwargs): + import _pytest._code + obj = _pytest._code.getrawcode(obj) + try: + strsrc = inspect.getsource(obj) + except IndentationError: + strsrc = "\"Buggy python version consider upgrading, cannot get source\"" + assert isinstance(strsrc, str) + return Source(strsrc, **kwargs) + +def deindent(lines, offset=None): + if offset is None: + for line in lines: + line = line.expandtabs() + s = line.lstrip() + if s: + offset = len(line)-len(s) + break + else: + offset = 0 + if offset == 0: + return list(lines) + newlines = [] + def readline_generator(lines): + for line in lines: + yield line + '\n' + while True: + yield '' + + it = readline_generator(lines) + + try: + for _, _, (sline, _), (eline, _), _ in tokenize.generate_tokens(lambda: next(it)): + if sline > len(lines): + break # End of input reached + if sline > len(newlines): + line = lines[sline - 1].expandtabs() + if line.lstrip() and line[:offset].isspace(): + line = line[offset:] # Deindent + newlines.append(line) + + for i in range(sline, eline): + # Don't deindent continuing lines of + # multiline tokens (i.e. multiline strings) + newlines.append(lines[i]) + except (IndentationError, tokenize.TokenError): + pass + # Add any lines we didn't see. E.g. if an exception was raised. + newlines.extend(lines[len(newlines):]) + return newlines + + +def get_statement_startend2(lineno, node): + import ast + # flatten all statements and except handlers into one lineno-list + # AST's line numbers start indexing at 1 + l = [] + for x in ast.walk(node): + if isinstance(x, _ast.stmt) or isinstance(x, _ast.ExceptHandler): + l.append(x.lineno - 1) + for name in "finalbody", "orelse": + val = getattr(x, name, None) + if val: + # treat the finally/orelse part as its own statement + l.append(val[0].lineno - 1 - 1) + l.sort() + insert_index = bisect_right(l, lineno) + start = l[insert_index - 1] + if insert_index >= len(l): + end = None + else: + end = l[insert_index] + return start, end + + +def getstatementrange_ast(lineno, source, assertion=False, astnode=None): + if astnode is None: + content = str(source) + if sys.version_info < (2,7): + content += "\n" + try: + astnode = compile(content, "source", "exec", 1024) # 1024 for AST + except ValueError: + start, end = getstatementrange_old(lineno, source, assertion) + return None, start, end + start, end = get_statement_startend2(lineno, astnode) + # we need to correct the end: + # - ast-parsing strips comments + # - there might be empty lines + # - we might have lesser indented code blocks at the end + if end is None: + end = len(source.lines) + + if end > start + 1: + # make sure we don't span differently indented code blocks + # by using the BlockFinder helper used which inspect.getsource() uses itself + block_finder = inspect.BlockFinder() + # if we start with an indented line, put blockfinder to "started" mode + block_finder.started = source.lines[start][0].isspace() + it = ((x + "\n") for x in source.lines[start:end]) + try: + for tok in tokenize.generate_tokens(lambda: next(it)): + block_finder.tokeneater(*tok) + except (inspect.EndOfBlock, IndentationError): + end = block_finder.last + start + except Exception: + pass + + # the end might still point to a comment or empty line, correct it + while end: + line = source.lines[end - 1].lstrip() + if line.startswith("#") or not line: + end -= 1 + else: + break + return astnode, start, end + + +def getstatementrange_old(lineno, source, assertion=False): + """ return (start, end) tuple which spans the minimal + statement region which containing the given lineno. + raise an IndexError if no such statementrange can be found. + """ + # XXX this logic is only used on python2.4 and below + # 1. find the start of the statement + from codeop import compile_command + for start in range(lineno, -1, -1): + if assertion: + line = source.lines[start] + # the following lines are not fully tested, change with care + if 'super' in line and 'self' in line and '__init__' in line: + raise IndexError("likely a subclass") + if "assert" not in line and "raise" not in line: + continue + trylines = source.lines[start:lineno+1] + # quick hack to prepare parsing an indented line with + # compile_command() (which errors on "return" outside defs) + trylines.insert(0, 'def xxx():') + trysource = '\n '.join(trylines) + # ^ space here + try: + compile_command(trysource) + except (SyntaxError, OverflowError, ValueError): + continue + + # 2. find the end of the statement + for end in range(lineno+1, len(source)+1): + trysource = source[start:end] + if trysource.isparseable(): + return start, end + raise SyntaxError("no valid source range around line %d " % (lineno,)) + + diff --git a/_pytest/assertion/reinterpret.py b/_pytest/assertion/reinterpret.py index 213a3a00a..f4262c3ac 100644 --- a/_pytest/assertion/reinterpret.py +++ b/_pytest/assertion/reinterpret.py @@ -3,6 +3,8 @@ Find intermediate evalutation results in assert statements through builtin AST. """ import ast import sys + +import _pytest._code import py from _pytest.assertion import util u = py.builtin._totext @@ -26,7 +28,7 @@ class AssertionError(util.BuiltinAssertionError): "<[broken __repr__] %s at %0xd>" % (toprint.__class__, id(toprint))) else: - f = py.code.Frame(sys._getframe(1)) + f = _pytest._code.Frame(sys._getframe(1)) try: source = f.code.fullsource if source is not None: @@ -102,7 +104,7 @@ def reinterpret(source, frame, should_fail=False): def run(offending_line, frame=None): if frame is None: - frame = py.code.Frame(sys._getframe(1)) + frame = _pytest._code.Frame(sys._getframe(1)) return reinterpret(offending_line, frame) def getfailure(e): diff --git a/_pytest/assertion/util.py b/_pytest/assertion/util.py index 401d04f10..2c1e39c02 100644 --- a/_pytest/assertion/util.py +++ b/_pytest/assertion/util.py @@ -1,6 +1,7 @@ """Utilities for assertion debugging""" import pprint +import _pytest._code import py try: from collections import Sequence @@ -179,7 +180,7 @@ def assertrepr_compare(config, op, left, right): explanation = [ u('(pytest_assertion plugin: representation of details failed. ' 'Probably an object has a faulty __repr__.)'), - u(py.code.ExceptionInfo())] + u(_pytest._code.ExceptionInfo())] if not explanation: return None diff --git a/_pytest/config.py b/_pytest/config.py index 5ffed81dd..761b0e52e 100644 --- a/_pytest/config.py +++ b/_pytest/config.py @@ -8,6 +8,7 @@ import warnings import py # DON't import pytest here because it causes import cycle troubles import sys, os +import _pytest._code import _pytest.hookspec # the extension point definitions from _pytest._pluggy import PluginManager, HookimplMarker, HookspecMarker @@ -158,7 +159,7 @@ class PytestPluginManager(PluginManager): Use :py:meth:`pluggy.PluginManager.add_hookspecs` instead. """ warning = dict(code="I2", - fslocation=py.code.getfslineno(sys._getframe(1)), + fslocation=_pytest._code.getfslineno(sys._getframe(1)), nodeid=None, message="use pluginmanager.add_hookspecs instead of " "deprecated addhooks() method.") @@ -195,7 +196,7 @@ class PytestPluginManager(PluginManager): def _verify_hook(self, hook, hookmethod): super(PytestPluginManager, self)._verify_hook(hook, hookmethod) if "__multicall__" in hookmethod.argnames: - fslineno = py.code.getfslineno(hookmethod.function) + fslineno = _pytest._code.getfslineno(hookmethod.function) warning = dict(code="I1", fslocation=fslineno, nodeid=None, diff --git a/_pytest/doctest.py b/_pytest/doctest.py index a00c0c946..a57f7a494 100644 --- a/_pytest/doctest.py +++ b/_pytest/doctest.py @@ -1,9 +1,12 @@ """ discover and run doctests in modules and test files.""" from __future__ import absolute_import + import traceback -import pytest, py + +import pytest +from _pytest._code.code import TerminalRepr, ReprFileLocation, ExceptionInfo from _pytest.python import FixtureRequest -from py._code.code import TerminalRepr, ReprFileLocation + def pytest_addoption(parser): @@ -107,7 +110,7 @@ class DoctestItem(pytest.Item): lines += checker.output_difference(example, doctestfailure.got, REPORT_UDIFF).split("\n") else: - inner_excinfo = py.code.ExceptionInfo(excinfo.value.exc_info) + inner_excinfo = ExceptionInfo(excinfo.value.exc_info) lines += ["UNEXPECTED EXCEPTION: %s" % repr(inner_excinfo.value)] lines += traceback.format_exception(*excinfo.value.exc_info) diff --git a/_pytest/main.py b/_pytest/main.py index 6454ba2ae..70d6896cb 100644 --- a/_pytest/main.py +++ b/_pytest/main.py @@ -1,9 +1,13 @@ """ core implementation of testing process: init, session, runtest loop. """ +import imp +import os import re +import sys +import _pytest +import _pytest._code import py -import pytest, _pytest -import os, sys, imp +import pytest try: from collections import MutableMapping as MappingMixin except ImportError: @@ -91,11 +95,11 @@ def wrap_session(config, doit): except pytest.UsageError: raise except KeyboardInterrupt: - excinfo = py.code.ExceptionInfo() + excinfo = _pytest._code.ExceptionInfo() config.hook.pytest_keyboard_interrupt(excinfo=excinfo) session.exitstatus = EXIT_INTERRUPTED except: - excinfo = py.code.ExceptionInfo() + excinfo = _pytest._code.ExceptionInfo() config.notify_exception(excinfo, config.option) session.exitstatus = EXIT_INTERNALERROR if excinfo.errisinstance(SystemExit): diff --git a/_pytest/pytester.py b/_pytest/pytester.py index 95f92d835..faed7f581 100644 --- a/_pytest/pytester.py +++ b/_pytest/pytester.py @@ -1,19 +1,20 @@ """ (disabled by default) support for testing pytest and pytest plugins. """ -import gc -import sys -import traceback -import os import codecs -import re -import time +import gc +import os import platform -from fnmatch import fnmatch +import re import subprocess +import sys +import time +import traceback +from fnmatch import fnmatch -import py -import pytest from py.builtin import print_ +from _pytest._code import Source +import py +import pytest from _pytest.main import Session, EXIT_OK @@ -472,7 +473,7 @@ class Testdir: ret = None for name, value in items: p = self.tmpdir.join(name).new(ext=ext) - source = py.code.Source(value) + source = Source(value) def my_totext(s, encoding="utf-8"): if py.builtin._isbytes(s): s = py.builtin._totext(s, encoding=encoding) @@ -835,7 +836,7 @@ class Testdir: to the temporarly directory to ensure it is a package. """ - kw = {self.request.function.__name__: py.code.Source(source).strip()} + kw = {self.request.function.__name__: Source(source).strip()} path = self.makepyfile(**kw) if withinit: self.makepyfile(__init__ = "#") @@ -1041,8 +1042,8 @@ class LineMatcher: def _getlines(self, lines2): if isinstance(lines2, str): - lines2 = py.code.Source(lines2) - if isinstance(lines2, py.code.Source): + lines2 = Source(lines2) + if isinstance(lines2, Source): lines2 = lines2.strip().lines return lines2 diff --git a/_pytest/python.py b/_pytest/python.py index 6f3a717af..065971be6 100644 --- a/_pytest/python.py +++ b/_pytest/python.py @@ -1,14 +1,15 @@ """ Python test discovery, setup and run of test functions. """ -import re import fnmatch import functools -import py import inspect +import re import types import sys + +import py import pytest +from _pytest._code.code import TerminalRepr from _pytest.mark import MarkDecorator, MarkerError -from py._code.code import TerminalRepr try: import enum @@ -86,7 +87,7 @@ def getfslineno(obj): obj = get_real_func(obj) if hasattr(obj, 'place_as'): obj = obj.place_as - fslineno = py.code.getfslineno(obj) + fslineno = _pytest._code.getfslineno(obj) assert isinstance(fslineno[1], int), obj return fslineno @@ -331,7 +332,7 @@ def pytest_pycollect_makeitem(collector, name, obj): def is_generator(func): try: - return py.code.getrawcode(func).co_flags & 32 # generator function + return _pytest._code.getrawcode(func).co_flags & 32 # generator function except AttributeError: # builtin functions have no bytecode # assume them to not be generators return False @@ -610,7 +611,7 @@ class Module(pytest.File, PyCollector): mod = self.fspath.pyimport(ensuresyspath=importmode) except SyntaxError: raise self.CollectError( - py.code.ExceptionInfo().getrepr(style="short")) + _pytest._code.ExceptionInfo().getrepr(style="short")) except self.fspath.ImportMismatchError: e = sys.exc_info()[1] raise self.CollectError( @@ -716,7 +717,7 @@ class FunctionMixin(PyobjMixin): def _prunetraceback(self, excinfo): if hasattr(self, '_obj') and not self.config.option.fulltrace: - code = py.code.Code(get_real_func(self.obj)) + code = _pytest._code.Code(get_real_func(self.obj)) path, firstlineno = code.path, code.firstlineno traceback = excinfo.traceback ntraceback = traceback.cut(path=path, firstlineno=firstlineno) @@ -1202,10 +1203,10 @@ def getlocation(function, curdir): # builtin pytest.raises helper def raises(expected_exception, *args, **kwargs): - """ assert that a code block/function call raises @expected_exception + """ assert that a code block/function call raises ``expected_exception`` and raise a failure exception otherwise. - This helper produces a ``py.code.ExceptionInfo()`` object. + This helper produces a ``ExceptionInfo()`` object (see below). If using Python 2.5 or above, you may use this function as a context manager:: @@ -1221,19 +1222,19 @@ def raises(expected_exception, *args, **kwargs): Lines of code after that, within the scope of the context manager will not be executed. For example:: - >>> with raises(OSError) as err: + >>> with raises(OSError) as exc_info: assert 1 == 1 # this will execute as expected raise OSError(errno.EEXISTS, 'directory exists') - assert err.errno == errno.EEXISTS # this will not execute + assert exc_info.value.errno == errno.EEXISTS # this will not execute Instead, the following approach must be taken (note the difference in scope):: - >>> with raises(OSError) as err: + >>> with raises(OSError) as exc_info: assert 1 == 1 # this will execute as expected raise OSError(errno.EEXISTS, 'directory exists') - assert err.errno == errno.EEXISTS # this will now execute + assert exc_info.value.errno == errno.EEXISTS # this will now execute Or you can specify a callable by passing a to-be-called lambda:: @@ -1254,21 +1255,22 @@ def raises(expected_exception, *args, **kwargs): >>> raises(ZeroDivisionError, "f(0)") - Performance note: - ----------------- + .. autoclass:: _pytest._code.ExceptionInfo + :members: - Similar to caught exception objects in Python, explicitly clearing - local references to returned ``py.code.ExceptionInfo`` objects can - help the Python interpreter speed up its garbage collection. + .. note:: + Similar to caught exception objects in Python, explicitly clearing + local references to returned ``ExceptionInfo`` objects can + help the Python interpreter speed up its garbage collection. - Clearing those references breaks a reference cycle - (``ExceptionInfo`` --> caught exception --> frame stack raising - the exception --> current frame stack --> local variables --> - ``ExceptionInfo``) which makes Python keep all objects referenced - from that cycle (including all local variables in the current - frame) alive until the next cyclic garbage collection run. See the - official Python ``try`` statement documentation for more detailed - information. + Clearing those references breaks a reference cycle + (``ExceptionInfo`` --> caught exception --> frame stack raising + the exception --> current frame stack --> local variables --> + ``ExceptionInfo``) which makes Python keep all objects referenced + from that cycle (including all local variables in the current + frame) alive until the next cyclic garbage collection run. See the + official Python ``try`` statement documentation for more detailed + information. """ __tracebackhide__ = True @@ -1297,18 +1299,18 @@ def raises(expected_exception, *args, **kwargs): loc.update(kwargs) #print "raises frame scope: %r" % frame.f_locals try: - code = py.code.Source(code).compile() + code = _pytest._code.Source(code).compile() py.builtin.exec_(code, frame.f_globals, loc) # XXX didn'T mean f_globals == f_locals something special? # this is destroyed here ... except expected_exception: - return py.code.ExceptionInfo() + return _pytest._code.ExceptionInfo() else: func = args[0] try: func(*args[1:], **kwargs) except expected_exception: - return py.code.ExceptionInfo() + return _pytest._code.ExceptionInfo() pytest.fail("DID NOT RAISE") class RaisesContext(object): @@ -1317,7 +1319,7 @@ class RaisesContext(object): self.excinfo = None def __enter__(self): - self.excinfo = object.__new__(py.code.ExceptionInfo) + self.excinfo = object.__new__(_pytest._code.ExceptionInfo) return self.excinfo def __exit__(self, *tp): @@ -2025,7 +2027,7 @@ class FixtureManager: def fail_fixturefunc(fixturefunc, msg): fs, lineno = getfslineno(fixturefunc) location = "%s:%s" % (fs, lineno+1) - source = py.code.Source(fixturefunc) + source = _pytest._code.Source(fixturefunc) pytest.fail(msg + ":\n\n" + str(source.indent()) + "\n" + location, pytrace=False) @@ -2168,14 +2170,14 @@ def getfuncargnames(function, startindex=None): startindex += num_mock_patch_args(function) function = realfunction if isinstance(function, functools.partial): - argnames = inspect.getargs(py.code.getrawcode(function.func))[0] + argnames = inspect.getargs(_pytest._code.getrawcode(function.func))[0] partial = function argnames = argnames[len(partial.args):] if partial.keywords: for kw in partial.keywords: argnames.remove(kw) else: - argnames = inspect.getargs(py.code.getrawcode(function))[0] + argnames = inspect.getargs(_pytest._code.getrawcode(function))[0] defaults = getattr(function, 'func_defaults', getattr(function, '__defaults__', None)) or () numdefaults = len(defaults) diff --git a/_pytest/recwarn.py b/_pytest/recwarn.py index 16723289d..a89474c03 100644 --- a/_pytest/recwarn.py +++ b/_pytest/recwarn.py @@ -1,6 +1,8 @@ """ recording warnings during test function execution. """ import inspect + +import _pytest._code import py import sys import warnings @@ -100,7 +102,7 @@ def warns(expected_warning, *args, **kwargs): loc.update(kwargs) with wcheck: - code = py.code.Source(code).compile() + code = _pytest._code.Source(code).compile() py.builtin.exec_(code, frame.f_globals, loc) else: func = args[0] diff --git a/_pytest/runner.py b/_pytest/runner.py index 979757c16..a50c2d738 100644 --- a/_pytest/runner.py +++ b/_pytest/runner.py @@ -5,7 +5,8 @@ from time import time import py import pytest -from py._code.code import TerminalRepr +from _pytest._code.code import TerminalRepr, ExceptionInfo + def pytest_namespace(): return { @@ -151,7 +152,7 @@ class CallInfo: self.stop = time() raise except: - self.excinfo = py.code.ExceptionInfo() + self.excinfo = ExceptionInfo() self.stop = time() def __repr__(self): @@ -215,7 +216,7 @@ def pytest_runtest_makereport(item, call): outcome = "passed" longrepr = None else: - if not isinstance(excinfo, py.code.ExceptionInfo): + if not isinstance(excinfo, ExceptionInfo): outcome = "failed" longrepr = excinfo elif excinfo.errisinstance(pytest.skip.Exception): diff --git a/_pytest/skipping.py b/_pytest/skipping.py index 9bd38d684..5b779c98b 100644 --- a/_pytest/skipping.py +++ b/_pytest/skipping.py @@ -291,9 +291,8 @@ def cached_eval(config, expr, d): try: return config._evalcache[expr] except KeyError: - #import sys - #print >>sys.stderr, ("cache-miss: %r" % expr) - exprcode = py.code.compile(expr, mode="eval") + import _pytest._code + exprcode = _pytest._code.compile(expr, mode="eval") config._evalcache[expr] = x = eval(exprcode, d) return x diff --git a/_pytest/unittest.py b/_pytest/unittest.py index 24fa9e921..8120e94fb 100644 --- a/_pytest/unittest.py +++ b/_pytest/unittest.py @@ -1,13 +1,12 @@ """ discovery and running of std-library "unittest" style tests. """ from __future__ import absolute_import -import traceback + import sys +import traceback import pytest -import py - - # for transfering markers +import _pytest._code from _pytest.python import transfer_markers from _pytest.skipping import MarkEvaluator @@ -101,7 +100,7 @@ class TestCaseFunction(pytest.Function): # unwrap potential exception info (see twisted trial support below) rawexcinfo = getattr(rawexcinfo, '_rawexcinfo', rawexcinfo) try: - excinfo = py.code.ExceptionInfo(rawexcinfo) + excinfo = _pytest._code.ExceptionInfo(rawexcinfo) except TypeError: try: try: @@ -117,7 +116,7 @@ class TestCaseFunction(pytest.Function): except KeyboardInterrupt: raise except pytest.fail.Exception: - excinfo = py.code.ExceptionInfo() + excinfo = _pytest._code.ExceptionInfo() self.__dict__.setdefault('_excinfo', []).append(excinfo) def addError(self, testcase, rawexcinfo): diff --git a/doc/en/assert.rst b/doc/en/assert.rst index f65225c92..beac284dc 100644 --- a/doc/en/assert.rst +++ b/doc/en/assert.rst @@ -81,13 +81,10 @@ and if you need to have access to the actual exception info you may use:: f() assert 'maximum recursion' in str(excinfo.value) -``excinfo`` is a `py.code.ExceptionInfo`_ instance, which is a wrapper around +``excinfo`` is a ``ExceptionInfo`` instance, which is a wrapper around the actual exception raised. The main attributes of interest are ``.type``, ``.value`` and ``.traceback``. -.. _py.code.ExceptionInfo: - http://pylib.readthedocs.org/en/latest/code.html#py-code-exceptioninfo - If you want to write test code that works on Python 2.4 as well, you may also use two other ways to test for an expected exception:: diff --git a/doc/en/example/assertion/failure_demo.py b/doc/en/example/assertion/failure_demo.py index ecc1cd356..a4ff758b1 100644 --- a/doc/en/example/assertion/failure_demo.py +++ b/doc/en/example/assertion/failure_demo.py @@ -1,4 +1,5 @@ from pytest import raises +import _pytest._code import py def otherfunc(a,b): @@ -159,7 +160,7 @@ def test_dynamic_compile_shows_nicely(): src = 'def foo():\n assert 1 == 0\n' name = 'abc-123' module = py.std.imp.new_module(name) - code = py.code.compile(src, name, 'exec') + code = _pytest._code.compile(src, name, 'exec') py.builtin.exec_(code, module.__dict__) py.std.sys.modules[name] = module module.foo() diff --git a/doc/en/example/multipython.py b/doc/en/example/multipython.py index b9242101b..66a368a12 100644 --- a/doc/en/example/multipython.py +++ b/doc/en/example/multipython.py @@ -4,6 +4,7 @@ serialization via the pickle module. """ import py import pytest +import _pytest._code pythonlist = ['python2.6', 'python2.7', 'python3.3'] @pytest.fixture(params=pythonlist) @@ -23,7 +24,7 @@ class Python: self.picklefile = picklefile def dumps(self, obj): dumpfile = self.picklefile.dirpath("dump.py") - dumpfile.write(py.code.Source(""" + dumpfile.write(_pytest._code.Source(""" import pickle f = open(%r, 'wb') s = pickle.dump(%r, f, protocol=2) @@ -33,7 +34,7 @@ class Python: def load_and_is_true(self, expression): loadfile = self.picklefile.dirpath("load.py") - loadfile.write(py.code.Source(""" + loadfile.write(_pytest._code.Source(""" import pickle f = open(%r, 'rb') obj = pickle.load(f) diff --git a/setup.py b/setup.py index ec9c9d430..6660f2160 100644 --- a/setup.py +++ b/setup.py @@ -75,7 +75,7 @@ def main(): # the following should be enabled for release install_requires=install_requires, extras_require=extras_require, - packages=['_pytest', '_pytest.assertion', '_pytest.vendored_packages'], + packages=['_pytest', '_pytest.assertion', '_pytest._code', '_pytest.vendored_packages'], py_modules=['pytest'], zip_safe=False, ) diff --git a/testing/acceptance_test.py b/testing/acceptance_test.py index 4cd731e72..74db4425d 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -1,5 +1,8 @@ import sys -import py, pytest + +import _pytest._code +import py +import pytest from _pytest.main import EXIT_NOTESTSCOLLECTED, EXIT_USAGEERROR @@ -197,7 +200,7 @@ class TestGeneralUsage: def test_chdir(self, testdir): testdir.tmpdir.join("py").mksymlinkto(py._pydir) p = testdir.tmpdir.join("main.py") - p.write(py.code.Source(""" + p.write(_pytest._code.Source(""" import sys, os sys.path.insert(0, '') import py diff --git a/testing/code/test_code.py b/testing/code/test_code.py new file mode 100644 index 000000000..e9c7f7ab1 --- /dev/null +++ b/testing/code/test_code.py @@ -0,0 +1,163 @@ +import sys + +import _pytest._code +import py +import pytest + + +def test_ne(): + code1 = _pytest._code.Code(compile('foo = "bar"', '', 'exec')) + assert code1 == code1 + code2 = _pytest._code.Code(compile('foo = "baz"', '', 'exec')) + assert code2 != code1 + +def test_code_gives_back_name_for_not_existing_file(): + name = 'abc-123' + co_code = compile("pass\n", name, 'exec') + assert co_code.co_filename == name + code = _pytest._code.Code(co_code) + assert str(code.path) == name + assert code.fullsource is None + +def test_code_with_class(): + class A: + pass + pytest.raises(TypeError, "_pytest._code.Code(A)") + +if True: + def x(): + pass + +def test_code_fullsource(): + code = _pytest._code.Code(x) + full = code.fullsource + assert 'test_code_fullsource()' in str(full) + +def test_code_source(): + code = _pytest._code.Code(x) + src = code.source() + expected = """def x(): + pass""" + assert str(src) == expected + +def test_frame_getsourcelineno_myself(): + def func(): + return sys._getframe(0) + f = func() + f = _pytest._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 = _pytest._code.Frame(f) + prop = f.code.__class__.fullsource + try: + f.code.__class__.fullsource = None + assert f.statement == _pytest._code.Source("") + finally: + f.code.__class__.fullsource = prop + +def test_code_from_func(): + co = _pytest._code.Code(test_frame_getsourcelineno_myself) + assert co.firstlineno + assert co.path + + + +def test_builtin_patch_unpatch(monkeypatch): + cpy_builtin = py.builtin.builtins + comp = cpy_builtin.compile + def mycompile(*args, **kwargs): + return comp(*args, **kwargs) + class Sub(AssertionError): + pass + monkeypatch.setattr(cpy_builtin, 'AssertionError', Sub) + monkeypatch.setattr(cpy_builtin, 'compile', mycompile) + _pytest._code.patch_builtins() + assert cpy_builtin.AssertionError != Sub + assert cpy_builtin.compile != mycompile + _pytest._code.unpatch_builtins() + assert cpy_builtin.AssertionError is Sub + assert cpy_builtin.compile == mycompile + + +def test_unicode_handling(): + value = py.builtin._totext('\xc4\x85\xc4\x87\n', 'utf-8').encode('utf8') + def f(): + raise Exception(value) + excinfo = pytest.raises(Exception, f) + str(excinfo) + if sys.version_info[0] < 3: + unicode(excinfo) + +def test_code_getargs(): + def f1(x): + pass + c1 = _pytest._code.Code(f1) + assert c1.getargs(var=True) == ('x',) + + def f2(x, *y): + pass + c2 = _pytest._code.Code(f2) + assert c2.getargs(var=True) == ('x', 'y') + + def f3(x, **z): + pass + c3 = _pytest._code.Code(f3) + assert c3.getargs(var=True) == ('x', 'z') + + def f4(x, *y, **z): + pass + c4 = _pytest._code.Code(f4) + assert c4.getargs(var=True) == ('x', 'y', 'z') + + +def test_frame_getargs(): + def f1(x): + return sys._getframe(0) + fr1 = _pytest._code.Frame(f1('a')) + assert fr1.getargs(var=True) == [('x', 'a')] + + def f2(x, *y): + return sys._getframe(0) + fr2 = _pytest._code.Frame(f2('a', 'b', 'c')) + assert fr2.getargs(var=True) == [('x', 'a'), ('y', ('b', 'c'))] + + def f3(x, **z): + return sys._getframe(0) + fr3 = _pytest._code.Frame(f3('a', b='c')) + assert fr3.getargs(var=True) == [('x', 'a'), ('z', {'b': 'c'})] + + def f4(x, *y, **z): + return sys._getframe(0) + fr4 = _pytest._code.Frame(f4('a', 'b', c='d')) + assert fr4.getargs(var=True) == [('x', 'a'), ('y', ('b',)), + ('z', {'c': 'd'})] + + +class TestExceptionInfo: + + def test_bad_getsource(self): + try: + if False: pass + else: assert False + except AssertionError: + exci = _pytest._code.ExceptionInfo() + assert exci.getrepr() + + +class TestTracebackEntry: + + def test_getsource(self): + try: + if False: pass + else: assert False + except AssertionError: + exci = _pytest._code.ExceptionInfo() + entry = exci.traceback[0] + source = entry.getsource() + assert len(source) == 4 + assert 'else: assert False' in source[3] diff --git a/testing/code/test_excinfo.py b/testing/code/test_excinfo.py new file mode 100644 index 000000000..2defa3103 --- /dev/null +++ b/testing/code/test_excinfo.py @@ -0,0 +1,911 @@ +# -*- coding: utf-8 -*- + +import _pytest +import py +import pytest +from _pytest._code.code import FormattedExcinfo, ReprExceptionInfo + +queue = py.builtin._tryimport('queue', 'Queue') + +failsonjython = pytest.mark.xfail("sys.platform.startswith('java')") +from test_source import astonly + +try: + import importlib +except ImportError: + invalidate_import_caches = None +else: + invalidate_import_caches = getattr(importlib, "invalidate_caches", None) + +import pytest +pytest_version_info = tuple(map(int, pytest.__version__.split(".")[:3])) + +class TWMock: + def __init__(self): + self.lines = [] + def sep(self, sep, line=None): + self.lines.append((sep, line)) + def line(self, line, **kw): + self.lines.append(line) + def markup(self, text, **kw): + return text + + fullwidth = 80 + +def test_excinfo_simple(): + try: + raise ValueError + except ValueError: + info = _pytest._code.ExceptionInfo() + assert info.type == ValueError + +def test_excinfo_getstatement(): + def g(): + raise ValueError + def f(): + g() + try: + f() + except ValueError: + excinfo = _pytest._code.ExceptionInfo() + linenumbers = [_pytest._code.getrawcode(f).co_firstlineno - 1 + 3, + _pytest._code.getrawcode(f).co_firstlineno - 1 + 1, + _pytest._code.getrawcode(g).co_firstlineno - 1 + 1, ] + l = list(excinfo.traceback) + foundlinenumbers = [x.lineno for x in l] + assert foundlinenumbers == linenumbers + #for x in info: + # print "%s:%d %s" %(x.path.relto(root), x.lineno, x.statement) + #xxx + +# testchain for getentries test below +def f(): + # + raise ValueError + # +def g(): + # + __tracebackhide__ = True + f() + # +def h(): + # + g() + # + +class TestTraceback_f_g_h: + def setup_method(self, method): + try: + h() + except ValueError: + self.excinfo = _pytest._code.ExceptionInfo() + + def test_traceback_entries(self): + tb = self.excinfo.traceback + entries = list(tb) + assert len(tb) == 4 # maybe fragile test + assert len(entries) == 4 # maybe fragile test + names = ['f', 'g', 'h'] + for entry in entries: + try: + names.remove(entry.frame.code.name) + except ValueError: + pass + assert not names + + def test_traceback_entry_getsource(self): + tb = self.excinfo.traceback + s = str(tb[-1].getsource() ) + assert s.startswith("def f():") + assert s.endswith("raise ValueError") + + @astonly + @failsonjython + def test_traceback_entry_getsource_in_construct(self): + source = _pytest._code.Source("""\ + def xyz(): + try: + raise ValueError + except somenoname: + pass + xyz() + """) + try: + exec (source.compile()) + except NameError: + tb = _pytest._code.ExceptionInfo().traceback + print (tb[-1].getsource()) + s = str(tb[-1].getsource()) + assert s.startswith("def xyz():\n try:") + assert s.strip().endswith("except somenoname:") + + def test_traceback_cut(self): + co = _pytest._code.Code(f) + path, firstlineno = co.path, co.firstlineno + traceback = self.excinfo.traceback + newtraceback = traceback.cut(path=path, firstlineno=firstlineno) + assert len(newtraceback) == 1 + newtraceback = traceback.cut(path=path, lineno=firstlineno+2) + assert len(newtraceback) == 1 + + def test_traceback_cut_excludepath(self, testdir): + p = testdir.makepyfile("def f(): raise ValueError") + excinfo = pytest.raises(ValueError, "p.pyimport().f()") + basedir = py.path.local(pytest.__file__).dirpath() + newtraceback = excinfo.traceback.cut(excludepath=basedir) + for x in newtraceback: + if hasattr(x, 'path'): + assert not py.path.local(x.path).relto(basedir) + assert newtraceback[-1].frame.code.path == p + + def test_traceback_filter(self): + traceback = self.excinfo.traceback + ntraceback = traceback.filter() + assert len(ntraceback) == len(traceback) - 1 + + def test_traceback_recursion_index(self): + def f(n): + if n < 10: + n += 1 + f(n) + excinfo = pytest.raises(RuntimeError, f, 8) + traceback = excinfo.traceback + recindex = traceback.recursionindex() + assert recindex == 3 + + def test_traceback_only_specific_recursion_errors(self, monkeypatch): + def f(n): + if n == 0: + raise RuntimeError("hello") + f(n-1) + + excinfo = pytest.raises(RuntimeError, f, 100) + monkeypatch.delattr(excinfo.traceback.__class__, "recursionindex") + repr = excinfo.getrepr() + assert "RuntimeError: hello" in str(repr.reprcrash) + + def test_traceback_no_recursion_index(self): + def do_stuff(): + raise RuntimeError + def reraise_me(): + import sys + exc, val, tb = sys.exc_info() + py.builtin._reraise(exc, val, tb) + def f(n): + try: + do_stuff() + except: + reraise_me() + excinfo = pytest.raises(RuntimeError, f, 8) + traceback = excinfo.traceback + recindex = traceback.recursionindex() + assert recindex is None + + def test_traceback_messy_recursion(self): + #XXX: simplified locally testable version + decorator = pytest.importorskip('decorator').decorator + + def log(f, *k, **kw): + print('%s %s' % (k, kw)) + f(*k, **kw) + log = decorator(log) + + def fail(): + raise ValueError('') + + fail = log(log(fail)) + + excinfo = pytest.raises(ValueError, fail) + assert excinfo.traceback.recursionindex() is None + + + + def test_traceback_getcrashentry(self): + def i(): + __tracebackhide__ = True + raise ValueError + def h(): + i() + def g(): + __tracebackhide__ = True + h() + def f(): + g() + + excinfo = pytest.raises(ValueError, f) + tb = excinfo.traceback + entry = tb.getcrashentry() + co = _pytest._code.Code(h) + assert entry.frame.code.path == co.path + assert entry.lineno == co.firstlineno + 1 + assert entry.frame.code.name == 'h' + + def test_traceback_getcrashentry_empty(self): + def g(): + __tracebackhide__ = True + raise ValueError + def f(): + __tracebackhide__ = True + g() + + excinfo = pytest.raises(ValueError, f) + tb = excinfo.traceback + entry = tb.getcrashentry() + co = _pytest._code.Code(g) + assert entry.frame.code.path == co.path + assert entry.lineno == co.firstlineno + 2 + assert entry.frame.code.name == 'g' + +def hello(x): + x + 5 + +def test_tbentry_reinterpret(): + try: + hello("hello") + except TypeError: + excinfo = _pytest._code.ExceptionInfo() + tbentry = excinfo.traceback[-1] + msg = tbentry.reinterpret() + assert msg.startswith("TypeError: ('hello' + 5)") + +def test_excinfo_exconly(): + excinfo = pytest.raises(ValueError, h) + assert excinfo.exconly().startswith('ValueError') + excinfo = pytest.raises(ValueError, + "raise ValueError('hello\\nworld')") + msg = excinfo.exconly(tryshort=True) + assert msg.startswith('ValueError') + assert msg.endswith("world") + +def test_excinfo_repr(): + excinfo = pytest.raises(ValueError, h) + s = repr(excinfo) + assert s == "" + +def test_excinfo_str(): + excinfo = pytest.raises(ValueError, h) + s = str(excinfo) + assert s.startswith(__file__[:-9]) # pyc file and $py.class + assert s.endswith("ValueError") + assert len(s.split(":")) >= 3 # on windows it's 4 + +def test_excinfo_errisinstance(): + excinfo = pytest.raises(ValueError, h) + assert excinfo.errisinstance(ValueError) + +def test_excinfo_no_sourcecode(): + try: + exec ("raise ValueError()") + except ValueError: + excinfo = _pytest._code.ExceptionInfo() + s = str(excinfo.traceback[-1]) + if py.std.sys.version_info < (2,5): + assert s == " File '':1 in ?\n ???\n" + else: + assert s == " File '':1 in \n ???\n" + +def test_excinfo_no_python_sourcecode(tmpdir): + #XXX: simplified locally testable version + tmpdir.join('test.txt').write("{{ h()}}:") + + jinja2 = pytest.importorskip('jinja2') + loader = jinja2.FileSystemLoader(str(tmpdir)) + env = jinja2.Environment(loader=loader) + template = env.get_template('test.txt') + excinfo = pytest.raises(ValueError, + template.render, h=h) + for item in excinfo.traceback: + print(item) #XXX: for some reason jinja.Template.render is printed in full + item.source # shouldnt fail + if item.path.basename == 'test.txt': + assert str(item.source) == '{{ h()}}:' + + +def test_entrysource_Queue_example(): + try: + queue.Queue().get(timeout=0.001) + except queue.Empty: + excinfo = _pytest._code.ExceptionInfo() + entry = excinfo.traceback[-1] + source = entry.getsource() + assert source is not None + s = str(source).strip() + assert s.startswith("def get") + +def test_codepath_Queue_example(): + try: + queue.Queue().get(timeout=0.001) + except queue.Empty: + excinfo = _pytest._code.ExceptionInfo() + entry = excinfo.traceback[-1] + path = entry.path + assert isinstance(path, py.path.local) + assert path.basename.lower() == "queue.py" + assert path.check() + +class TestFormattedExcinfo: + def pytest_funcarg__importasmod(self, request): + def importasmod(source): + source = _pytest._code.Source(source) + tmpdir = request.getfuncargvalue("tmpdir") + modpath = tmpdir.join("mod.py") + tmpdir.ensure("__init__.py") + modpath.write(source) + if invalidate_import_caches is not None: + invalidate_import_caches() + return modpath.pyimport() + return importasmod + + def excinfo_from_exec(self, source): + source = _pytest._code.Source(source).strip() + try: + exec (source.compile()) + except KeyboardInterrupt: + raise + except: + return _pytest._code.ExceptionInfo() + assert 0, "did not raise" + + def test_repr_source(self): + pr = FormattedExcinfo() + source = _pytest._code.Source(""" + def f(x): + pass + """).strip() + pr.flow_marker = "|" + lines = pr.get_source(source, 0) + assert len(lines) == 2 + assert lines[0] == "| def f(x):" + assert lines[1] == " pass" + + def test_repr_source_excinfo(self): + """ check if indentation is right """ + pr = FormattedExcinfo() + excinfo = self.excinfo_from_exec(""" + def f(): + assert 0 + f() + """) + pr = FormattedExcinfo() + source = pr._getentrysource(excinfo.traceback[-1]) + lines = pr.get_source(source, 1, excinfo) + assert lines == [ + ' def f():', + '> assert 0', + 'E assert 0' + ] + + + def test_repr_source_not_existing(self): + pr = FormattedExcinfo() + co = compile("raise ValueError()", "", "exec") + try: + exec (co) + except ValueError: + excinfo = _pytest._code.ExceptionInfo() + repr = pr.repr_excinfo(excinfo) + assert repr.reprtraceback.reprentries[1].lines[0] == "> ???" + + def test_repr_many_line_source_not_existing(self): + pr = FormattedExcinfo() + co = compile(""" +a = 1 +raise ValueError() +""", "", "exec") + try: + exec (co) + except ValueError: + excinfo = _pytest._code.ExceptionInfo() + repr = pr.repr_excinfo(excinfo) + assert repr.reprtraceback.reprentries[1].lines[0] == "> ???" + + def test_repr_source_failing_fullsource(self): + pr = FormattedExcinfo() + + class FakeCode(object): + class raw: + co_filename = '?' + path = '?' + firstlineno = 5 + + def fullsource(self): + return None + fullsource = property(fullsource) + + class FakeFrame(object): + code = FakeCode() + f_locals = {} + f_globals = {} + + class FakeTracebackEntry(_pytest._code.Traceback.Entry): + def __init__(self, tb): + self.lineno = 5+3 + + @property + def frame(self): + return FakeFrame() + + class Traceback(_pytest._code.Traceback): + Entry = FakeTracebackEntry + + class FakeExcinfo(_pytest._code.ExceptionInfo): + typename = "Foo" + def __init__(self): + pass + + def exconly(self, tryshort): + return "EXC" + def errisinstance(self, cls): + return False + + excinfo = FakeExcinfo() + class FakeRawTB(object): + tb_next = None + tb = FakeRawTB() + excinfo.traceback = Traceback(tb) + + fail = IOError() # noqa + repr = pr.repr_excinfo(excinfo) + assert repr.reprtraceback.reprentries[0].lines[0] == "> ???" + + fail = py.error.ENOENT # noqa + repr = pr.repr_excinfo(excinfo) + assert repr.reprtraceback.reprentries[0].lines[0] == "> ???" + + + def test_repr_local(self): + p = FormattedExcinfo(showlocals=True) + loc = {'y': 5, 'z': 7, 'x': 3, '@x': 2, '__builtins__': {}} + reprlocals = p.repr_locals(loc) + assert reprlocals.lines + assert reprlocals.lines[0] == '__builtins__ = ' + assert reprlocals.lines[1] == 'x = 3' + assert reprlocals.lines[2] == 'y = 5' + assert reprlocals.lines[3] == 'z = 7' + + def test_repr_tracebackentry_lines(self, importasmod): + mod = importasmod(""" + def func1(): + raise ValueError("hello\\nworld") + """) + excinfo = pytest.raises(ValueError, mod.func1) + excinfo.traceback = excinfo.traceback.filter() + p = FormattedExcinfo() + reprtb = p.repr_traceback_entry(excinfo.traceback[-1]) + + # test as intermittent entry + lines = reprtb.lines + assert lines[0] == ' def func1():' + assert lines[1] == '> raise ValueError("hello\\nworld")' + + # test as last entry + p = FormattedExcinfo(showlocals=True) + repr_entry = p.repr_traceback_entry(excinfo.traceback[-1], excinfo) + lines = repr_entry.lines + assert lines[0] == ' def func1():' + assert lines[1] == '> raise ValueError("hello\\nworld")' + assert lines[2] == 'E ValueError: hello' + assert lines[3] == 'E world' + assert not lines[4:] + + loc = repr_entry.reprlocals is not None + loc = repr_entry.reprfileloc + assert loc.path == mod.__file__ + assert loc.lineno == 3 + #assert loc.message == "ValueError: hello" + + def test_repr_tracebackentry_lines2(self, importasmod): + mod = importasmod(""" + def func1(m, x, y, z): + raise ValueError("hello\\nworld") + """) + excinfo = pytest.raises(ValueError, mod.func1, "m"*90, 5, 13, "z"*120) + excinfo.traceback = excinfo.traceback.filter() + entry = excinfo.traceback[-1] + p = FormattedExcinfo(funcargs=True) + reprfuncargs = p.repr_args(entry) + assert reprfuncargs.args[0] == ('m', repr("m"*90)) + assert reprfuncargs.args[1] == ('x', '5') + assert reprfuncargs.args[2] == ('y', '13') + assert reprfuncargs.args[3] == ('z', repr("z" * 120)) + + p = FormattedExcinfo(funcargs=True) + repr_entry = p.repr_traceback_entry(entry) + assert repr_entry.reprfuncargs.args == reprfuncargs.args + tw = TWMock() + repr_entry.toterminal(tw) + assert tw.lines[0] == "m = " + repr('m' * 90) + assert tw.lines[1] == "x = 5, y = 13" + assert tw.lines[2] == "z = " + repr('z' * 120) + + def test_repr_tracebackentry_lines_var_kw_args(self, importasmod): + mod = importasmod(""" + def func1(x, *y, **z): + raise ValueError("hello\\nworld") + """) + excinfo = pytest.raises(ValueError, mod.func1, 'a', 'b', c='d') + excinfo.traceback = excinfo.traceback.filter() + entry = excinfo.traceback[-1] + p = FormattedExcinfo(funcargs=True) + reprfuncargs = p.repr_args(entry) + assert reprfuncargs.args[0] == ('x', repr('a')) + assert reprfuncargs.args[1] == ('y', repr(('b',))) + assert reprfuncargs.args[2] == ('z', repr({'c': 'd'})) + + p = FormattedExcinfo(funcargs=True) + repr_entry = p.repr_traceback_entry(entry) + assert repr_entry.reprfuncargs.args == reprfuncargs.args + tw = TWMock() + repr_entry.toterminal(tw) + assert tw.lines[0] == "x = 'a', y = ('b',), z = {'c': 'd'}" + + def test_repr_tracebackentry_short(self, importasmod): + mod = importasmod(""" + def func1(): + raise ValueError("hello") + def entry(): + func1() + """) + excinfo = pytest.raises(ValueError, mod.entry) + p = FormattedExcinfo(style="short") + reprtb = p.repr_traceback_entry(excinfo.traceback[-2]) + lines = reprtb.lines + basename = py.path.local(mod.__file__).basename + assert lines[0] == ' func1()' + assert basename in str(reprtb.reprfileloc.path) + assert reprtb.reprfileloc.lineno == 5 + + # test last entry + p = FormattedExcinfo(style="short") + reprtb = p.repr_traceback_entry(excinfo.traceback[-1], excinfo) + lines = reprtb.lines + assert lines[0] == ' raise ValueError("hello")' + assert lines[1] == 'E ValueError: hello' + assert basename in str(reprtb.reprfileloc.path) + assert reprtb.reprfileloc.lineno == 3 + + def test_repr_tracebackentry_no(self, importasmod): + mod = importasmod(""" + def func1(): + raise ValueError("hello") + def entry(): + func1() + """) + excinfo = pytest.raises(ValueError, mod.entry) + p = FormattedExcinfo(style="no") + p.repr_traceback_entry(excinfo.traceback[-2]) + + p = FormattedExcinfo(style="no") + reprentry = p.repr_traceback_entry(excinfo.traceback[-1], excinfo) + lines = reprentry.lines + assert lines[0] == 'E ValueError: hello' + assert not lines[1:] + + def test_repr_traceback_tbfilter(self, importasmod): + mod = importasmod(""" + def f(x): + raise ValueError(x) + def entry(): + f(0) + """) + excinfo = pytest.raises(ValueError, mod.entry) + p = FormattedExcinfo(tbfilter=True) + reprtb = p.repr_traceback(excinfo) + assert len(reprtb.reprentries) == 2 + p = FormattedExcinfo(tbfilter=False) + reprtb = p.repr_traceback(excinfo) + assert len(reprtb.reprentries) == 3 + + def test_traceback_short_no_source(self, importasmod, monkeypatch): + mod = importasmod(""" + def func1(): + raise ValueError("hello") + def entry(): + func1() + """) + excinfo = pytest.raises(ValueError, mod.entry) + from _pytest._code.code import Code + monkeypatch.setattr(Code, 'path', 'bogus') + excinfo.traceback[0].frame.code.path = "bogus" + p = FormattedExcinfo(style="short") + reprtb = p.repr_traceback_entry(excinfo.traceback[-2]) + lines = reprtb.lines + last_p = FormattedExcinfo(style="short") + last_reprtb = last_p.repr_traceback_entry(excinfo.traceback[-1], excinfo) + last_lines = last_reprtb.lines + monkeypatch.undo() + assert lines[0] == ' func1()' + + assert last_lines[0] == ' raise ValueError("hello")' + assert last_lines[1] == 'E ValueError: hello' + + def test_repr_traceback_and_excinfo(self, importasmod): + mod = importasmod(""" + def f(x): + raise ValueError(x) + def entry(): + f(0) + """) + excinfo = pytest.raises(ValueError, mod.entry) + + for style in ("long", "short"): + p = FormattedExcinfo(style=style) + reprtb = p.repr_traceback(excinfo) + assert len(reprtb.reprentries) == 2 + assert reprtb.style == style + assert not reprtb.extraline + repr = p.repr_excinfo(excinfo) + assert repr.reprtraceback + assert len(repr.reprtraceback.reprentries) == len(reprtb.reprentries) + assert repr.reprcrash.path.endswith("mod.py") + assert repr.reprcrash.message == "ValueError: 0" + + def test_repr_traceback_with_invalid_cwd(self, importasmod, monkeypatch): + mod = importasmod(""" + def f(x): + raise ValueError(x) + def entry(): + f(0) + """) + excinfo = pytest.raises(ValueError, mod.entry) + + p = FormattedExcinfo() + def raiseos(): + raise OSError(2) + monkeypatch.setattr(py.std.os, 'getcwd', raiseos) + assert p._makepath(__file__) == __file__ + p.repr_traceback(excinfo) + + def test_repr_excinfo_addouterr(self, importasmod): + mod = importasmod(""" + def entry(): + raise ValueError() + """) + excinfo = pytest.raises(ValueError, mod.entry) + repr = excinfo.getrepr() + repr.addsection("title", "content") + twmock = TWMock() + repr.toterminal(twmock) + assert twmock.lines[-1] == "content" + assert twmock.lines[-2] == ("-", "title") + + def test_repr_excinfo_reprcrash(self, importasmod): + mod = importasmod(""" + def entry(): + raise ValueError() + """) + excinfo = pytest.raises(ValueError, mod.entry) + repr = excinfo.getrepr() + assert repr.reprcrash.path.endswith("mod.py") + assert repr.reprcrash.lineno == 3 + assert repr.reprcrash.message == "ValueError" + assert str(repr.reprcrash).endswith("mod.py:3: ValueError") + + def test_repr_traceback_recursion(self, importasmod): + mod = importasmod(""" + def rec2(x): + return rec1(x+1) + def rec1(x): + return rec2(x-1) + def entry(): + rec1(42) + """) + excinfo = pytest.raises(RuntimeError, mod.entry) + + for style in ("short", "long", "no"): + p = FormattedExcinfo(style="short") + reprtb = p.repr_traceback(excinfo) + assert reprtb.extraline == "!!! Recursion detected (same locals & position)" + assert str(reprtb) + + def test_tb_entry_AssertionError(self, importasmod): + # probably this test is a bit redundant + # as py/magic/testing/test_assertion.py + # already tests correctness of + # assertion-reinterpretation logic + mod = importasmod(""" + def somefunc(): + x = 1 + assert x == 2 + """) + excinfo = pytest.raises(AssertionError, mod.somefunc) + + p = FormattedExcinfo() + reprentry = p.repr_traceback_entry(excinfo.traceback[-1], excinfo) + lines = reprentry.lines + assert lines[-1] == "E assert 1 == 2" + + def test_reprexcinfo_getrepr(self, importasmod): + mod = importasmod(""" + def f(x): + raise ValueError(x) + def entry(): + f(0) + """) + excinfo = pytest.raises(ValueError, mod.entry) + + for style in ("short", "long", "no"): + for showlocals in (True, False): + repr = excinfo.getrepr(style=style, showlocals=showlocals) + assert isinstance(repr, ReprExceptionInfo) + assert repr.reprtraceback.style == style + + def test_reprexcinfo_unicode(self): + from _pytest._code.code import TerminalRepr + class MyRepr(TerminalRepr): + def toterminal(self, tw): + tw.line(py.builtin._totext("я", "utf-8")) + x = py.builtin._totext(MyRepr()) + assert x == py.builtin._totext("я", "utf-8") + + def test_toterminal_long(self, importasmod): + mod = importasmod(""" + def g(x): + raise ValueError(x) + def f(): + g(3) + """) + excinfo = pytest.raises(ValueError, mod.f) + excinfo.traceback = excinfo.traceback.filter() + repr = excinfo.getrepr() + tw = TWMock() + repr.toterminal(tw) + assert tw.lines[0] == "" + tw.lines.pop(0) + assert tw.lines[0] == " def f():" + assert tw.lines[1] == "> g(3)" + assert tw.lines[2] == "" + assert tw.lines[3].endswith("mod.py:5: ") + assert tw.lines[4] == ("_ ", None) + assert tw.lines[5] == "" + assert tw.lines[6] == " def g(x):" + assert tw.lines[7] == "> raise ValueError(x)" + assert tw.lines[8] == "E ValueError: 3" + assert tw.lines[9] == "" + assert tw.lines[10].endswith("mod.py:3: ValueError") + + def test_toterminal_long_missing_source(self, importasmod, tmpdir): + mod = importasmod(""" + def g(x): + raise ValueError(x) + def f(): + g(3) + """) + excinfo = pytest.raises(ValueError, mod.f) + tmpdir.join('mod.py').remove() + excinfo.traceback = excinfo.traceback.filter() + repr = excinfo.getrepr() + tw = TWMock() + repr.toterminal(tw) + assert tw.lines[0] == "" + tw.lines.pop(0) + assert tw.lines[0] == "> ???" + assert tw.lines[1] == "" + assert tw.lines[2].endswith("mod.py:5: ") + assert tw.lines[3] == ("_ ", None) + assert tw.lines[4] == "" + assert tw.lines[5] == "> ???" + assert tw.lines[6] == "E ValueError: 3" + assert tw.lines[7] == "" + assert tw.lines[8].endswith("mod.py:3: ValueError") + + def test_toterminal_long_incomplete_source(self, importasmod, tmpdir): + mod = importasmod(""" + def g(x): + raise ValueError(x) + def f(): + g(3) + """) + excinfo = pytest.raises(ValueError, mod.f) + tmpdir.join('mod.py').write('asdf') + excinfo.traceback = excinfo.traceback.filter() + repr = excinfo.getrepr() + tw = TWMock() + repr.toterminal(tw) + assert tw.lines[0] == "" + tw.lines.pop(0) + assert tw.lines[0] == "> ???" + assert tw.lines[1] == "" + assert tw.lines[2].endswith("mod.py:5: ") + assert tw.lines[3] == ("_ ", None) + assert tw.lines[4] == "" + assert tw.lines[5] == "> ???" + assert tw.lines[6] == "E ValueError: 3" + assert tw.lines[7] == "" + assert tw.lines[8].endswith("mod.py:3: ValueError") + + def test_toterminal_long_filenames(self, importasmod): + mod = importasmod(""" + def f(): + raise ValueError() + """) + excinfo = pytest.raises(ValueError, mod.f) + tw = TWMock() + path = py.path.local(mod.__file__) + old = path.dirpath().chdir() + try: + repr = excinfo.getrepr(abspath=False) + repr.toterminal(tw) + line = tw.lines[-1] + x = py.path.local().bestrelpath(path) + if len(x) < len(str(path)): + assert line == "mod.py:3: ValueError" + + repr = excinfo.getrepr(abspath=True) + repr.toterminal(tw) + line = tw.lines[-1] + assert line == "%s:3: ValueError" %(path,) + finally: + old.chdir() + + @pytest.mark.parametrize('reproptions', [ + {'style': style, 'showlocals': showlocals, + 'funcargs': funcargs, 'tbfilter': tbfilter + } for style in ("long", "short", "no") + for showlocals in (True, False) + for tbfilter in (True, False) + for funcargs in (True, False)]) + def test_format_excinfo(self, importasmod, reproptions): + mod = importasmod(""" + def g(x): + raise ValueError(x) + def f(): + g(3) + """) + excinfo = pytest.raises(ValueError, mod.f) + tw = py.io.TerminalWriter(stringio=True) + repr = excinfo.getrepr(**reproptions) + repr.toterminal(tw) + assert tw.stringio.getvalue() + + + def test_native_style(self): + excinfo = self.excinfo_from_exec(""" + assert 0 + """) + repr = excinfo.getrepr(style='native') + assert "assert 0" in str(repr.reprcrash) + s = str(repr) + assert s.startswith('Traceback (most recent call last):\n File') + assert s.endswith('\nAssertionError: assert 0') + assert 'exec (source.compile())' in s + # python 2.4 fails to get the source line for the assert + if py.std.sys.version_info >= (2, 5): + assert s.count('assert 0') == 2 + + def test_traceback_repr_style(self, importasmod): + mod = importasmod(""" + def f(): + g() + def g(): + h() + def h(): + i() + def i(): + raise ValueError() + """) + excinfo = pytest.raises(ValueError, mod.f) + excinfo.traceback = excinfo.traceback.filter() + excinfo.traceback[1].set_repr_style("short") + excinfo.traceback[2].set_repr_style("short") + r = excinfo.getrepr(style="long") + tw = TWMock() + r.toterminal(tw) + for line in tw.lines: print (line) + assert tw.lines[0] == "" + assert tw.lines[1] == " def f():" + assert tw.lines[2] == "> g()" + assert tw.lines[3] == "" + assert tw.lines[4].endswith("mod.py:3: ") + assert tw.lines[5] == ("_ ", None) + assert tw.lines[6].endswith("in g") + assert tw.lines[7] == " h()" + assert tw.lines[8].endswith("in h") + assert tw.lines[9] == " i()" + assert tw.lines[10] == ("_ ", None) + assert tw.lines[11] == "" + assert tw.lines[12] == " def i():" + assert tw.lines[13] == "> raise ValueError()" + assert tw.lines[14] == "E ValueError" + assert tw.lines[15] == "" + assert tw.lines[16].endswith("mod.py:9: ValueError") diff --git a/testing/code/test_source.py b/testing/code/test_source.py new file mode 100644 index 000000000..007ad1433 --- /dev/null +++ b/testing/code/test_source.py @@ -0,0 +1,659 @@ +# flake8: noqa +# disable flake check on this file because some constructs are strange +# or redundant on purpose and can't be disable on a line-by-line basis +import sys + +import _pytest._code +import py +import pytest +from _pytest._code import Source +from _pytest._code.source import _ast + +if _ast is not None: + astonly = pytest.mark.nothing +else: + astonly = pytest.mark.xfail("True", reason="only works with AST-compile") + +failsonjython = pytest.mark.xfail("sys.platform.startswith('java')") + +def test_source_str_function(): + x = Source("3") + assert str(x) == "3" + + x = Source(" 3") + assert str(x) == "3" + + x = Source(""" + 3 + """, rstrip=False) + assert str(x) == "\n3\n " + + x = Source(""" + 3 + """, rstrip=True) + assert str(x) == "\n3" + +def test_unicode(): + try: + unicode + except NameError: + return + x = Source(unicode("4")) + assert str(x) == "4" + co = _pytest._code.compile(unicode('u"\xc3\xa5"', 'utf8'), mode='eval') + val = eval(co) + assert isinstance(val, unicode) + +def test_source_from_function(): + source = _pytest._code.Source(test_source_str_function) + assert str(source).startswith('def test_source_str_function():') + +def test_source_from_method(): + class TestClass: + def test_method(self): + pass + source = _pytest._code.Source(TestClass().test_method) + assert source.lines == ["def test_method(self):", + " pass"] + +def test_source_from_lines(): + lines = ["a \n", "b\n", "c"] + source = _pytest._code.Source(lines) + assert source.lines == ['a ', 'b', 'c'] + +def test_source_from_inner_function(): + def f(): + pass + source = _pytest._code.Source(f, deindent=False) + assert str(source).startswith(' def f():') + source = _pytest._code.Source(f) + assert str(source).startswith('def f():') + +def test_source_putaround_simple(): + source = Source("raise ValueError") + source = source.putaround( + "try:", """\ + except ValueError: + x = 42 + else: + x = 23""") + assert str(source)=="""\ +try: + raise ValueError +except ValueError: + x = 42 +else: + x = 23""" + +def test_source_putaround(): + source = Source() + source = source.putaround(""" + if 1: + x=1 + """) + assert str(source).strip() == "if 1:\n x=1" + +def test_source_strips(): + source = Source("") + assert source == Source() + assert str(source) == '' + assert source.strip() == source + +def test_source_strip_multiline(): + source = Source() + source.lines = ["", " hello", " "] + source2 = source.strip() + assert source2.lines == [" hello"] + +def test_syntaxerror_rerepresentation(): + ex = pytest.raises(SyntaxError, _pytest._code.compile, 'xyz xyz') + assert ex.value.lineno == 1 + assert ex.value.offset in (4,7) # XXX pypy/jython versus cpython? + assert ex.value.text.strip(), 'x x' + +def test_isparseable(): + assert Source("hello").isparseable() + assert Source("if 1:\n pass").isparseable() + assert Source(" \nif 1:\n pass").isparseable() + assert not Source("if 1:\n").isparseable() + assert not Source(" \nif 1:\npass").isparseable() + assert not Source(chr(0)).isparseable() + +class TestAccesses: + source = Source("""\ + def f(x): + pass + def g(x): + pass + """) + def test_getrange(self): + x = self.source[0:2] + assert x.isparseable() + assert len(x.lines) == 2 + assert str(x) == "def f(x):\n pass" + + def test_getline(self): + x = self.source[0] + assert x == "def f(x):" + + def test_len(self): + assert len(self.source) == 4 + + def test_iter(self): + l = [x for x in self.source] + assert len(l) == 4 + +class TestSourceParsingAndCompiling: + source = Source("""\ + def f(x): + assert (x == + 3 + + 4) + """).strip() + + def test_compile(self): + co = _pytest._code.compile("x=3") + d = {} + exec (co, d) + assert d['x'] == 3 + + def test_compile_and_getsource_simple(self): + co = _pytest._code.compile("x=3") + exec (co) + source = _pytest._code.Source(co) + assert str(source) == "x=3" + + def test_compile_and_getsource_through_same_function(self): + def gensource(source): + return _pytest._code.compile(source) + co1 = gensource(""" + def f(): + raise KeyError() + """) + co2 = gensource(""" + def f(): + raise ValueError() + """) + source1 = py.std.inspect.getsource(co1) + assert 'KeyError' in source1 + source2 = py.std.inspect.getsource(co2) + assert 'ValueError' in source2 + + def test_getstatement(self): + #print str(self.source) + ass = str(self.source[1:]) + for i in range(1, 4): + #print "trying start in line %r" % self.source[i] + s = self.source.getstatement(i) + #x = s.deindent() + assert str(s) == ass + + def test_getstatementrange_triple_quoted(self): + #print str(self.source) + source = Source("""hello(''' + ''')""") + s = source.getstatement(0) + assert s == str(source) + s = source.getstatement(1) + assert s == str(source) + + @astonly + def test_getstatementrange_within_constructs(self): + source = Source("""\ + try: + try: + raise ValueError + except SomeThing: + pass + finally: + 42 + """) + assert len(source) == 7 + # check all lineno's that could occur in a traceback + #assert source.getstatementrange(0) == (0, 7) + #assert source.getstatementrange(1) == (1, 5) + assert source.getstatementrange(2) == (2, 3) + assert source.getstatementrange(3) == (3, 4) + assert source.getstatementrange(4) == (4, 5) + #assert source.getstatementrange(5) == (0, 7) + assert source.getstatementrange(6) == (6, 7) + + def test_getstatementrange_bug(self): + source = Source("""\ + try: + x = ( + y + + z) + except: + pass + """) + assert len(source) == 6 + assert source.getstatementrange(2) == (1, 4) + + def test_getstatementrange_bug2(self): + source = Source("""\ + assert ( + 33 + == + [ + X(3, + b=1, c=2 + ), + ] + ) + """) + assert len(source) == 9 + assert source.getstatementrange(5) == (0, 9) + + def test_getstatementrange_ast_issue58(self): + source = Source("""\ + + def test_some(): + for a in [a for a in + CAUSE_ERROR]: pass + + x = 3 + """) + assert getstatement(2, source).lines == source.lines[2:3] + assert getstatement(3, source).lines == source.lines[3:4] + + @pytest.mark.skipif("sys.version_info < (2,6)") + def test_getstatementrange_out_of_bounds_py3(self): + source = Source("if xxx:\n from .collections import something") + r = source.getstatementrange(1) + assert r == (1,2) + + def test_getstatementrange_with_syntaxerror_issue7(self): + source = Source(":") + pytest.raises(SyntaxError, lambda: source.getstatementrange(0)) + + @pytest.mark.skipif("sys.version_info < (2,6)") + def test_compile_to_ast(self): + import ast + source = Source("x = 4") + mod = source.compile(flag=ast.PyCF_ONLY_AST) + assert isinstance(mod, ast.Module) + compile(mod, "", "exec") + + def test_compile_and_getsource(self): + co = self.source.compile() + py.builtin.exec_(co, globals()) + f(7) + excinfo = pytest.raises(AssertionError, "f(6)") + frame = excinfo.traceback[-1].frame + stmt = frame.code.fullsource.getstatement(frame.lineno) + #print "block", str(block) + assert str(stmt).strip().startswith('assert') + + def test_compilefuncs_and_path_sanity(self): + def check(comp, name): + co = comp(self.source, name) + if not name: + expected = "codegen %s:%d>" %(mypath, mylineno+2+1) + else: + expected = "codegen %r %s:%d>" % (name, mypath, mylineno+2+1) + fn = co.co_filename + assert fn.endswith(expected) + + mycode = _pytest._code.Code(self.test_compilefuncs_and_path_sanity) + mylineno = mycode.firstlineno + mypath = mycode.path + + for comp in _pytest._code.compile, _pytest._code.Source.compile: + for name in '', None, 'my': + yield check, comp, name + + def test_offsetless_synerr(self): + pytest.raises(SyntaxError, _pytest._code.compile, "lambda a,a: 0", mode='eval') + +def test_getstartingblock_singleline(): + class A: + def __init__(self, *args): + frame = sys._getframe(1) + self.source = _pytest._code.Frame(frame).statement + + x = A('x', 'y') + + l = [i for i in x.source.lines if i.strip()] + assert len(l) == 1 + +def test_getstartingblock_multiline(): + class A: + def __init__(self, *args): + frame = sys._getframe(1) + self.source = _pytest._code.Frame(frame).statement + + x = A('x', + 'y' \ + , + 'z') + + l = [i for i in x.source.lines if i.strip()] + assert len(l) == 4 + +def test_getline_finally(): + def c(): pass + excinfo = pytest.raises(TypeError, """ + teardown = None + try: + c(1) + finally: + if teardown: + teardown() + """) + source = excinfo.traceback[-1].statement + assert str(source).strip() == 'c(1)' + +def test_getfuncsource_dynamic(): + source = """ + def f(): + raise ValueError + + def g(): pass + """ + co = _pytest._code.compile(source) + py.builtin.exec_(co, globals()) + assert str(_pytest._code.Source(f)).strip() == 'def f():\n raise ValueError' + assert str(_pytest._code.Source(g)).strip() == 'def g(): pass' + + +def test_getfuncsource_with_multine_string(): + def f(): + c = '''while True: + pass +''' + assert str(_pytest._code.Source(f)).strip() == "def f():\n c = '''while True:\n pass\n'''" + + +def test_deindent(): + from _pytest._code.source import deindent as deindent + assert deindent(['\tfoo', '\tbar', ]) == ['foo', 'bar'] + + def f(): + c = '''while True: + pass +''' + import inspect + lines = deindent(inspect.getsource(f).splitlines()) + assert lines == ["def f():", " c = '''while True:", " pass", "'''"] + + source = """ + def f(): + def g(): + pass + """ + lines = deindent(source.splitlines()) + assert lines == ['', 'def f():', ' def g():', ' pass', ' '] + +@pytest.mark.xfail("sys.version_info[:3] < (2,7,0) or " + "((3,0) <= sys.version_info[:2] < (3,2))") +def test_source_of_class_at_eof_without_newline(tmpdir): + # this test fails because the implicit inspect.getsource(A) below + # does not return the "x = 1" last line. + source = _pytest._code.Source(''' + class A(object): + def method(self): + x = 1 + ''') + path = tmpdir.join("a.py") + path.write(source) + s2 = _pytest._code.Source(tmpdir.join("a.py").pyimport().A) + assert str(source).strip() == str(s2).strip() + +if True: + def x(): + pass + +def test_getsource_fallback(): + from _pytest._code.source import getsource + expected = """def x(): + pass""" + src = getsource(x) + assert src == expected + +def test_idem_compile_and_getsource(): + from _pytest._code.source import getsource + expected = "def x(): pass" + co = _pytest._code.compile(expected) + src = getsource(co) + assert src == expected + +def test_findsource_fallback(): + from _pytest._code.source import findsource + src, lineno = findsource(x) + assert 'test_findsource_simple' in str(src) + assert src[lineno] == ' def x():' + +def test_findsource(): + from _pytest._code.source import findsource + co = _pytest._code.compile("""if 1: + def x(): + pass +""") + + src, lineno = findsource(co) + assert 'if 1:' in str(src) + + d = {} + eval(co, d) + src, lineno = findsource(d['x']) + assert 'if 1:' in str(src) + assert src[lineno] == " def x():" + + +def test_getfslineno(): + from _pytest._code import getfslineno + + def f(x): + pass + + fspath, lineno = getfslineno(f) + + assert fspath.basename == "test_source.py" + assert lineno == _pytest._code.getrawcode(f).co_firstlineno - 1 # see findsource + + class A(object): + pass + + fspath, lineno = getfslineno(A) + + _, A_lineno = py.std.inspect.findsource(A) + assert fspath.basename == "test_source.py" + assert lineno == A_lineno + + assert getfslineno(3) == ("", -1) + class B: + pass + B.__name__ = "B2" + assert getfslineno(B)[1] == -1 + +def test_code_of_object_instance_with_call(): + class A: + pass + pytest.raises(TypeError, lambda: _pytest._code.Source(A())) + class WithCall: + def __call__(self): + pass + + code = _pytest._code.Code(WithCall()) + assert 'pass' in str(code.source()) + + class Hello(object): + def __call__(self): + pass + pytest.raises(TypeError, lambda: _pytest._code.Code(Hello)) + + +def getstatement(lineno, source): + from _pytest._code.source import getstatementrange_ast + source = _pytest._code.Source(source, deindent=False) + ast, start, end = getstatementrange_ast(lineno, source) + return source[start:end] + +def test_oneline(): + source = getstatement(0, "raise ValueError") + assert str(source) == "raise ValueError" + +def test_comment_and_no_newline_at_end(): + from _pytest._code.source import getstatementrange_ast + source = Source(['def test_basic_complex():', + ' assert 1 == 2', + '# vim: filetype=pyopencl:fdm=marker']) + ast, start, end = getstatementrange_ast(1, source) + assert end == 2 + +def test_oneline_and_comment(): + source = getstatement(0, "raise ValueError\n#hello") + assert str(source) == "raise ValueError" + +@pytest.mark.xfail(hasattr(sys, "pypy_version_info"), + reason='does not work on pypy') +def test_comments(): + source = '''def test(): + "comment 1" + x = 1 + # comment 2 + # comment 3 + + assert False + +""" +comment 4 +""" +''' + for line in range(2,6): + assert str(getstatement(line, source)) == ' x = 1' + for line in range(6,10): + assert str(getstatement(line, source)) == ' assert False' + assert str(getstatement(10, source)) == '"""' + +def test_comment_in_statement(): + source = '''test(foo=1, + # comment 1 + bar=2) +''' + for line in range(1,3): + assert str(getstatement(line, source)) == \ + 'test(foo=1,\n # comment 1\n bar=2)' + +def test_single_line_else(): + source = getstatement(1, "if False: 2\nelse: 3") + assert str(source) == "else: 3" + +def test_single_line_finally(): + source = getstatement(1, "try: 1\nfinally: 3") + assert str(source) == "finally: 3" + +def test_issue55(): + source = ('def round_trip(dinp):\n assert 1 == dinp\n' + 'def test_rt():\n round_trip("""\n""")\n') + s = getstatement(3, source) + assert str(s) == ' round_trip("""\n""")' + + +def XXXtest_multiline(): + source = getstatement(0, """\ +raise ValueError( + 23 +) +x = 3 +""") + assert str(source) == "raise ValueError(\n 23\n)" + +class TestTry: + pytestmark = astonly + source = """\ +try: + raise ValueError +except Something: + raise IndexError(1) +else: + raise KeyError() +""" + + def test_body(self): + source = getstatement(1, self.source) + assert str(source) == " raise ValueError" + + def test_except_line(self): + source = getstatement(2, self.source) + assert str(source) == "except Something:" + + def test_except_body(self): + source = getstatement(3, self.source) + assert str(source) == " raise IndexError(1)" + + def test_else(self): + source = getstatement(5, self.source) + assert str(source) == " raise KeyError()" + +class TestTryFinally: + source = """\ +try: + raise ValueError +finally: + raise IndexError(1) +""" + + def test_body(self): + source = getstatement(1, self.source) + assert str(source) == " raise ValueError" + + def test_finally(self): + source = getstatement(3, self.source) + assert str(source) == " raise IndexError(1)" + + + +class TestIf: + pytestmark = astonly + source = """\ +if 1: + y = 3 +elif False: + y = 5 +else: + y = 7 +""" + + def test_body(self): + source = getstatement(1, self.source) + assert str(source) == " y = 3" + + def test_elif_clause(self): + source = getstatement(2, self.source) + assert str(source) == "elif False:" + + def test_elif(self): + source = getstatement(3, self.source) + assert str(source) == " y = 5" + + def test_else(self): + source = getstatement(5, self.source) + assert str(source) == " y = 7" + +def test_semicolon(): + s = """\ +hello ; pytest.skip() +""" + source = getstatement(0, s) + assert str(source) == s.strip() + +def test_def_online(): + s = """\ +def func(): raise ValueError(42) + +def something(): + pass +""" + source = getstatement(0, s) + assert str(source) == "def func(): raise ValueError(42)" + +def XXX_test_expression_multiline(): + source = """\ +something +''' +'''""" + result = getstatement(1, source) + assert str(result) == "'''\n'''" + diff --git a/testing/python/collect.py b/testing/python/collect.py index bebc13318..752cd81e3 100644 --- a/testing/python/collect.py +++ b/testing/python/collect.py @@ -1,6 +1,9 @@ import sys from textwrap import dedent -import pytest, py + +import _pytest._code +import py +import pytest from _pytest.main import EXIT_NOTESTSCOLLECTED @@ -598,13 +601,13 @@ class TestConftestCustomization: def test_customized_pymakemodule_issue205_subdir(self, testdir): b = testdir.mkdir("a").mkdir("b") - b.join("conftest.py").write(py.code.Source(""" + b.join("conftest.py").write(_pytest._code.Source(""" def pytest_pycollect_makemodule(__multicall__): mod = __multicall__.execute() mod.obj.hello = "world" return mod """)) - b.join("test_module.py").write(py.code.Source(""" + b.join("test_module.py").write(_pytest._code.Source(""" def test_hello(): assert hello == "world" """)) @@ -613,7 +616,7 @@ class TestConftestCustomization: def test_customized_pymakeitem(self, testdir): b = testdir.mkdir("a").mkdir("b") - b.join("conftest.py").write(py.code.Source(""" + b.join("conftest.py").write(_pytest._code.Source(""" import pytest @pytest.hookimpl(hookwrapper=True) def pytest_pycollect_makeitem(): @@ -624,7 +627,7 @@ class TestConftestCustomization: for func in result: func._some123 = "world" """)) - b.join("test_module.py").write(py.code.Source(""" + b.join("test_module.py").write(_pytest._code.Source(""" import pytest @pytest.fixture() @@ -662,7 +665,7 @@ class TestConftestCustomization: def test_setup_only_available_in_subdir(testdir): sub1 = testdir.mkpydir("sub1") sub2 = testdir.mkpydir("sub2") - sub1.join("conftest.py").write(py.code.Source(""" + sub1.join("conftest.py").write(_pytest._code.Source(""" import pytest def pytest_runtest_setup(item): assert item.fspath.purebasename == "test_in_sub1" @@ -671,7 +674,7 @@ def test_setup_only_available_in_subdir(testdir): def pytest_runtest_teardown(item): assert item.fspath.purebasename == "test_in_sub1" """)) - sub2.join("conftest.py").write(py.code.Source(""" + sub2.join("conftest.py").write(_pytest._code.Source(""" import pytest def pytest_runtest_setup(item): assert item.fspath.purebasename == "test_in_sub2" @@ -787,7 +790,7 @@ class TestTracebackCutting: except ValueError: _, _, tb = sys.exc_info() - tb = py.code.Traceback(tb) + tb = _pytest._code.Traceback(tb) assert isinstance(tb[-1].path, str) assert not filter_traceback(tb[-1]) @@ -810,7 +813,7 @@ class TestTracebackCutting: _, _, tb = sys.exc_info() testdir.tmpdir.join('filter_traceback_entry_as_str.py').remove() - tb = py.code.Traceback(tb) + tb = _pytest._code.Traceback(tb) assert isinstance(tb[-1].path, str) assert filter_traceback(tb[-1]) diff --git a/testing/python/fixture.py b/testing/python/fixture.py index 203176443..506d8426e 100644 --- a/testing/python/fixture.py +++ b/testing/python/fixture.py @@ -1,9 +1,13 @@ -import pytest, py, sys -from _pytest import python as funcargs -from _pytest.python import FixtureLookupError -from _pytest.pytester import get_public_names from textwrap import dedent +import _pytest._code +import pytest +import sys +from _pytest import python as funcargs +from _pytest.pytester import get_public_names +from _pytest.python import FixtureLookupError + + def test_getfuncargnames(): def f(): pass assert not funcargs.getfuncargnames(f) @@ -86,12 +90,12 @@ class TestFillFixtures: def test_conftest_funcargs_only_available_in_subdir(self, testdir): sub1 = testdir.mkpydir("sub1") sub2 = testdir.mkpydir("sub2") - sub1.join("conftest.py").write(py.code.Source(""" + sub1.join("conftest.py").write(_pytest._code.Source(""" import pytest def pytest_funcarg__arg1(request): pytest.raises(Exception, "request.getfuncargvalue('arg2')") """)) - sub2.join("conftest.py").write(py.code.Source(""" + sub2.join("conftest.py").write(_pytest._code.Source(""" import pytest def pytest_funcarg__arg2(request): pytest.raises(Exception, "request.getfuncargvalue('arg1')") @@ -156,7 +160,7 @@ class TestFillFixtures: return 'spam' """) pkg = testdir.mkpydir("pkg") - pkg.join("conftest.py").write(py.code.Source(""" + pkg.join("conftest.py").write(_pytest._code.Source(""" import pytest @pytest.fixture @@ -164,7 +168,7 @@ class TestFillFixtures: return spam * 2 """)) testfile = pkg.join("test_spam.py") - testfile.write(py.code.Source(""" + testfile.write(_pytest._code.Source(""" def test_spam(spam): assert spam == "spamspam" """)) @@ -258,7 +262,7 @@ class TestFillFixtures: return request.param """) subdir = testdir.mkpydir('subdir') - subdir.join("conftest.py").write(py.code.Source(""" + subdir.join("conftest.py").write(_pytest._code.Source(""" import pytest @pytest.fixture @@ -266,7 +270,7 @@ class TestFillFixtures: return 'spam' """)) testfile = subdir.join("test_spam.py") - testfile.write(py.code.Source(""" + testfile.write(_pytest._code.Source(""" def test_spam(spam): assert spam == "spam" """)) @@ -312,7 +316,7 @@ class TestFillFixtures: return 'spam' """) subdir = testdir.mkpydir('subdir') - subdir.join("conftest.py").write(py.code.Source(""" + subdir.join("conftest.py").write(_pytest._code.Source(""" import pytest @pytest.fixture(params=[1, 2, 3]) @@ -320,7 +324,7 @@ class TestFillFixtures: return request.param """)) testfile = subdir.join("test_spam.py") - testfile.write(py.code.Source(""" + testfile.write(_pytest._code.Source(""" params = {'spam': 1} def test_spam(spam): @@ -609,7 +613,7 @@ class TestRequestBasic: def test_fixtures_sub_subdir_normalize_sep(self, testdir): # this tests that normalization of nodeids takes place b = testdir.mkdir("tests").mkdir("unit") - b.join("conftest.py").write(py.code.Source(""" + b.join("conftest.py").write(_pytest._code.Source(""" def pytest_funcarg__arg1(): pass """)) @@ -1349,7 +1353,7 @@ class TestAutouseDiscovery: class TestAutouseManagement: def test_autouse_conftest_mid_directory(self, testdir): pkgdir = testdir.mkpydir("xyz123") - pkgdir.join("conftest.py").write(py.code.Source(""" + pkgdir.join("conftest.py").write(_pytest._code.Source(""" import pytest @pytest.fixture(autouse=True) def app(): @@ -1357,7 +1361,7 @@ class TestAutouseManagement: sys._myapp = "hello" """)) t = pkgdir.ensure("tests", "test_app.py") - t.write(py.code.Source(""" + t.write(_pytest._code.Source(""" import sys def test_app(): assert sys._myapp == "hello" diff --git a/testing/python/integration.py b/testing/python/integration.py index 0c436e32b..dea86f942 100644 --- a/testing/python/integration.py +++ b/testing/python/integration.py @@ -1,6 +1,7 @@ import pytest -from _pytest import runner from _pytest import python +from _pytest import runner + class TestOEJSKITSpecials: def test_funcarg_non_pycollectobj(self, testdir): # rough jstests usage diff --git a/testing/python/metafunc.py b/testing/python/metafunc.py index 111ca615a..21869cff9 100644 --- a/testing/python/metafunc.py +++ b/testing/python/metafunc.py @@ -1,7 +1,9 @@ # -*- coding: utf-8 -*- import re -import pytest, py +import _pytest._code +import py +import pytest from _pytest import python as funcargs class TestMetafunc: @@ -838,11 +840,11 @@ class TestMetafuncFunctional: def test_generate_tests_only_done_in_subdir(self, testdir): sub1 = testdir.mkpydir("sub1") sub2 = testdir.mkpydir("sub2") - sub1.join("conftest.py").write(py.code.Source(""" + sub1.join("conftest.py").write(_pytest._code.Source(""" def pytest_generate_tests(metafunc): assert metafunc.function.__name__ == "test_1" """)) - sub2.join("conftest.py").write(py.code.Source(""" + sub2.join("conftest.py").write(_pytest._code.Source(""" def pytest_generate_tests(metafunc): assert metafunc.function.__name__ == "test_2" """)) diff --git a/testing/python/raises.py b/testing/python/raises.py index edeb52226..0370f982b 100644 --- a/testing/python/raises.py +++ b/testing/python/raises.py @@ -38,10 +38,11 @@ class TestRaises: testdir.makepyfile(""" from __future__ import with_statement import py, pytest + import _pytest._code def test_simple(): with pytest.raises(ZeroDivisionError) as excinfo: - assert isinstance(excinfo, py.code.ExceptionInfo) + assert isinstance(excinfo, _pytest._code.ExceptionInfo) 1/0 print (excinfo) assert excinfo.type == ZeroDivisionError diff --git a/testing/test_assertinterpret.py b/testing/test_assertinterpret.py index 44b4f23c4..67a352ce7 100644 --- a/testing/test_assertinterpret.py +++ b/testing/test_assertinterpret.py @@ -1,8 +1,9 @@ "PYTEST_DONT_REWRITE" -import pytest, py - +import py +import pytest from _pytest.assertion import util + def exvalue(): return py.std.sys.exc_info()[1] diff --git a/testing/test_assertion.py b/testing/test_assertion.py index 34b900f64..f259f7fa1 100644 --- a/testing/test_assertion.py +++ b/testing/test_assertion.py @@ -2,8 +2,10 @@ import sys import textwrap -import py, pytest import _pytest.assertion as plugin +import _pytest._code +import py +import pytest from _pytest.assertion import reinterpret from _pytest.assertion import util @@ -22,7 +24,7 @@ def mock_config(): def interpret(expr): - return reinterpret.reinterpret(expr, py.code.Frame(sys._getframe(1))) + return reinterpret.reinterpret(expr, _pytest._code.Frame(sys._getframe(1))) class TestBinReprIntegration: diff --git a/testing/test_assertrewrite.py b/testing/test_assertrewrite.py index 544250ad5..d06cb3a00 100644 --- a/testing/test_assertrewrite.py +++ b/testing/test_assertrewrite.py @@ -10,6 +10,7 @@ if sys.platform.startswith("java"): # XXX should be xfail pytest.skip("assert rewrite does currently not work on jython") +import _pytest._code from _pytest.assertion import util from _pytest.assertion.rewrite import rewrite_asserts, PYTEST_TAG from _pytest.main import EXIT_NOTESTSCOLLECTED @@ -17,7 +18,7 @@ from _pytest.main import EXIT_NOTESTSCOLLECTED def setup_module(mod): mod._old_reprcompare = util._reprcompare - py.code._reprcompare = None + _pytest._code._reprcompare = None def teardown_module(mod): util._reprcompare = mod._old_reprcompare @@ -31,7 +32,7 @@ def rewrite(src): def getmsg(f, extra_ns=None, must_pass=False): """Rewrite the assertions in f, run it, and get the failure message.""" - src = '\n'.join(py.code.Code(f).source().lines) + src = '\n'.join(_pytest._code.Code(f).source().lines) mod = rewrite(src) code = compile(mod, "", "exec") ns = {} @@ -669,7 +670,7 @@ class TestAssertionRewriteHookDetails(object): """Implement optional PEP302 api (#808). """ path = testdir.mkpydir("foo") - path.join("test_foo.py").write(py.code.Source(""" + path.join("test_foo.py").write(_pytest._code.Source(""" class Test: def test_foo(self): import pkgutil diff --git a/testing/test_cache.py b/testing/test_cache.py index adac4a1a6..75557af38 100755 --- a/testing/test_cache.py +++ b/testing/test_cache.py @@ -1,8 +1,9 @@ import sys + +import _pytest import pytest import os import shutil -import py pytest_plugins = "pytester", @@ -129,6 +130,7 @@ def test_cache_show(testdir): class TestLastFailed: + def test_lastfailed_usecase(self, testdir, monkeypatch): monkeypatch.setenv("PYTHONDONTWRITEBYTECODE", 1) p = testdir.makepyfile(""" @@ -143,7 +145,7 @@ class TestLastFailed: result.stdout.fnmatch_lines([ "*2 failed*", ]) - p.write(py.code.Source(""" + p.write(_pytest._code.Source(""" def test_1(): assert 1 @@ -175,11 +177,11 @@ class TestLastFailed: ]) def test_failedfirst_order(self, testdir): - testdir.tmpdir.join('test_a.py').write(py.code.Source(""" + testdir.tmpdir.join('test_a.py').write(_pytest._code.Source(""" def test_always_passes(): assert 1 """)) - testdir.tmpdir.join('test_b.py').write(py.code.Source(""" + testdir.tmpdir.join('test_b.py').write(_pytest._code.Source(""" def test_always_fails(): assert 0 """)) @@ -218,7 +220,7 @@ class TestLastFailed: result.stdout.fnmatch_lines([ "*1 failed*", ]) - p2.write(py.code.Source(""" + p2.write(_pytest._code.Source(""" def test_b1(): assert 1 """)) @@ -238,7 +240,7 @@ class TestLastFailed: assert 0 """) p2 = testdir.tmpdir.join("test_something.py") - p2.write(py.code.Source(""" + p2.write(_pytest._code.Source(""" def test_2(): assert 0 """)) diff --git a/testing/test_capture.py b/testing/test_capture.py index b5b374a72..73660692b 100644 --- a/testing/test_capture.py +++ b/testing/test_capture.py @@ -4,6 +4,8 @@ from __future__ import with_statement import pickle import os import sys + +import _pytest._code import py import pytest import contextlib @@ -481,7 +483,7 @@ class TestCaptureFixture: def test_setup_failure_does_not_kill_capturing(testdir): sub1 = testdir.mkpydir("sub1") - sub1.join("conftest.py").write(py.code.Source(""" + sub1.join("conftest.py").write(_pytest._code.Source(""" def pytest_runtest_setup(item): raise ValueError(42) """)) diff --git a/testing/test_config.py b/testing/test_config.py index e818dff38..5a984b35d 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -1,5 +1,6 @@ import py, pytest +import _pytest._code from _pytest.config import getcfg, get_common_ancestor, determine_setup from _pytest.main import EXIT_NOTESTSCOLLECTED @@ -7,7 +8,7 @@ class TestParseIni: def test_getcfg_and_config(self, testdir, tmpdir): sub = tmpdir.mkdir("sub") sub.chdir() - tmpdir.join("setup.cfg").write(py.code.Source(""" + tmpdir.join("setup.cfg").write(_pytest._code.Source(""" [pytest] name = value """)) @@ -21,7 +22,7 @@ class TestParseIni: def test_append_parse_args(self, testdir, tmpdir, monkeypatch): monkeypatch.setenv('PYTEST_ADDOPTS', '--color no -rs --tb="short"') - tmpdir.join("setup.cfg").write(py.code.Source(""" + tmpdir.join("setup.cfg").write(_pytest._code.Source(""" [pytest] addopts = --verbose """)) @@ -296,7 +297,7 @@ class TestConfigFromdictargs: assert config.option.capture == 'no' def test_inifilename(self, tmpdir): - tmpdir.join("foo/bar.ini").ensure().write(py.code.Source(""" + tmpdir.join("foo/bar.ini").ensure().write(_pytest._code.Source(""" [pytest] name = value """)) @@ -309,7 +310,7 @@ class TestConfigFromdictargs: } cwd = tmpdir.join('a/b') - cwd.join('pytest.ini').ensure().write(py.code.Source(""" + cwd.join('pytest.ini').ensure().write(_pytest._code.Source(""" [pytest] name = wrong-value should_not_be_set = true diff --git a/testing/test_conftest.py b/testing/test_conftest.py index a0b77cfa5..6f5e77f6d 100644 --- a/testing/test_conftest.py +++ b/testing/test_conftest.py @@ -1,5 +1,8 @@ from textwrap import dedent -import py, pytest + +import _pytest._code +import py +import pytest from _pytest.config import PytestPluginManager from _pytest.main import EXIT_NOTESTSCOLLECTED, EXIT_USAGEERROR @@ -156,7 +159,7 @@ def test_setinitial_conftest_subdirs(testdir, name): def test_conftest_confcutdir(testdir): testdir.makeconftest("assert 0") x = testdir.mkdir("x") - x.join("conftest.py").write(py.code.Source(""" + x.join("conftest.py").write(_pytest._code.Source(""" def pytest_addoption(parser): parser.addoption("--xyz", action="store_true") """)) @@ -174,7 +177,7 @@ def test_no_conftest(testdir): def test_conftest_existing_resultlog(testdir): x = testdir.mkdir("tests") - x.join("conftest.py").write(py.code.Source(""" + x.join("conftest.py").write(_pytest._code.Source(""" def pytest_addoption(parser): parser.addoption("--xyz", action="store_true") """)) @@ -184,7 +187,7 @@ def test_conftest_existing_resultlog(testdir): def test_conftest_existing_junitxml(testdir): x = testdir.mkdir("tests") - x.join("conftest.py").write(py.code.Source(""" + x.join("conftest.py").write(_pytest._code.Source(""" def pytest_addoption(parser): parser.addoption("--xyz", action="store_true") """)) @@ -361,18 +364,18 @@ def test_search_conftest_up_to_inifile(testdir, confcutdir, passed, error): root = testdir.tmpdir src = root.join('src').ensure(dir=1) src.join('pytest.ini').write('[pytest]') - src.join('conftest.py').write(py.code.Source(""" + src.join('conftest.py').write(_pytest._code.Source(""" import pytest @pytest.fixture def fix1(): pass """)) - src.join('test_foo.py').write(py.code.Source(""" + src.join('test_foo.py').write(_pytest._code.Source(""" def test_1(fix1): pass def test_2(out_of_reach): pass """)) - root.join('conftest.py').write(py.code.Source(""" + root.join('conftest.py').write(_pytest._code.Source(""" import pytest @pytest.fixture def out_of_reach(): pass diff --git a/testing/test_doctest.py b/testing/test_doctest.py index b05fef478..a4821ee4c 100644 --- a/testing/test_doctest.py +++ b/testing/test_doctest.py @@ -1,7 +1,7 @@ # encoding: utf-8 import sys +import _pytest._code from _pytest.doctest import DoctestItem, DoctestModule, DoctestTextfile -import py import pytest class TestDoctests: @@ -181,7 +181,7 @@ class TestDoctests: assert 'text-line-after' not in result.stdout.str() def test_doctest_linedata_missing(self, testdir): - testdir.tmpdir.join('hello.py').write(py.code.Source(""" + testdir.tmpdir.join('hello.py').write(_pytest._code.Source(""" class Fun(object): @property def test(self): @@ -201,7 +201,7 @@ class TestDoctests: def test_doctest_unex_importerror(self, testdir): - testdir.tmpdir.join("hello.py").write(py.code.Source(""" + testdir.tmpdir.join("hello.py").write(_pytest._code.Source(""" import asdalsdkjaslkdjasd """)) testdir.maketxtfile(""" @@ -229,7 +229,7 @@ class TestDoctests: def test_doctestmodule_external_and_issue116(self, testdir): p = testdir.mkpydir("hello") - p.join("__init__.py").write(py.code.Source(""" + p.join("__init__.py").write(_pytest._code.Source(""" def somefunc(): ''' >>> i = 0 diff --git a/testing/test_pdb.py b/testing/test_pdb.py index 092b1684e..eeddcf0ae 100644 --- a/testing/test_pdb.py +++ b/testing/test_pdb.py @@ -1,7 +1,8 @@ - -import py import sys +import _pytest._code + + def runpdb_and_get_report(testdir, source): p = testdir.makepyfile(source) result = testdir.runpytest_inprocess("--pdb", p) @@ -27,7 +28,7 @@ class TestPDB: """) assert rep.failed assert len(pdblist) == 1 - tb = py.code.Traceback(pdblist[0][0]) + tb = _pytest._code.Traceback(pdblist[0][0]) assert tb[-1].name == "test_func" def test_pdb_on_xfail(self, testdir, pdblist): diff --git a/testing/test_resultlog.py b/testing/test_resultlog.py index ef1d6d040..74d13f643 100644 --- a/testing/test_resultlog.py +++ b/testing/test_resultlog.py @@ -1,8 +1,12 @@ -import py, pytest import os + +import _pytest._code +import py +import pytest +from _pytest.main import Node, Item, FSCollector from _pytest.resultlog import generic_path, ResultLog, \ pytest_configure, pytest_unconfigure -from _pytest.main import Node, Item, FSCollector + def test_generic_path(testdir): from _pytest.main import Session @@ -140,7 +144,7 @@ class TestWithFunctionIntegration: try: raise ValueError except ValueError: - excinfo = py.code.ExceptionInfo() + excinfo = _pytest._code.ExceptionInfo() reslog = ResultLog(None, py.io.TextIO()) reslog.pytest_internalerror(excinfo.getrepr(style=style)) entry = reslog.logfile.getvalue() diff --git a/testing/test_runner.py b/testing/test_runner.py index b01727dbb..6f4a0cee3 100644 --- a/testing/test_runner.py +++ b/testing/test_runner.py @@ -1,6 +1,10 @@ from __future__ import with_statement -import pytest, py, sys, os +import _pytest._code +import os +import py +import pytest +import sys from _pytest import runner, main class TestSetupState: @@ -408,14 +412,14 @@ def test_pytest_exit(): try: pytest.exit("hello") except pytest.exit.Exception: - excinfo = py.code.ExceptionInfo() + excinfo = _pytest._code.ExceptionInfo() assert excinfo.errisinstance(KeyboardInterrupt) def test_pytest_fail(): try: pytest.fail("hello") except pytest.fail.Exception: - excinfo = py.code.ExceptionInfo() + excinfo = _pytest._code.ExceptionInfo() s = excinfo.exconly(tryshort=True) assert s.startswith("Failed") @@ -459,7 +463,7 @@ def test_exception_printing_skip(): try: pytest.skip("hello") except pytest.skip.Exception: - excinfo = py.code.ExceptionInfo() + excinfo = _pytest._code.ExceptionInfo() s = excinfo.exconly(tryshort=True) assert s.startswith("Skipped") @@ -488,7 +492,7 @@ def test_importorskip(monkeypatch): mod2 = pytest.importorskip("hello123", minversion="1.3") assert mod2 == mod except pytest.skip.Exception: - print(py.code.ExceptionInfo()) + print(_pytest._code.ExceptionInfo()) pytest.fail("spurious skip") def test_importorskip_imports_last_module_part(): @@ -505,7 +509,7 @@ def test_importorskip_dev_module(monkeypatch): pytest.raises(pytest.skip.Exception, """ pytest.importorskip('mockmodule1', minversion='0.14.0')""") except pytest.skip.Exception: - print(py.code.ExceptionInfo()) + print(_pytest._code.ExceptionInfo()) pytest.fail("spurious skip") diff --git a/testing/test_terminal.py b/testing/test_terminal.py index b5166a22e..a898d9553 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -2,15 +2,17 @@ terminal reporting of the full testing process. """ import collections -import pytest -import py import sys +import _pytest._pluggy as pluggy +import _pytest._code +import py +import pytest +from _pytest import runner from _pytest.main import EXIT_NOTESTSCOLLECTED from _pytest.terminal import TerminalReporter, repr_pythonversion, getreportopt from _pytest.terminal import build_summary_stats_line, _plugin_nameversions -from _pytest import runner -import _pytest._pluggy as pluggy + def basic_run_report(item): runner.call_and_report(item, "setup", log=False) @@ -153,7 +155,7 @@ class TestTerminal: def test_itemreport_directclasses_not_shown_as_subclasses(self, testdir): a = testdir.mkpydir("a123") - a.join("test_hello123.py").write(py.code.Source(""" + a.join("test_hello123.py").write(_pytest._code.Source(""" class TestClass: def test_method(self): pass @@ -268,7 +270,7 @@ class TestCollectonly: p = testdir.makepyfile("import Errlkjqweqwe") result = testdir.runpytest("--collect-only", p) assert result.ret == 1 - result.stdout.fnmatch_lines(py.code.Source(""" + result.stdout.fnmatch_lines(_pytest._code.Source(""" *ERROR* *import Errlk* *ImportError* diff --git a/testing/test_unittest.py b/testing/test_unittest.py index 53dde6ea3..144aad79b 100644 --- a/testing/test_unittest.py +++ b/testing/test_unittest.py @@ -260,6 +260,7 @@ def test_testcase_custom_exception_info(testdir, type): testdir.makepyfile(""" from unittest import TestCase import py, pytest + import _pytest._code class MyTestCase(TestCase): def run(self, result): excinfo = pytest.raises(ZeroDivisionError, lambda: 0/0) @@ -269,7 +270,7 @@ def test_testcase_custom_exception_info(testdir, type): def t(*args): mp.undo() raise TypeError() - mp.setattr(py.code, 'ExceptionInfo', t) + mp.setattr(_pytest._code, 'ExceptionInfo', t) try: excinfo = excinfo._excinfo result.add%(type)s(self, excinfo)