""" Python related collection nodes. Here is an example of a tree of collectors and test items that this modules provides:: Module # File Class Instance Function Generator ... Function Generator Function DoctestFile # File DoctestFileContent # acts as Item """ import py from py.__.test.collect import configproperty, warnoldcollect from py.__.code.source import findsource pydir = py.path.local(py.__file__).dirpath() class PyobjMixin(object): def obj(): def fget(self): try: return self._obj except AttributeError: self._obj = obj = self._getobj() return obj def fset(self, value): self._obj = value return property(fget, fset, None, "underlying python object") obj = obj() def _getobj(self): return getattr(self.parent.obj, self.name) def getmodpath(self, stopatmodule=True, includemodule=False): """ return python path relative to the containing module. """ chain = self.listchain() chain.reverse() parts = [] for node in chain: if isinstance(node, Instance): continue name = node.name if isinstance(node, Module): assert name.endswith(".py") name = name[:-3] if stopatmodule: if includemodule: parts.append(name) break parts.append(name) parts.reverse() s = ".".join(parts) return s.replace(".[", "[") def getfslineno(self): try: return self._fslineno except AttributeError: pass obj = self.obj # let decorators etc specify a sane ordering if hasattr(obj, 'place_as'): obj = obj.place_as try: code = py.code.Code(obj) except TypeError: # fallback to fn = (py.std.inspect.getsourcefile(obj) or py.std.inspect.getfile(obj)) fspath = fn and py.path.local(fn) or None if fspath: try: _, lineno = findsource(obj) except IOError: lineno = None else: lineno = None else: fspath = code.path lineno = code.firstlineno self._fslineno = fspath, lineno return fspath, lineno def repr_metainfo(self): fspath, lineno = self.getfslineno() modpath = self.getmodpath() return self.ReprMetaInfo( fspath=fspath, lineno=lineno, modpath=modpath, ) class PyCollectorMixin(PyobjMixin, py.test.collect.Collector): Class = configproperty('Class') Instance = configproperty('Instance') Function = configproperty('Function') Generator = configproperty('Generator') def funcnamefilter(self, name): return name.startswith('test') def classnamefilter(self, name): return name.startswith('Test') def collect(self): l = self._deprecated_collect() if l is not None: return l name2items = self._buildname2items() colitems = name2items.values() colitems.sort() return colitems def _buildname2items(self): # NB. we avoid random getattrs and peek in the __dict__ instead d = {} dicts = [getattr(self.obj, '__dict__', {})] for basecls in py.std.inspect.getmro(self.obj.__class__): dicts.append(basecls.__dict__) seen = {} for dic in dicts: for name, obj in dic.items(): if name in seen: continue seen[name] = True res = self.makeitem(name, obj) if res is not None: d[name] = res return d def _deprecated_join(self, name): if self.__class__.join != py.test.collect.Collector.join: warnoldcollect() return self.join(name) def makeitem(self, name, obj): res = self.config.pytestplugins.call_firstresult( "pytest_pymodule_makeitem", modcol=self, name=name, obj=obj) if res: return res if (self.classnamefilter(name)) and \ py.std.inspect.isclass(obj): res = self._deprecated_join(name) if res is not None: return res return self.Class(name, parent=self) elif self.funcnamefilter(name) and callable(obj): res = self._deprecated_join(name) if res is not None: return res if obj.func_code.co_flags & 32: # generator function return self.Generator(name, parent=self) else: return self.Function(name, parent=self) class Module(py.test.collect.File, PyCollectorMixin): def _getobj(self): return self._memoizedcall('_obj', self._importtestmodule) def _importtestmodule(self): # we assume we are only called once per module mod = self.fspath.pyimport() #print "imported test module", mod self.config.pytestplugins.consider_module(mod) return mod def setup(self): if getattr(self.obj, 'disabled', 0): py.test.skip("%r is disabled" %(self.obj,)) if not self.config.option.nomagic: #print "*" * 20, "INVOKE assertion", self py.magic.invoke(assertion=1) mod = self.obj self.config.pytestplugins.register(mod) if hasattr(mod, 'setup_module'): self.obj.setup_module(mod) def teardown(self): if hasattr(self.obj, 'teardown_module'): self.obj.teardown_module(self.obj) if not self.config.option.nomagic: #print "*" * 20, "revoke assertion", self py.magic.revoke(assertion=1) self.config.pytestplugins.unregister(self.obj) class Class(PyCollectorMixin, py.test.collect.Collector): def collect(self): l = self._deprecated_collect() if l is not None: return l return [self.Instance(name="()", parent=self)] def setup(self): if getattr(self.obj, 'disabled', 0): py.test.skip("%r is disabled" %(self.obj,)) setup_class = getattr(self.obj, 'setup_class', None) if setup_class is not None: setup_class = getattr(setup_class, 'im_func', setup_class) setup_class(self.obj) def teardown(self): teardown_class = getattr(self.obj, 'teardown_class', None) if teardown_class is not None: teardown_class = getattr(teardown_class, 'im_func', teardown_class) teardown_class(self.obj) def _getsortvalue(self): return self.getfslineno() class Instance(PyCollectorMixin, py.test.collect.Collector): def _getobj(self): return self.parent.obj() def Function(self): return getattr(self.obj, 'Function', PyCollectorMixin.Function.__get__(self)) # XXX for python 2.2 def _keywords(self): return [] Function = property(Function) #def __repr__(self): # return "<%s of '%s'>" %(self.__class__.__name__, # self.parent.obj.__name__) def newinstance(self): self.obj = self._getobj() return self.obj class FunctionMixin(PyobjMixin): """ mixin for the code common to Function and Generator. """ def _getsortvalue(self): return self.getfslineno() def setup(self): """ perform setup for this test function. """ if hasattr(self.obj, 'im_self'): name = 'setup_method' else: name = 'setup_function' if isinstance(self.parent, Instance): obj = self.parent.newinstance() self.obj = self._getobj() else: obj = self.parent.obj setup_func_or_method = getattr(obj, name, None) if setup_func_or_method is not None: return setup_func_or_method(self.obj) def teardown(self): """ perform teardown for this test function. """ if hasattr(self.obj, 'im_self'): name = 'teardown_method' else: name = 'teardown_function' obj = self.parent.obj teardown_func_or_meth = getattr(obj, name, None) if teardown_func_or_meth is not None: teardown_func_or_meth(self.obj) def _prunetraceback(self, traceback): if hasattr(self, '_obj') and not self.config.option.fulltrace: code = py.code.Code(self.obj) path, firstlineno = code.path, code.firstlineno ntraceback = traceback.cut(path=path, firstlineno=firstlineno) if ntraceback == traceback: ntraceback = ntraceback.cut(path=path) if ntraceback == traceback: ntraceback = ntraceback.cut(excludepath=pydir) traceback = ntraceback.filter() return traceback def repr_failure(self, excinfo, outerr): return self._repr_failure_py(excinfo, outerr) shortfailurerepr = "F" class Generator(FunctionMixin, PyCollectorMixin, py.test.collect.Collector): def collect(self): # test generators are collectors yet participate in # the test-item setup and teardown protocol. # otherwise we could avoid global setupstate self.config._setupstate.prepare(self) l = [] seen = {} for i, x in py.builtin.enumerate(self.obj()): name, call, args = self.getcallargs(x) if not callable(call): raise TypeError("%r yielded non callable test %r" %(self.obj, call,)) if name is None: name = "[%d]" % i else: name = "['%s']" % name if name in seen: raise ValueError("%r generated tests with non-unique name %r" %(self, name)) seen[name] = True l.append(self.Function(name, self, args=args, callobj=call)) return l def getcallargs(self, obj): if not isinstance(obj, (tuple, list)): obj = (obj,) # explict naming if isinstance(obj[0], basestring): name = obj[0] obj = obj[1:] else: name = None call, args = obj[0], obj[1:] return name, call, args # # Test Items # _dummy = object() class Function(FunctionMixin, py.test.collect.Item): """ a Function Item is responsible for setting up and executing a Python callable test object. """ def __init__(self, name, parent=None, config=None, args=(), callobj=_dummy): super(Function, self).__init__(name, parent, config=config) self._finalizers = [] self._args = args if callobj is not _dummy: self._obj = callobj def addfinalizer(self, func): self._finalizers.append(func) def teardown(self): finalizers = self._finalizers while finalizers: call = finalizers.pop() call() super(Function, self).teardown() def readkeywords(self): d = super(Function, self).readkeywords() d.update(self.obj.func_dict) return d def runtest(self): """ execute the given test function. """ if not self._deprecated_testexecution(): kw = self.lookup_allargs() ret = self.config.pytestplugins.call_firstresult( "pytest_pyfunc_call", pyfuncitem=self, args=self._args, kwargs=kw) def lookup_allargs(self): kwargs = {} if not self._args: # standard Python Test function/method case funcobj = self.obj startindex = getattr(funcobj, 'im_self', None) and 1 or 0 argnames = py.std.inspect.getargs(self.obj.func_code)[0] for i, argname in py.builtin.enumerate(argnames): if i < startindex: continue try: kwargs[argname] = self.lookup_onearg(argname) except LookupError, e: numdefaults = len(funcobj.func_defaults or ()) if i + numdefaults >= len(argnames): continue # continue # seems that our args have defaults else: raise else: pass # XXX lookup of arguments for yielded/generated tests as well return kwargs def lookup_onearg(self, argname): value = self.config.pytestplugins.call_firstresult( "pytest_pyfuncarg_" + argname, pyfuncitem=self) if value is not None: return value else: metainfo = self.repr_metainfo() #self.config.bus.notify("pyfuncarg_lookuperror", argname) raise LookupError("funcargument %r not found for: %s" %(argname,metainfo.verboseline())) def __eq__(self, other): try: return (self.name == other.name and self._args == other._args and self.parent == other.parent and self.obj == other.obj) except AttributeError: pass return False def __ne__(self, other): return not self == other # DEPRECATED #from py.__.test.plugin.pytest_doctest import DoctestFile