test_ok1/py/_test/pycollect.py

371 lines
13 KiB
Python

"""
Python related collection nodes.
"""
import py
import inspect
from py._test.collect import configproperty, warnoldcollect
from py._test import funcargs
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
# xxx let decorators etc specify a sane ordering
if hasattr(obj, 'place_as'):
obj = obj.place_as
self._fslineno = py.code.getfslineno(obj)
return self._fslineno
def reportinfo(self):
fspath, lineno = self._getfslineno()
modpath = self.getmodpath()
return fspath, lineno, 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
# NB. we avoid random getattrs and peek in the __dict__ instead
dicts = [getattr(self.obj, '__dict__', {})]
for basecls in inspect.getmro(self.obj.__class__):
dicts.append(basecls.__dict__)
seen = {}
l = []
for dic in dicts:
for name, obj in dic.items():
if name in seen:
continue
seen[name] = True
if name[0] != "_":
res = self.makeitem(name, obj)
if res is None:
continue
if not isinstance(res, list):
res = [res]
l.extend(res)
l.sort(key=lambda item: item.reportinfo()[:2])
return l
def _deprecated_join(self, name):
if self.__class__.join != py.test.collect.Collector.join:
warnoldcollect()
return self.join(name)
def makeitem(self, name, obj):
return self.ihook.pytest_pycollect_makeitem(
collector=self, name=name, obj=obj)
def _istestclasscandidate(self, name, obj):
if self.classnamefilter(name) and \
inspect.isclass(obj):
if hasinit(obj):
# XXX WARN
return False
return True
def _genfunctions(self, name, funcobj):
module = self.getparent(Module).obj
clscol = self.getparent(Class)
cls = clscol and clscol.obj or None
metafunc = funcargs.Metafunc(funcobj, config=self.config,
cls=cls, module=module)
gentesthook = self.config.hook.pytest_generate_tests
plugins = funcargs.getplugins(self, withpy=True)
gentesthook.pcall(plugins, metafunc=metafunc)
if not metafunc._calls:
return self.Function(name, parent=self)
l = []
for callspec in metafunc._calls:
subname = "%s[%s]" %(name, callspec.id)
function = self.Function(name=subname, parent=self,
callspec=callspec, callobj=funcobj)
l.append(function)
return l
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.pluginmanager.consider_module(mod)
return mod
def setup(self):
if getattr(self.obj, 'disabled', 0):
py.log._apiwarn(">1.1.1", "%r uses 'disabled' which is deprecated, "
"use pytestmark=..., see pytest_skipping plugin" % (self.obj,))
py.test.skip("%r is disabled" %(self.obj,))
if hasattr(self.obj, 'setup_module'):
#XXX: nose compat hack, move to nose plugin
# if it takes a positional arg, its probably a py.test style one
# so we pass the current module object
if inspect.getargspec(self.obj.setup_module)[0]:
self.obj.setup_module(self.obj)
else:
self.obj.setup_module()
def teardown(self):
if hasattr(self.obj, 'teardown_module'):
#XXX: nose compat hack, move to nose plugin
# if it takes a positional arg, its probably a py.test style one
# so we pass the current module object
if inspect.getargspec(self.obj.teardown_module)[0]:
self.obj.teardown_module(self.obj)
else:
self.obj.teardown_module()
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.log._apiwarn(">1.1.1", "%r uses 'disabled' which is deprecated, "
"use pytestmark=..., see pytest_skipping plugin" % (self.obj,))
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)
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 setup(self):
""" perform setup for this test function. """
if inspect.ismethod(self.obj):
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:
setup_func_or_method(self.obj)
def teardown(self):
""" perform teardown for this test function. """
if inspect.ismethod(self.obj):
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=py._pydir)
traceback = ntraceback.filter()
return traceback
def repr_failure(self, excinfo, outerr=None):
assert outerr is None, "XXX outerr usage is deprecated"
return self._repr_failure_py(excinfo)
shortfailurerepr = "F"
class Generator(FunctionMixin, PyCollectorMixin, py.test.collect.Collector):
def collect(self):
# test generators are seen as collectors but they also
# invoke setup/teardown on popular request
# (induced by the common "test_*" naming shared with normal tests)
self.config._setupstate.prepare(self)
l = []
seen = {}
for i, x in enumerate(self.obj()):
name, call, args = self.getcallargs(x)
if not py.builtin.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], py.builtin._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.
"""
_genid = None
def __init__(self, name, parent=None, args=None, config=None,
callspec=None, callobj=_dummy):
super(Function, self).__init__(name, parent, config=config)
self._args = args
if self._isyieldedfunction():
assert not callspec, "yielded functions (deprecated) cannot have funcargs"
else:
if callspec is not None:
self.funcargs = callspec.funcargs or {}
self._genid = callspec.id
if hasattr(callspec, "param"):
self._requestparam = callspec.param
else:
self.funcargs = {}
if callobj is not _dummy:
self._obj = callobj
self.function = getattr(self.obj, 'im_func', self.obj)
def _getobj(self):
name = self.name
i = name.find("[") # parametrization
if i != -1:
name = name[:i]
return getattr(self.parent.obj, name)
def _isyieldedfunction(self):
return self._args is not None
def readkeywords(self):
d = super(Function, self).readkeywords()
d.update(py.builtin._getfuncdict(self.obj))
return d
def runtest(self):
""" execute the underlying test function. """
self.ihook.pytest_pyfunc_call(pyfuncitem=self)
def setup(self):
super(Function, self).setup()
if hasattr(self, 'funcargs'):
funcargs.fillfuncargs(self)
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 and
getattr(self, '_genid', None) ==
getattr(other, '_genid', None)
)
except AttributeError:
pass
return False
def __ne__(self, other):
return not self == other
def __hash__(self):
return hash((self.parent, self.name))
def hasinit(obj):
init = getattr(obj, '__init__', None)
if init:
if not isinstance(init, type(object.__init__)):
return True