put automatic funcarg_ API to Py*objects only, refine internal subclassing and initialisation logic

This commit is contained in:
holger krekel 2012-07-16 10:46:44 +02:00
parent 66ed2d123a
commit 8adac2878f
4 changed files with 175 additions and 157 deletions

View File

@ -119,8 +119,9 @@ class CaptureManager:
return "", "" return "", ""
def activate_funcargs(self, pyfuncitem): def activate_funcargs(self, pyfuncitem):
if pyfuncitem.funcargs: funcargs = getattr(pyfuncitem, "funcargs", None)
for name, capfuncarg in pyfuncitem.funcargs.items(): if funcargs is not None:
for name, capfuncarg in funcargs.items():
if name in ('capsys', 'capfd'): if name in ('capsys', 'capfd'):
assert not hasattr(self, '_capturing_funcarg') assert not hasattr(self, '_capturing_funcarg')
self._capturing_funcarg = capfuncarg self._capturing_funcarg = capfuncarg

View File

@ -3,7 +3,6 @@
import py import py
import pytest, _pytest import pytest, _pytest
import os, sys, imp import os, sys, imp
from _pytest.monkeypatch import monkeypatch
tracebackcutdir = py.path.local(_pytest.__file__).dirpath() tracebackcutdir = py.path.local(_pytest.__file__).dirpath()
@ -151,130 +150,6 @@ def compatproperty(name):
return property(fget) return property(fget)
def pyobj_property(name):
def get(self):
node = self.getparent(getattr(pytest, name))
if node is not None:
return node.obj
doc = "python %s object this node was collected from (can be None)." % (
name.lower(),)
return property(get, None, None, doc)
class Request(object):
_argprefix = "pytest_funcarg__"
class LookupError(LookupError):
""" error while performing funcarg factory lookup. """
def _initattr(self):
self._name2factory = {}
self._currentarg = None
@property
def _plugins(self):
extra = [obj for obj in (self.module, self.instance) if obj]
return self.getplugins() + extra
def _getscopeitem(self, scope):
if scope == "function":
return self
elif scope == "session":
return None
elif scope == "class":
x = self.getparent(pytest.Class)
if x is not None:
return x
scope = "module"
if scope == "module":
return self.getparent(pytest.Module)
raise ValueError("unknown scope %r" %(scope,))
def getfuncargvalue(self, argname):
""" Retrieve a named function argument value.
This function looks up a matching factory and invokes
it to obtain the return value. The factory receives
the same request object and can itself perform recursive
calls to this method, effectively allowing to make use of
multiple other funcarg values or to decorate values from
other name-matching factories.
"""
try:
return self.funcargs[argname]
except KeyError:
pass
except TypeError:
self.funcargs = getattr(self, "_funcargs", {})
if argname not in self._name2factory:
self._name2factory[argname] = self.config.pluginmanager.listattr(
plugins=self._plugins,
attrname=self._argprefix + str(argname)
)
#else: we are called recursively
if not self._name2factory[argname]:
self._raiselookupfailed(argname)
funcargfactory = self._name2factory[argname].pop()
oldarg = self._currentarg
mp = monkeypatch()
mp.setattr(self, '_currentarg', argname)
try:
param = self.callspec.getparam(argname)
except (AttributeError, ValueError):
pass
else:
mp.setattr(self, 'param', param, raising=False)
try:
self.funcargs[argname] = res = funcargfactory(self)
finally:
mp.undo()
return res
def addfinalizer(self, finalizer):
""" add a no-args finalizer function to be called when the underlying
node is torn down."""
self.session._setupstate.addfinalizer(finalizer, self)
def cached_setup(self, setup, teardown=None,
scope="module", extrakey=None):
""" Return a cached testing resource created by ``setup`` &
detroyed by a respective ``teardown(resource)`` call.
:arg teardown: function receiving a previously setup resource.
:arg setup: a no-argument function creating a resource.
:arg scope: a string value out of ``function``, ``class``, ``module``
or ``session`` indicating the caching lifecycle of the resource.
:arg extrakey: added to internal caching key.
"""
if not hasattr(self.config, '_setupcache'):
self.config._setupcache = {} # XXX weakref?
colitem = self._getscopeitem(scope)
cachekey = (self._currentarg, colitem, extrakey)
cache = self.config._setupcache
try:
val = cache[cachekey]
except KeyError:
val = setup()
cache[cachekey] = val
if teardown is not None:
def finalizer():
del cache[cachekey]
teardown(val)
self.session._setupstate.addfinalizer(finalizer, colitem)
return val
def _raiselookupfailed(self, argname):
available = []
for plugin in self._plugins:
for name in vars(plugin):
if name.startswith(self._argprefix):
name = name[len(self._argprefix):]
if name not in available:
available.append(name)
fspath, lineno, msg = self.reportinfo()
msg = "LookupError: no factory found for function argument %r" % (argname,)
msg += "\n available funcargs: %s" %(", ".join(available),)
msg += "\n use 'py.test --funcargs [testpath]' for help on them."
raise self.LookupError(msg)
class Node(object): class Node(object):
""" base class for Collector and Item the test collection tree. """ base class for Collector and Item the test collection tree.
@ -302,6 +177,12 @@ class Node(object):
#: fspath sensitive hook proxy used to call pytest hooks #: fspath sensitive hook proxy used to call pytest hooks
self.ihook = self.session.gethookproxy(self.fspath) self.ihook = self.session.gethookproxy(self.fspath)
self.extrainit()
def extrainit(self):
""""extra initialization after Node is initialized. Implemented
by some subclasses. """
Module = compatproperty("Module") Module = compatproperty("Module")
Class = compatproperty("Class") Class = compatproperty("Class")
Instance = compatproperty("Instance") Instance = compatproperty("Instance")
@ -309,11 +190,6 @@ class Node(object):
File = compatproperty("File") File = compatproperty("File")
Item = compatproperty("Item") Item = compatproperty("Item")
module = pyobj_property("Module")
cls = pyobj_property("Class")
instance = pyobj_property("Instance")
def _getcustomclass(self, name): def _getcustomclass(self, name):
cls = getattr(self, name) cls = getattr(self, name)
if cls != getattr(pytest, name): if cls != getattr(pytest, name):
@ -476,20 +352,12 @@ class FSCollector(Collector):
class File(FSCollector): class File(FSCollector):
""" base class for collecting tests from a file. """ """ base class for collecting tests from a file. """
class Item(Node, Request): class Item(Node):
""" a basic test invocation item. Note that for a single function """ a basic test invocation item. Note that for a single function
there might be multiple test invocation items. there might be multiple test invocation items.
""" """
nextitem = None nextitem = None
def __init__(self, name, parent=None, config=None, session=None):
super(Item, self).__init__(name, parent, config, session)
self._initattr()
self.funcargs = None # later set to a dict from fillfuncargs() or
# from getfuncargvalue(). Setting it to
# None prevents users from performing
# "name in item.funcargs" checks too early.
def reportinfo(self): def reportinfo(self):
return self.fspath, None, "" return self.fspath, None, ""
@ -532,10 +400,9 @@ class Session(FSCollector):
__module__ = 'builtins' # for py3 __module__ = 'builtins' # for py3
def __init__(self, config): def __init__(self, config):
super(Session, self).__init__(py.path.local(), parent=None, FSCollector.__init__(self, py.path.local(), parent=None,
config=config, session=self) config=config, session=self)
assert self.config.pluginmanager.register( self.config.pluginmanager.register(self, name="session", prepend=True)
self, name="session", prepend=True)
self._testsfailed = 0 self._testsfailed = 0
self.shouldstop = False self.shouldstop = False
self.trace = config.trace.root.get("collection") self.trace = config.trace.root.get("collection")

