mid-scale refactoring to make request API available directly on items.

This commit was slightly tricky because i want to backward
compatibility especially for the oejskit plugin which
uses Funcarg-filling for non-Function objects.
This commit is contained in:
holger krekel 2012-06-25 17:35:33 +02:00
parent 227d847216
commit 91b6f2bda8
16 changed files with 529 additions and 399 deletions

View File

@ -1,6 +1,19 @@
Changes between 2.2.4 and 2.2.5.dev Changes between 2.2.4 and 2.3.0.dev
----------------------------------- -----------------------------------
- merge FuncargRequest and Item API such that funcarg-functionality
is now natively available on the "item" object passed to the various
pytest_runtest hooks. This allows more sensitive behaviour
of e.g. the pytest-django plugin which previously had no full
access to all instantiated funcargs.
This internal API re-organisation is a fully backward compatible
change: existing factories accepting a "request" object will
get a Function "item" object which carries the same API. In fact,
the FuncargRequest API (or rather then a ResourceRequestAPI)
could be available for all collection and item nodes but this is
left for later consideration because it would render the documentation
invalid and the "funcarg" naming sounds odd in context of
directory, file, class, etc. nodes.
- catch unicode-issues when writing failure representations - catch unicode-issues when writing failure representations
to terminal to prevent the whole session from crashing to terminal to prevent the whole session from crashing
- fix xfail/skip confusion: a skip-mark or an imperative pytest.skip - fix xfail/skip confusion: a skip-mark or an imperative pytest.skip
@ -23,6 +36,7 @@ Changes between 2.2.4 and 2.2.5.dev
pytest-django, trial and unittest integration. pytest-django, trial and unittest integration.
- reporting refinements: - reporting refinements:
- pytest_report_header now receives a "startdir" so that - pytest_report_header now receives a "startdir" so that
you can use startdir.bestrelpath(yourpath) to show you can use startdir.bestrelpath(yourpath) to show
nice relative path nice relative path

View File

@ -1,2 +1,2 @@
# #
__version__ = '2.2.5.dev4' __version__ = '2.3.0.dev1'

View File

@ -119,7 +119,7 @@ class CaptureManager:
return "", "" return "", ""
def activate_funcargs(self, pyfuncitem): def activate_funcargs(self, pyfuncitem):
if hasattr(pyfuncitem, 'funcargs'): if pyfuncitem.funcargs:
for name, capfuncarg in pyfuncitem.funcargs.items(): for name, capfuncarg in pyfuncitem.funcargs.items():
if name in ('capsys', 'capfd'): if name in ('capsys', 'capfd'):
assert not hasattr(self, '_capturing_funcarg') assert not hasattr(self, '_capturing_funcarg')
@ -186,7 +186,7 @@ def pytest_funcarg__capsys(request):
captured output available via ``capsys.readouterr()`` method calls captured output available via ``capsys.readouterr()`` method calls
which return a ``(out, err)`` tuple. which return a ``(out, err)`` tuple.
""" """
if "capfd" in request._funcargs: if "capfd" in request.funcargs:
raise request.LookupError(error_capsysfderror) raise request.LookupError(error_capsysfderror)
return CaptureFuncarg(py.io.StdCapture) return CaptureFuncarg(py.io.StdCapture)
@ -195,7 +195,7 @@ def pytest_funcarg__capfd(request):
captured output available via ``capsys.readouterr()`` method calls captured output available via ``capsys.readouterr()`` method calls
which return a ``(out, err)`` tuple. which return a ``(out, err)`` tuple.
""" """
if "capsys" in request._funcargs: if "capsys" in request.funcargs:
raise request.LookupError(error_capsysfderror) raise request.LookupError(error_capsysfderror)
if not hasattr(os, 'dup'): if not hasattr(os, 'dup'):
pytest.skip("capfd funcarg needs os.dup") pytest.skip("capfd funcarg needs os.dup")

View File

@ -3,6 +3,8 @@
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()
# exitcodes for the command line # exitcodes for the command line
@ -144,33 +146,162 @@ class HookProxy:
def compatproperty(name): def compatproperty(name):
def fget(self): def fget(self):
# deprecated - use pytest.name
return getattr(pytest, name) return getattr(pytest, name)
return property(fget, None, None, return property(fget)
"deprecated attribute %r, use pytest.%s" % (name, name))
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 all Nodes in the collection tree. """ base class for Collector and Item the test collection tree.
Collector subclasses have children, Items are terminal nodes.""" Collector subclasses have children, Items are terminal nodes."""
def __init__(self, name, parent=None, config=None, session=None): def __init__(self, name, parent=None, config=None, session=None):
#: a unique name with the scope of the parent #: a unique name within the scope of the parent node
self.name = name self.name = name
#: the parent collector node. #: the parent collector node.
self.parent = parent self.parent = parent
#: the test config object #: the pytest config object
self.config = config or parent.config self.config = config or parent.config
#: the collection this node is part of #: the session this node is part of
self.session = session or parent.session self.session = session or parent.session
#: filesystem path where this node was collected from #: filesystem path where this node was collected from (can be None)
self.fspath = getattr(parent, 'fspath', None) self.fspath = getattr(parent, 'fspath', None)
self.ihook = self.session.gethookproxy(self.fspath)
#: keywords on this node (node name is always contained)
self.keywords = {self.name: True} self.keywords = {self.name: True}
#: fspath sensitive hook proxy used to call pytest hooks
self.ihook = self.session.gethookproxy(self.fspath)
Module = compatproperty("Module") Module = compatproperty("Module")
Class = compatproperty("Class") Class = compatproperty("Class")
Instance = compatproperty("Instance") Instance = compatproperty("Instance")
@ -178,6 +309,11 @@ 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):
@ -193,12 +329,14 @@ class Node(object):
# methods for ordering nodes # methods for ordering nodes
@property @property
def nodeid(self): def nodeid(self):
""" a ::-separated string denoting its collection tree address. """
try: try:
return self._nodeid return self._nodeid
except AttributeError: except AttributeError:
self._nodeid = x = self._makeid() self._nodeid = x = self._makeid()
return x return x
def _makeid(self): def _makeid(self):
return self.parent.nodeid + "::" + self.name return self.parent.nodeid + "::" + self.name
@ -338,15 +476,36 @@ 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): class Item(Node, Request):
""" 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, ""
def applymarker(self, marker):
""" Apply a marker to this item. This method is
useful if you have several parametrized function
and want to mark a single one of them.
:arg marker: a :py:class:`_pytest.mark.MarkDecorator` object
created by a call to ``py.test.mark.NAME(...)``.
"""
if not isinstance(marker, pytest.mark.XYZ.__class__):
raise ValueError("%r is not a py.test.mark.* object")
self.keywords[marker.markname] = marker
@property @property
def location(self): def location(self):
try: try:

View File

@ -318,7 +318,7 @@ class TmpTestdir:
# used from runner functional tests # used from runner functional tests
item = self.getitem(source) item = self.getitem(source)
# the test class where we are called from wants to provide the runner # the test class where we are called from wants to provide the runner
testclassinstance = py.builtin._getimself(self.request.function) testclassinstance = self.request.instance
runner = testclassinstance.getrunner() runner = testclassinstance.getrunner()
return runner(item) return runner(item)

View File