View File

@ -4,7 +4,7 @@ import inspect
import sys import sys
import pytest import pytest
from py._code.code import TerminalRepr from py._code.code import TerminalRepr
from _pytest.main import Request, Item from _pytest.monkeypatch import monkeypatch
import _pytest import _pytest
cutdir = py.path.local(_pytest.__file__).dirpath() cutdir = py.path.local(_pytest.__file__).dirpath()
@ -24,6 +24,135 @@ def cached_property(f):
return x return x
return property(get) return property(get)
def pyobj_property(name):
def get(self):
node = self.getparent(getattr(pytest, name))
if node is not None:
return node.obj
doc = "python %s object this node was collected from (can be None)." % (
name.lower(),)
return property(get, None, None, doc)
class Request(object):
_argprefix = "pytest_funcarg__"
class LookupError(LookupError):
""" error while performing funcarg factory lookup. """
def extrainit(self):
self._name2factory = {}
self._currentarg = None
self.funcargs = None # later set to a dict from fillfuncargs() or
# from getfuncargvalue(). Setting it to
# None prevents users from performing
# "name in item.funcargs" checks too early.
@property
def _plugins(self):
extra = [obj for obj in (self.module, self.instance) if obj]
return self.getplugins() + extra
def _getscopeitem(self, scope):
if scope == "function":
return self
elif scope == "session":
return None
elif scope == "class":
x = self.getparent(pytest.Class)
if x is not None:
return x
scope = "module"
if scope == "module":
return self.getparent(pytest.Module)
raise ValueError("unknown scope %r" %(scope,))
def getfuncargvalue(self, argname):
""" Retrieve a named function argument value.
This function looks up a matching factory and invokes
it to obtain the return value. The factory receives
can itself perform recursive calls to this method,
either for using multiple other funcarg values under the hood
or to decorate values from other factories matching the same name.
"""
try:
return self.funcargs[argname]
except KeyError:
pass
except TypeError:
self.funcargs = getattr(self, "_funcargs", {})
if argname not in self._name2factory:
self._name2factory[argname] = self.config.pluginmanager.listattr(
plugins=self._plugins,
attrname=self._argprefix + str(argname)
)
#else: we are called recursively
if not self._name2factory[argname]:
self._raiselookupfailed(argname)
funcargfactory = self._name2factory[argname].pop()
oldarg = self._currentarg
mp = monkeypatch()
mp.setattr(self, '_currentarg', argname)
try:
param = self.callspec.getparam(argname)
except (AttributeError, ValueError):
pass
else:
mp.setattr(self, 'param', param, raising=False)
try:
self.funcargs[argname] = res = funcargfactory(self)
finally:
mp.undo()
return res
def addfinalizer(self, finalizer):
""" add a no-args finalizer function to be called when the underlying
node is torn down."""
self.session._setupstate.addfinalizer(finalizer, self)
def cached_setup(self, setup, teardown=None,
scope="module", extrakey=None):
""" Return a cached testing resource created by ``setup`` &
detroyed by a respective ``teardown(resource)`` call.
:arg teardown: function receiving a previously setup resource.
:arg setup: a no-argument function creating a resource.
:arg scope: a string value out of ``function``, ``class``, ``module``
or ``session`` indicating the caching lifecycle of the resource.
:arg extrakey: added to internal caching key.
"""
if not hasattr(self.config, '_setupcache'):
self.config._setupcache = {} # XXX weakref?
colitem = self._getscopeitem(scope)
cachekey = (self._currentarg, colitem, extrakey)
cache = self.config._setupcache
try:
val = cache[cachekey]
except KeyError:
val = setup()
cache[cachekey] = val
if teardown is not None:
def finalizer():
del cache[cachekey]
teardown(val)
self.session._setupstate.addfinalizer(finalizer, colitem)
return val
def _raiselookupfailed(self, argname):
available = []
for plugin in self._plugins:
for name in vars(plugin):
if name.startswith(self._argprefix):
name = name[len(self._argprefix):]
if name not in available:
available.append(name)
fspath, lineno, msg = self.reportinfo()
msg = "LookupError: no factory found for function argument %r" % (argname,)
msg += "\n available funcargs: %s" %(", ".join(available),)
msg += "\n use 'py.test --funcargs [testpath]' for help on them."
raise self.LookupError(msg)
def pytest_addoption(parser): def pytest_addoption(parser):
group = parser.getgroup("general") group = parser.getgroup("general")
@ -133,8 +262,12 @@ def is_generator(func):
# assume them to not be generators # assume them to not be generators
return False return False
class PyobjMixin(object): class PyobjContext(object):
module = pyobj_property("Module")
cls = pyobj_property("Class")
instance = pyobj_property("Instance")
class PyobjMixin(Request, PyobjContext):
def obj(): def obj():
def fget(self): def fget(self):
try: try:
@ -203,7 +336,7 @@ class PyobjMixin(object):
assert isinstance(lineno, int) assert isinstance(lineno, int)
return fspath, lineno, modpath return fspath, lineno, modpath
class PyCollectorMixin(PyobjMixin, pytest.Collector): class PyCollector(PyobjMixin, pytest.Collector):
def funcnamefilter(self, name): def funcnamefilter(self, name):
for prefix in self.config.getini("python_functions"): for prefix in self.config.getini("python_functions"):
@ -283,7 +416,7 @@ def transfer_markers(funcobj, cls, mod):
else: else:
pytestmark(funcobj) pytestmark(funcobj)
class Module(pytest.File, PyCollectorMixin): class Module(pytest.File, PyCollector):
""" Collector for test classes and functions. """ """ Collector for test classes and functions. """
def _getobj(self): def _getobj(self):
return self._memoizedcall('_obj', self._importtestmodule) return self._memoizedcall('_obj', self._importtestmodule)
@ -331,7 +464,7 @@ class Module(pytest.File, PyCollectorMixin):
else: else:
self.obj.teardown_module() self.obj.teardown_module()
class Class(PyCollectorMixin, pytest.Collector): class Class(PyCollector):
""" Collector for test methods. """ """ Collector for test methods. """
def collect(self): def collect(self):
return [self._getcustomclass("Instance")(name="()", parent=self)] return [self._getcustomclass("Instance")(name="()", parent=self)]
@ -350,7 +483,7 @@ class Class(PyCollectorMixin, pytest.Collector):
teardown_class = getattr(teardown_class, '__func__', teardown_class) teardown_class = getattr(teardown_class, '__func__', teardown_class)
teardown_class(self.obj) teardown_class(self.obj)
class Instance(PyCollectorMixin, pytest.Collector): class Instance(PyCollector):
def _getobj(self): def _getobj(self):
return self.parent.obj() return self.parent.obj()
@ -437,7 +570,7 @@ class FuncargLookupErrorRepr(TerminalRepr):
tw.line("%s:%d" % (self.filename, self.firstlineno+1)) tw.line("%s:%d" % (self.filename, self.firstlineno+1))
class Generator(FunctionMixin, PyCollectorMixin, pytest.Collector): class Generator(FunctionMixin, PyCollector):
def collect(self): def collect(self):
# test generators are seen as collectors but they also # test generators are seen as collectors but they also
# invoke setup/teardown on popular request # invoke setup/teardown on popular request
@ -882,7 +1015,7 @@ def itemapi_property(name, set=False):
return property(get, set, None, doc) return property(get, set, None, doc)
class OldFuncargRequest(Request): class OldFuncargRequest(Request, PyobjContext):
""" (deprecated) helper interactions with a test function invocation. """ (deprecated) helper interactions with a test function invocation.
Note that there is an optional ``param`` attribute in case Note that there is an optional ``param`` attribute in case
@ -892,9 +1025,11 @@ class OldFuncargRequest(Request):
""" """
def __init__(self, pyfuncitem): def __init__(self, pyfuncitem):
self._pyfuncitem = pyfuncitem self._pyfuncitem = pyfuncitem
Request._initattr(self) Request.extrainit(self)
self.funcargs = pyfuncitem.funcargs
self.getplugins = self._pyfuncitem.getplugins self.getplugins = self._pyfuncitem.getplugins
self.reportinfo = self._pyfuncitem.reportinfo self.reportinfo = self._pyfuncitem.reportinfo
self.getparent = self._pyfuncitem.getparent
try: try:
self.param = self._pyfuncitem.param self.param = self._pyfuncitem.param
except AttributeError: except AttributeError:
@ -906,9 +1041,6 @@ class OldFuncargRequest(Request):
_getscopeitem = itemapi_property("_getscopeitem") _getscopeitem = itemapi_property("_getscopeitem")
funcargs = itemapi_property("funcargs", set=True) funcargs = itemapi_property("funcargs", set=True)
keywords = itemapi_property("keywords") keywords = itemapi_property("keywords")
module = itemapi_property("module")
cls = itemapi_property("cls")
instance = itemapi_property("instance")
config = itemapi_property("config") config = itemapi_property("config")
session = itemapi_property("session") session = itemapi_property("session")
fspath = itemapi_property("fspath") fspath = itemapi_property("fspath")

View File

@ -1647,3 +1647,21 @@ class TestRequestAPI:
]) ])
class TestResourceIntegrationFunctional:
def test_parametrize_with_ids(self, testdir):
testdir.makepyfile("""
import pytest
def pytest_generate_tests(metafunc):
metafunc.parametrize(("a", "b"), [(1,1), (1,2)],
ids=["basic", "advanced"])
def test_function(a, b):
assert a == b
""")
result = testdir.runpytest("-v")
assert result.ret == 1
result.stdout.fnmatch_lines([
"*test_function*basic*PASSED",
"*test_function*advanced*FAILED",
])