@ -4,11 +4,27 @@ import inspect
import sys import sys
import pytest import pytest
from py._code.code import TerminalRepr from py._code.code import TerminalRepr
from _pytest.monkeypatch import monkeypatch from _pytest.main import Request, Item
import _pytest import _pytest
cutdir = py.path.local(_pytest.__file__).dirpath() cutdir = py.path.local(_pytest.__file__).dirpath()
def cached_property(f):
"""returns a cached property that is calculated by function f.
taken from http://code.activestate.com/recipes/576563-cached-property/"""
def get(self):
try:
return self._property_cache[f]
except AttributeError:
self._property_cache = {}
x = self._property_cache[f] = f(self)
return x
except KeyError:
x = self._property_cache[f] = f(self)
return x
return property(get)
def pytest_addoption(parser): def pytest_addoption(parser):
group = parser.getgroup("general") group = parser.getgroup("general")
group.addoption('--funcargs', group.addoption('--funcargs',
@ -60,13 +76,21 @@ def pytest_funcarg__pytestconfig(request):
""" the pytest config object with access to command line opts.""" """ the pytest config object with access to command line opts."""
return request.config return request.config
def pytest_pyfunc_call(__multicall__, pyfuncitem): def pytest_pyfunc_call(__multicall__, pyfuncitem):
if not __multicall__.execute(): if not __multicall__.execute():
testfunction = pyfuncitem.obj testfunction = pyfuncitem.obj
if pyfuncitem._isyieldedfunction(): if pyfuncitem._isyieldedfunction():
testfunction(*pyfuncitem._args) testfunction(*pyfuncitem._args)
else: else:
funcargs = pyfuncitem.funcargs try:
funcargnames = pyfuncitem.funcargnames
except AttributeError:
funcargs = pyfuncitem.funcargs
else:
funcargs = {}
for name in funcargnames:
funcargs[name] = pyfuncitem.funcargs[name]
testfunction(**funcargs) testfunction(**funcargs)
def pytest_collect_file(path, parent): def pytest_collect_file(path, parent):
@ -110,6 +134,7 @@ def is_generator(func):
return False return False
class PyobjMixin(object): class PyobjMixin(object):
def obj(): def obj():
def fget(self): def fget(self):
try: try:
@ -232,12 +257,15 @@ class PyCollectorMixin(PyobjMixin, pytest.Collector):
gentesthook.pcall(plugins, metafunc=metafunc) gentesthook.pcall(plugins, metafunc=metafunc)
Function = self._getcustomclass("Function") Function = self._getcustomclass("Function")
if not metafunc._calls: if not metafunc._calls:
return Function(name, parent=self) return Function(name, parent=self,
funcargnames=metafunc.funcargnames)
l = [] l = []
for callspec in metafunc._calls: for callspec in metafunc._calls:
subname = "%s[%s]" %(name, callspec.id) subname = "%s[%s]" %(name, callspec.id)
function = Function(name=subname, parent=self, function = Function(name=subname, parent=self,
callspec=callspec, callobj=funcobj, keywords={callspec.id:True}) callspec=callspec, callobj=funcobj,
funcargnames=metafunc.funcargnames,
keywords={callspec.id:True})
l.append(function) l.append(function)
return l return l
@ -256,6 +284,7 @@ def transfer_markers(funcobj, cls, mod):
pytestmark(funcobj) pytestmark(funcobj)
class Module(pytest.File, PyCollectorMixin): class Module(pytest.File, PyCollectorMixin):
""" Collector for test classes and functions. """
def _getobj(self): def _getobj(self):
return self._memoizedcall('_obj', self._importtestmodule) return self._memoizedcall('_obj', self._importtestmodule)
@ -303,7 +332,7 @@ class Module(pytest.File, PyCollectorMixin):
self.obj.teardown_module() self.obj.teardown_module()
class Class(PyCollectorMixin, pytest.Collector): class Class(PyCollectorMixin, pytest.Collector):
""" Collector for test methods. """
def collect(self): def collect(self):
return [self._getcustomclass("Instance")(name="()", parent=self)] return [self._getcustomclass("Instance")(name="()", parent=self)]
@ -373,7 +402,7 @@ class FunctionMixin(PyobjMixin):
excinfo.traceback = ntraceback.filter() excinfo.traceback = ntraceback.filter()
def _repr_failure_py(self, excinfo, style="long"): def _repr_failure_py(self, excinfo, style="long"):
if excinfo.errisinstance(FuncargRequest.LookupError): if excinfo.errisinstance(Request.LookupError):
fspath, lineno, msg = self.reportinfo() fspath, lineno, msg = self.reportinfo()
lines, _ = inspect.getsourcelines(self.obj) lines, _ = inspect.getsourcelines(self.obj)
for i, line in enumerate(lines): for i, line in enumerate(lines):
@ -445,77 +474,6 @@ class Generator(FunctionMixin, PyCollectorMixin, pytest.Collector):
return name, call, args return name, call, args
#
# Test Items
#
_dummy = object()
class Function(FunctionMixin, pytest.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, keywords=None, session=None):
super(Function, self).__init__(name, parent,
config=config, session=session)
self._args = args
if self._isyieldedfunction():
assert not callspec, (
"yielded functions (deprecated) cannot have funcargs")
else:
if callspec is not None:
self.callspec = callspec
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)
self.keywords.update(py.builtin._getfuncdict(self.obj) or {})
if keywords:
self.keywords.update(keywords)
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 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'):
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): def hasinit(obj):
init = getattr(obj, '__init__', None) init = getattr(obj, '__init__', None)
if init: if init:
@ -535,10 +493,20 @@ def getfuncargnames(function, startindex=None):
return argnames[startindex:-numdefaults] return argnames[startindex:-numdefaults]
return argnames[startindex:] return argnames[startindex:]
def fillfuncargs(function): def fillfuncargs(node):
""" fill missing funcargs. """ """ fill missing funcargs. """
request = FuncargRequest(pyfuncitem=function) if not isinstance(node, Function):
request._fillfuncargs() node = FuncargRequest(pyfuncitem=node)
if node.funcargs is None:
node.funcargs = getattr(node, "_funcargs", {})
if not isinstance(node, Function) or not node._isyieldedfunction():
try:
funcargnames = node.funcargnames
except AttributeError:
funcargnames = getfuncargnames(node.function)
if funcargnames:
for argname in funcargnames:
node.getfuncargvalue(argname)
_notexists = object() _notexists = object()
@ -697,195 +665,6 @@ class IDMaker:
l.append(str(val)) l.append(str(val))
return "-".join(l) return "-".join(l)
class FuncargRequest:
""" A request for function arguments from a test function.
Note that there is an optional ``param`` attribute in case
there was an invocation to metafunc.addcall(param=...).
If no such call was done in a ``pytest_generate_tests``
hook, the attribute will not be present.
"""
_argprefix = "pytest_funcarg__"
_argname = None
class LookupError(LookupError):
""" error on performing funcarg request. """
def __init__(self, pyfuncitem):
self._pyfuncitem = pyfuncitem
if hasattr(pyfuncitem, '_requestparam'):
self.param = pyfuncitem._requestparam
extra = [obj for obj in (self.module, self.instance) if obj]
self._plugins = pyfuncitem.getplugins() + extra
self._funcargs = self._pyfuncitem.funcargs.copy()
self._name2factory = {}
self._currentarg = None
@property
def function(self):
""" function object of the test invocation. """
return self._pyfuncitem.obj
@property
def keywords(self):
""" keywords of the test function item.
.. versionadded:: 2.0
"""
return self._pyfuncitem.keywords
@property
def module(self):
""" module where the test function was collected. """
return self._pyfuncitem.getparent(pytest.Module).obj
@property
def cls(self):
""" class (can be None) where the test function was collected. """
clscol = self._pyfuncitem.getparent(pytest.Class)
if clscol:
return clscol.obj
@property
def instance(self):
""" instance (can be None) on which test function was collected. """
return py.builtin._getimself(self.function)
@property
def config(self):
""" the pytest config object associated with this request. """
return self._pyfuncitem.config
@property
def fspath(self):
""" the file system path of the test module which collected this test. """
return self._pyfuncitem.fspath
def _fillfuncargs(self):
argnames = getfuncargnames(self.function)
if argnames:
assert not getattr(self._pyfuncitem, '_args', None), (
"yielded functions cannot have funcargs")
for argname in argnames:
if argname not in self._pyfuncitem.funcargs:
self._pyfuncitem.funcargs[argname] = self.getfuncargvalue(argname)
def applymarker(self, marker):
""" Apply a marker to a single test function invocation.
This method is useful if you don't want to have a keyword/marker
on all function invocations.
:arg marker: a :py:class:`_pytest.mark.MarkDecorator` object
created by a call to ``py.test.mark.NAME(...)``.
"""
if not isinstance(marker, py.test.mark.XYZ.__class__):
raise ValueError("%r is not a py.test.mark.* object")
self._pyfuncitem.keywords[marker.markname] = marker
def cached_setup(self, setup, teardown=None, scope="module", extrakey=None):
""" Return a testing resource managed by ``setup`` &
``teardown`` calls. ``scope`` and ``extrakey`` determine when the
``teardown`` function will be called so that subsequent calls to
``setup`` would recreate the resource.
: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 of (funcargname, scope).
"""
if not hasattr(self.config, '_setupcache'):
self.config._setupcache = {} # XXX weakref?
cachekey = (self._currentarg, self._getscopeitem(scope), 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._addfinalizer(finalizer, scope=scope)
return val
def getfuncargvalue(self, argname):
""" Retrieve a function argument by name for this test
function invocation. This allows one function argument factory
to call another function argument factory. If there are two
funcarg factories for the same test function argument the first
factory may use ``getfuncargvalue`` to call the second one and
do something additional with the resource.
"""
try:
return self._funcargs[argname]
except KeyError:
pass
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._pyfuncitem.callspec.getparam(argname)
except (AttributeError, ValueError):
pass
else:
mp.setattr(self, 'param', param, raising=False)
try:
self._funcargs[argname] = res = funcargfactory(request=self)
finally:
mp.undo()
return res
def _getscopeitem(self, scope):
if scope == "function":
return self._pyfuncitem
elif scope == "session":
return None
elif scope == "class":
x = self._pyfuncitem.getparent(pytest.Class)
if x is not None:
return x
scope = "module"
if scope == "module":
return self._pyfuncitem.getparent(pytest.Module)
raise ValueError("unknown finalization scope %r" %(scope,))
def addfinalizer(self, finalizer):
"""add finalizer function to be called after test function
finished execution. """
self._addfinalizer(finalizer, scope="function")
def _addfinalizer(self, finalizer, scope):
colitem = self._getscopeitem(scope)
self._pyfuncitem.session._setupstate.addfinalizer(
finalizer=finalizer, colitem=colitem)
def __repr__(self):
return "<FuncargRequest for %r>" %(self._pyfuncitem)
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._pyfuncitem.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 showfuncargs(config): def showfuncargs(config):
from _pytest.main import wrap_session from _pytest.main import wrap_session
@ -903,8 +682,8 @@ def _showfuncargs_main(config, session):
for plugin in plugins: for plugin in plugins:
available = [] available = []
for name, factory in vars(plugin).items(): for name, factory in vars(plugin).items():
if name.startswith(FuncargRequest._argprefix): if name.startswith(Request._argprefix):
name = name[len(FuncargRequest._argprefix):] name = name[len(Request._argprefix):]
if name not in available: if name not in available:
available.append([name, factory]) available.append([name, factory])
if available: if available:
@ -1009,3 +788,131 @@ class RaisesContext(object):
self.excinfo.__init__(tp) self.excinfo.__init__(tp)
return issubclass(self.excinfo.type, self.ExpectedException) return issubclass(self.excinfo.type, self.ExpectedException)
#
# the basic py.test Function item
#
_dummy = object()
class Function(FunctionMixin, pytest.Item):
""" a Function Item is responsible for setting up and executing a
Python test function.
"""
_genid = None
def __init__(self, name, parent=None, args=None, config=None,
callspec=None, callobj=_dummy, keywords=None,
session=None, funcargnames=()):
super(Function, self).__init__(name, parent, config=config,
session=session)
self.funcargnames = funcargnames
self._args = args
if self._isyieldedfunction():
assert not callspec, (
"yielded functions (deprecated) cannot have funcargs")
else:
if callspec is not None:
self.callspec = callspec
self._funcargs = callspec.funcargs or {}
self._genid = callspec.id
if hasattr(callspec, "param"):
self.param = callspec.param
if callobj is not _dummy:
self._obj = callobj
self.keywords.update(py.builtin._getfuncdict(self.obj) or {})
if keywords:
self.keywords.update(keywords)
@property
def function(self):
"underlying python 'function' object"
return 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)
@property
def _pyfuncitem(self):
"(compatonly) for code expecting pytest-2.2 style request objects"
return self
def _isyieldedfunction(self):
return getattr(self, "_args", None) is not None
def runtest(self):
""" execute the underlying test function. """
self.ihook.pytest_pyfunc_call(pyfuncitem=self)
def setup(self):
super(Function, self).setup()
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 itemapi_property(name, set=False):
prop = getattr(Function, name, None)
doc = getattr(prop, "__doc__", None)
def get(self):
return getattr(self._pyfuncitem, name)
if set:
def set(self, value):
setattr(self._pyfuncitem, name, value)
else:
set = None
return property(get, set, None, doc)
class FuncargRequest(Request):
""" (deprecated) helper interactions with a test function invocation.
Note that there is an optional ``param`` attribute in case
there was an invocation to metafunc.addcall(param=...).
If no such call was done in a ``pytest_generate_tests``
hook, the attribute will not be present.
"""
def __init__(self, pyfuncitem):
self._pyfuncitem = pyfuncitem
Request._initattr(self)
self.getplugins = self._pyfuncitem.getplugins
self.reportinfo = self._pyfuncitem.reportinfo
try:
self.param = self._pyfuncitem.param
except AttributeError:
pass
def __repr__(self):
return "<FuncargRequest for %r>" % (self._pyfuncitem.name)
_getscopeitem = itemapi_property("_getscopeitem")
funcargs = itemapi_property("funcargs", set=True)
keywords = itemapi_property("keywords")
module = itemapi_property("module")
cls = itemapi_property("cls")
instance = itemapi_property("instance")
config = itemapi_property("config")
session = itemapi_property("session")
fspath = itemapi_property("fspath")
applymarker = itemapi_property("applymarker")
@property
def function(self):
return self._pyfuncitem.obj

View File

@ -1,4 +1,6 @@
""" (disabled by default) create result information in a plain text file. """ """ log machine-parseable test session result information in a plain
text file.
"""
import py import py

View File

@ -54,15 +54,15 @@ def pytest_configure(config):
mp.setattr(config, '_tmpdirhandler', t, raising=False) mp.setattr(config, '_tmpdirhandler', t, raising=False)
mp.setattr(pytest, 'ensuretemp', t.ensuretemp, raising=False) mp.setattr(pytest, 'ensuretemp', t.ensuretemp, raising=False)
def pytest_funcarg__tmpdir(request): def pytest_funcarg__tmpdir(item):
"""return a temporary directory path object """return a temporary directory path object
which is unique to each test function invocation, which is unique to each test function invocation,
created as a sub directory of the base temporary created as a sub directory of the base temporary
directory. The returned object is a `py.path.local`_ directory. The returned object is a `py.path.local`_
path object. path object.
""" """
name = request._pyfuncitem.name name = item.name
name = py.std.re.sub("[\W]", "_", name) name = py.std.re.sub("[\W]", "_", name)
x = request.config._tmpdirhandler.mktemp(name, numbered=True) x = item.config._tmpdirhandler.mktemp(name, numbered=True)
return x return x

View File

@ -17,7 +17,7 @@
# #
# The full version, including alpha/beta/rc tags. # The full version, including alpha/beta/rc tags.
# The short X.Y version. # The short X.Y version.
version = release = "2.2.4.3" version = release = "2.3.0.dev1"
import sys, os import sys, os
@ -26,6 +26,8 @@ import sys, os
# documentation root, use os.path.abspath to make it absolute, like shown here. # documentation root, use os.path.abspath to make it absolute, like shown here.
#sys.path.insert(0, os.path.abspath('.')) #sys.path.insert(0, os.path.abspath('.'))
autodoc_member_order = "bysource"
# -- General configuration ----------------------------------------------------- # -- General configuration -----------------------------------------------------
# If your documentation needs a minimal Sphinx version, state it here. # If your documentation needs a minimal Sphinx version, state it here.
@ -53,6 +55,7 @@ project = u'pytest'
copyright = u'2011, holger krekel et alii' copyright = u'2011, holger krekel et alii'
# The language for content autogenerated by Sphinx. Refer to documentation # The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages. # for a list of supported languages.
#language = None #language = None
@ -78,7 +81,7 @@ exclude_patterns = ['links.inc', '_build', 'naming20.txt', 'test/*',
# If true, the current module name will be prepended to all description # If true, the current module name will be prepended to all description
# unit titles (such as .. function::). # unit titles (such as .. function::).
#add_module_names = True add_module_names = False
# If true, sectionauthor and moduleauthor directives will be shown in the # If true, sectionauthor and moduleauthor directives will be shown in the
# output. They are ignored by default. # output. They are ignored by default.
@ -87,6 +90,8 @@ exclude_patterns = ['links.inc', '_build', 'naming20.txt', 'test/*',
# The name of the Pygments (syntax highlighting) style to use. # The name of the Pygments (syntax highlighting) style to use.
pygments_style = 'sphinx' pygments_style = 'sphinx'
# A list of ignored prefixes for module index sorting. # A list of ignored prefixes for module index sorting.
#modindex_common_prefix = [] #modindex_common_prefix = []

View File

@ -11,26 +11,27 @@ Injecting objects into test functions (funcargs)
Dependency injection through function arguments Dependency injection through function arguments
================================================= =================================================
py.test lets you inject objects into test functions and precisely py.test lets you inject objects into test invocations and precisely
control their life cycle in relation to the test execution. It is control their life cycle in relation to the overall test execution. Moreover,
also possible to run a test function multiple times with different objects. you can run a test function multiple times injecting different objects.
The basic mechanism for injecting objects is also called the The basic mechanism for injecting objects is also called the
*funcarg mechanism* because objects are ultimately injected *funcarg mechanism* because objects are ultimately injected
by calling a test function with it as an argument. Unlike the by calling a test function with it as an argument. Unlike the
classical xUnit approach *funcargs* relate more to `Dependency Injection`_ classical xUnit approach *funcargs* relate more to `Dependency Injection`_
because they help to de-couple test code from objects required for because they help to de-couple test code from objects required for
them to execute. them to execute. At test writing time you do not need to care for the
details of how your required resources are constructed or if they
live through a function, class, module or session scope.
.. _`Dependency injection`: http://en.wikipedia.org/wiki/Dependency_injection .. _`Dependency injection`: http://en.wikipedia.org/wiki/Dependency_injection
To create a value with which to call a test function a factory function To create a value with which to call a test function a factory function
is called which gets full access to the test function context and can is called which gets full access to the test function context and can
register finalizers or invoke lifecycle-caching helpers. The factory register finalizers or invoke lifecycle-caching helpers. The factory
can be implemented in same test class or test module, or in a can be implemented in same test class or test module, in a
per-directory ``conftest.py`` file or even in an external plugin. This per-directory ``conftest.py`` file or in an external plugin. This
allows full de-coupling of test code and objects needed for test allows total de-coupling of test and setup code.
execution.
A test function may be invoked multiple times in which case we A test function may be invoked multiple times in which case we
speak of :ref:`parametrized testing <parametrizing-tests>`. This can be speak of :ref:`parametrized testing <parametrizing-tests>`. This can be
@ -38,7 +39,7 @@ very useful if you want to test e.g. against different database backends
or with multiple numerical arguments sets and want to reuse the same set or with multiple numerical arguments sets and want to reuse the same set
of test functions. of test functions.
py.test comes with :ref:`builtinfuncargs` and there are some refined usages in the examples section. py.test comes with some :ref:`builtinfuncargs` and there are some refined usages in the examples section.
.. _funcarg: .. _funcarg:
@ -55,10 +56,8 @@ Let's look at a simple self-contained test module::
assert myfuncarg == 17 assert myfuncarg == 17
This test function needs an injected object named ``myfuncarg``. This test function needs an injected object named ``myfuncarg``.
py.test will discover and call the factory named py.test will automatically discover and call the ``pytest_funcarg__myfuncarg``
``pytest_funcarg__myfuncarg`` within the same module in this case. factory. Running the test looks like this::
Running the test looks like this::
$ py.test test_simplefactory.py $ py.test test_simplefactory.py
=========================== test session starts ============================ =========================== test session starts ============================
@ -79,9 +78,9 @@ Running the test looks like this::
test_simplefactory.py:5: AssertionError test_simplefactory.py:5: AssertionError
========================= 1 failed in 0.01 seconds ========================= ========================= 1 failed in 0.01 seconds =========================
This means that indeed the test function was called with a ``myfuncarg`` This shows that the test function was called with a ``myfuncarg``
argument value of ``42`` and the assert fails. Here is how py.test argument value of ``42`` and the assert fails as expected. Here is
comes to call the test function this way: how py.test comes to call the test function this way:
1. py.test :ref:`finds <test discovery>` the ``test_function`` because 1. py.test :ref:`finds <test discovery>` the ``test_function`` because
of the ``test_`` prefix. The test function needs a function argument of the ``test_`` prefix. The test function needs a function argument
@ -99,13 +98,22 @@ Note that if you misspell a function argument or want
to use one that isn't available, you'll see an error to use one that isn't available, you'll see an error
with a list of available function arguments. with a list of available function arguments.
You can always issue:: .. Note::
py.test --funcargs test_simplefactory.py You can always issue::
to see available function arguments (which you can also py.test --funcargs test_simplefactory.py
think of as "resources").
to see available function arguments.
The request object passed to factories
-----------------------------------------
Each funcarg factory receives a :py:class:`~_pytest.main.Request` object which
provides methods to manage caching and finalization in the context of the
test invocation as well as several attributes of the the underlying test item. In fact, as of version pytest-2.3, the request API is implemented on all Item
objects and therefore the request object has general :py:class:`Node attributes and methods <_pytest.main.Node>` attributes. This is a backward compatible
change so no changes are neccessary for pre-2.3 funcarg factories.
.. _`parametrizing tests, generalized`: http://tetamap.wordpress.com/2009/05/13/parametrizing-python-tests-generalized/ .. _`parametrizing tests, generalized`: http://tetamap.wordpress.com/2009/05/13/parametrizing-python-tests-generalized/
@ -116,27 +124,6 @@ think of as "resources").
.. _`funcarg factory`: .. _`funcarg factory`:
.. _factory: .. _factory:
The funcarg **request** object
=============================================
Each funcarg factory receives a **request** object tied to a specific test
function call. A request object is passed to a funcarg factory and provides
access to test configuration and context:
.. autoclass:: _pytest.python.FuncargRequest()
:members: function,cls,module,keywords,config
.. _`useful caching and finalization helpers`:
.. automethod:: FuncargRequest.addfinalizer
.. automethod:: FuncargRequest.cached_setup
.. automethod:: FuncargRequest.applymarker
.. automethod:: FuncargRequest.getfuncargvalue
.. _`test generators`: .. _`test generators`:
.. _`parametrizing-tests`: .. _`parametrizing-tests`:
.. _`parametrized test functions`: .. _`parametrized test functions`:

View File

@ -296,7 +296,6 @@ into interactive debugging when a test failure occurs.
The :py:mod:`_pytest.terminal` reported specifically uses The :py:mod:`_pytest.terminal` reported specifically uses
the reporting hook to print information about a test run. the reporting hook to print information about a test run.
Collection hooks Collection hooks
------------------------------ ------------------------------
@ -327,37 +326,44 @@ test execution:
.. autofunction: pytest_runtest_logreport .. autofunction: pytest_runtest_logreport
Reference of important objects involved in hooks Reference of objects involved in hooks
=========================================================== ===========================================================
.. autoclass:: _pytest.config.Config .. autoclass:: _pytest.main.Request()
:members: :members:
.. autoclass:: _pytest.config.Parser .. autoclass:: _pytest.config.Config()
:members: :members:
.. autoclass:: _pytest.main.Node(name, parent) .. autoclass:: _pytest.config.Parser()
:members: :members:
.. .. autoclass:: _pytest.main.Node()
.. autoclass:: _pytest.main.File(fspath, parent)
:members:
.. autoclass:: _pytest.main.Item(name, parent)
:members:
.. autoclass:: _pytest.python.Module(name, parent)
:members:
.. autoclass:: _pytest.python.Class(name, parent)
:members:
.. autoclass:: _pytest.python.Function(name, parent)
:members:
.. autoclass:: _pytest.runner.CallInfo
:members: :members:
.. autoclass:: _pytest.runner.TestReport .. autoclass:: _pytest.main.Collector()
:members:
:show-inheritance:
.. autoclass:: _pytest.main.Item()
:members:
:show-inheritance:
.. autoclass:: _pytest.python.Module()
:members:
:show-inheritance:
.. autoclass:: _pytest.python.Class()
:members:
:show-inheritance:
.. autoclass:: _pytest.python.Function()
:members:
:show-inheritance:
.. autoclass:: _pytest.runner.CallInfo()
:members:
.. autoclass:: _pytest.runner.TestReport()
:members: :members:

View File

@ -24,7 +24,7 @@ def main():
name='pytest', name='pytest',
description='py.test: simple powerful testing with Python', description='py.test: simple powerful testing with Python',
long_description = long_description, long_description = long_description,
version='2.2.5.dev4', version='2.3.0.dev1',
url='http://pytest.org', url='http://pytest.org',
license='MIT license', license='MIT license',
platforms=['unix', 'linux', 'osx', 'cygwin', 'win32'], platforms=['unix', 'linux', 'osx', 'cygwin', 'win32'],

View File

@ -12,33 +12,25 @@ def interpret(expr):
class TestBinReprIntegration: class TestBinReprIntegration:
pytestmark = needsnewassert pytestmark = needsnewassert
def pytest_funcarg__hook(self, request): def test_pytest_assertrepr_compare_called(self, testdir):
class MockHook(object): testdir.makeconftest("""
def __init__(self): l = []
self.called = False def pytest_assertrepr_compare(op, left, right):
self.args = tuple() l.append((op, left, right))
self.kwargs = dict() def pytest_funcarg__l(request):
return l
def __call__(self, op, left, right): """)
self.called = True testdir.makepyfile("""
self.op = op def test_hello():
self.left = left assert 0 == 1
self.right = right def test_check(l):
mockhook = MockHook() assert l == [("==", 0, 1)]
monkeypatch = request.getfuncargvalue("monkeypatch") """)
monkeypatch.setattr(util, '_reprcompare', mockhook) result = testdir.runpytest("-v")
return mockhook result.stdout.fnmatch_lines([
"*test_hello*FAIL*",
def test_pytest_assertrepr_compare_called(self, hook): "*test_check*PASS*",
interpret('assert 0 == 1') ])
assert hook.called
def test_pytest_assertrepr_compare_args(self, hook):
interpret('assert [0, 1] == [0, 2]')
assert hook.op == '=='
assert hook.left == [0, 1]
assert hook.right == [0, 2]
def callequal(left, right): def callequal(left, right):
return plugin.pytest_assertrepr_compare('==', left, right) return plugin.pytest_assertrepr_compare('==', left, right)

View File

@ -690,8 +690,8 @@ class TestRequest:
assert val2 == 2 assert val2 == 2
val2 = req.getfuncargvalue("other") # see about caching val2 = req.getfuncargvalue("other") # see about caching
assert val2 == 2 assert val2 == 2
req._fillfuncargs() pytest._fillfuncargs(item)
assert item.funcargs == {'something': 1} assert item.funcargs == {'something': 1, "other": 2}
def test_request_addfinalizer(self, testdir): def test_request_addfinalizer(self, testdir):
item = testdir.getitem(""" item = testdir.getitem("""
@ -700,9 +700,8 @@ class TestRequest:
request.addfinalizer(lambda: teardownlist.append(1)) request.addfinalizer(lambda: teardownlist.append(1))
def test_func(something): pass def test_func(something): pass
""") """)
req = funcargs.FuncargRequest(item) item.session._setupstate.prepare(item)
req._pyfuncitem.session._setupstate.prepare(item) # XXX pytest._fillfuncargs(item)
req._fillfuncargs()
# successively check finalization calls # successively check finalization calls
teardownlist = item.getparent(pytest.Module).obj.teardownlist teardownlist = item.getparent(pytest.Module).obj.teardownlist
ss = item.session._setupstate ss = item.session._setupstate
@ -799,7 +798,8 @@ class TestRequestCachedSetup:
req3 = funcargs.FuncargRequest(item3) req3 = funcargs.FuncargRequest(item3)
ret3a = req3.cached_setup(setup, scope="class") ret3a = req3.cached_setup(setup, scope="class")
ret3b = req3.cached_setup(setup, scope="class") ret3b = req3.cached_setup(setup, scope="class")
assert ret3a == ret3b == "hello2" assert ret3a == "hello2"
assert ret3b == "hello2"
req4 = funcargs.FuncargRequest(item4) req4 = funcargs.FuncargRequest(item4)
ret4 = req4.cached_setup(setup, scope="class") ret4 = req4.cached_setup(setup, scope="class")
assert ret4 == ret3a assert ret4 == ret3a
@ -830,11 +830,12 @@ class TestRequestCachedSetup:
ret1 = req1.cached_setup(setup, teardown, scope="function") ret1 = req1.cached_setup(setup, teardown, scope="function")
assert l == ['setup'] assert l == ['setup']
# artificial call of finalizer # artificial call of finalizer
req1._pyfuncitem.session._setupstate._callfinalizers(item1) setupstate = req1._pyfuncitem.session._setupstate
setupstate._callfinalizers(item1)
assert l == ["setup", "teardown"] assert l == ["setup", "teardown"]
ret2 = req1.cached_setup(setup, teardown, scope="function") ret2 = req1.cached_setup(setup, teardown, scope="function")
assert l == ["setup", "teardown", "setup"] assert l == ["setup", "teardown", "setup"]
req1._pyfuncitem.session._setupstate._callfinalizers(item1) setupstate._callfinalizers(item1)
assert l == ["setup", "teardown", "setup", "teardown"] assert l == ["setup", "teardown", "setup", "teardown"]
def test_request_cached_setup_two_args(self, testdir): def test_request_cached_setup_two_args(self, testdir):
@ -1092,9 +1093,9 @@ class TestMetafuncFunctional:
def pytest_generate_tests(metafunc): def pytest_generate_tests(metafunc):
metafunc.addcall(param=metafunc) metafunc.addcall(param=metafunc)
def pytest_funcarg__metafunc(request): def pytest_funcarg__metafunc(item):
assert request._pyfuncitem._genid == "0" assert item._genid == "0"
return request.param return item.param
def test_function(metafunc, pytestconfig): def test_function(metafunc, pytestconfig):
assert metafunc.config == pytestconfig assert metafunc.config == pytestconfig
@ -1588,3 +1589,61 @@ def test_issue117_sessionscopeteardown(testdir):
"*3/x*", "*3/x*",
"*ZeroDivisionError*", "*ZeroDivisionError*",
]) ])
class TestRequestAPI:
def test_addfinalizer_cachedsetup_getfuncargvalue(self, testdir):
testdir.makeconftest("""
l = []
def pytest_runtest_setup(item):
item.addfinalizer(lambda: l.append(1))
l2 = item.getfuncargvalue("l")
assert l2 is l
item.cached_setup(lambda: l.append(2), lambda val: l.append(3),
scope="function")
def pytest_funcarg__l(request):
return l
""")
testdir.makepyfile("""
def test_hello():
pass
def test_hello2(l):
assert l == [2, 3, 1, 2]
""")
result = testdir.runpytest()
assert result.ret == 0
result.stdout.fnmatch_lines([
"*2 passed*",
])
def test_runtest_setup_sees_filled_funcargs(self, testdir):
testdir.makeconftest("""
def pytest_runtest_setup(item):
assert item.funcargs is None
""")
testdir.makepyfile("""
def pytest_funcarg__a(request):
return 1
def pytest_funcarg__b(request):
return request.getfuncargvalue("a") + 1
def test_hello(b):
assert b == 2
""")
result = testdir.runpytest()
assert result.ret == 0
result.stdout.fnmatch_lines([
"*1 passed*",
])
result = testdir.makeconftest("""
import pytest
@pytest.mark.trylast
def pytest_runtest_setup(item):
assert item.funcargs == {"a": 1, "b": 2}
""")
result = testdir.runpytest()
assert result.ret == 0
result.stdout.fnmatch_lines([
"*1 passed*",
])

View File

@ -2,7 +2,6 @@ import py, pytest
import os import os
from _pytest.tmpdir import pytest_funcarg__tmpdir, TempdirHandler from _pytest.tmpdir import pytest_funcarg__tmpdir, TempdirHandler
from _pytest.python import FuncargRequest
def test_funcarg(testdir): def test_funcarg(testdir):
item = testdir.getitem(""" item = testdir.getitem("""
@ -11,12 +10,12 @@ def test_funcarg(testdir):
metafunc.addcall(id='b') metafunc.addcall(id='b')
def test_func(tmpdir): pass def test_func(tmpdir): pass
""", 'test_func[a]') """, 'test_func[a]')
p = pytest_funcarg__tmpdir(FuncargRequest(item)) p = pytest_funcarg__tmpdir(item)
assert p.check() assert p.check()
bn = p.basename.strip("0123456789") bn = p.basename.strip("0123456789")
assert bn.endswith("test_func_a_") assert bn.endswith("test_func_a_")
item.name = "qwe/\\abc" item.name = "qwe/\\abc"
p = pytest_funcarg__tmpdir(FuncargRequest(item)) p = pytest_funcarg__tmpdir(item)
assert p.check() assert p.check()
bn = p.basename.strip("0123456789") bn = p.basename.strip("0123456789")
assert bn == "qwe__abc" assert bn == "qwe__abc"

View File

@ -24,7 +24,7 @@ basepython=python2.7
deps=pytest-xdist deps=pytest-xdist
commands= commands=
py.test -n3 -rfsxX \ py.test -n3 -rfsxX \
--ignore .tox --junitxml={envlogdir}/junit-{envname}.xml [] --ignore .tox --junitxml={envlogdir}/junit-{envname}.xml testing
[testenv:trial] [testenv:trial]
changedir=. changedir=.