[py-svn] commit/pytest: hpk42: mid-scale refactoring to make request API available directly on items.
Bitbucket
commits-noreply at bitbucket.org
Mon Jun 25 17:38:58 CEST 2012
1 new commit in pytest:
https://bitbucket.org/hpk42/pytest/changeset/c529c3593197/
changeset: c529c3593197
user: hpk42
date: 2012-06-25 17:35:33
summary: 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.
affected #: 16 files
diff -r 5bcd225c3b5eb502209b537546ad0f83e40de032 -r c529c3593197291538b5b55f59dd1da608433e6c CHANGELOG
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -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
to terminal to prevent the whole session from crashing
- fix xfail/skip confusion: a skip-mark or an imperative pytest.skip
@@ -23,6 +36,7 @@
pytest-django, trial and unittest integration.
- reporting refinements:
+
- pytest_report_header now receives a "startdir" so that
you can use startdir.bestrelpath(yourpath) to show
nice relative path
diff -r 5bcd225c3b5eb502209b537546ad0f83e40de032 -r c529c3593197291538b5b55f59dd1da608433e6c _pytest/__init__.py
--- a/_pytest/__init__.py
+++ b/_pytest/__init__.py
@@ -1,2 +1,2 @@
#
-__version__ = '2.2.5.dev4'
+__version__ = '2.3.0.dev1'
diff -r 5bcd225c3b5eb502209b537546ad0f83e40de032 -r c529c3593197291538b5b55f59dd1da608433e6c _pytest/capture.py
--- a/_pytest/capture.py
+++ b/_pytest/capture.py
@@ -119,7 +119,7 @@
return "", ""
def activate_funcargs(self, pyfuncitem):
- if hasattr(pyfuncitem, 'funcargs'):
+ if pyfuncitem.funcargs:
for name, capfuncarg in pyfuncitem.funcargs.items():
if name in ('capsys', 'capfd'):
assert not hasattr(self, '_capturing_funcarg')
@@ -186,7 +186,7 @@
captured output available via ``capsys.readouterr()`` method calls
which return a ``(out, err)`` tuple.
"""
- if "capfd" in request._funcargs:
+ if "capfd" in request.funcargs:
raise request.LookupError(error_capsysfderror)
return CaptureFuncarg(py.io.StdCapture)
@@ -195,7 +195,7 @@
captured output available via ``capsys.readouterr()`` method calls
which return a ``(out, err)`` tuple.
"""
- if "capsys" in request._funcargs:
+ if "capsys" in request.funcargs:
raise request.LookupError(error_capsysfderror)
if not hasattr(os, 'dup'):
pytest.skip("capfd funcarg needs os.dup")
diff -r 5bcd225c3b5eb502209b537546ad0f83e40de032 -r c529c3593197291538b5b55f59dd1da608433e6c _pytest/main.py
--- a/_pytest/main.py
+++ b/_pytest/main.py
@@ -3,6 +3,8 @@
import py
import pytest, _pytest
import os, sys, imp
+from _pytest.monkeypatch import monkeypatch
+
tracebackcutdir = py.path.local(_pytest.__file__).dirpath()
# exitcodes for the command line
@@ -144,32 +146,161 @@
def compatproperty(name):
def fget(self):
+ # deprecated - use pytest.name
return getattr(pytest, name)
- return property(fget, None, None,
- "deprecated attribute %r, use pytest.%s" % (name, name))
+ 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):
- """ 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."""
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
#: the parent collector node.
self.parent = parent
- #: the test config object
+ #: the pytest config object
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
- #: 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)
+
+ #: keywords on this node (node name is always contained)
+ self.keywords = {self.name: True}
+
+ #: fspath sensitive hook proxy used to call pytest hooks
self.ihook = self.session.gethookproxy(self.fspath)
- self.keywords = {self.name: True}
Module = compatproperty("Module")
Class = compatproperty("Class")
@@ -178,6 +309,11 @@
File = compatproperty("File")
Item = compatproperty("Item")
+ module = pyobj_property("Module")
+ cls = pyobj_property("Class")
+ instance = pyobj_property("Instance")
+
+
def _getcustomclass(self, name):
cls = getattr(self, name)
if cls != getattr(pytest, name):
@@ -193,12 +329,14 @@
# methods for ordering nodes
@property
def nodeid(self):
+ """ a ::-separated string denoting its collection tree address. """
try:
return self._nodeid
except AttributeError:
self._nodeid = x = self._makeid()
return x
+
def _makeid(self):
return self.parent.nodeid + "::" + self.name
@@ -338,15 +476,36 @@
class File(FSCollector):
""" 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
there might be multiple test invocation items.
"""
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):
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
def location(self):
try:
diff -r 5bcd225c3b5eb502209b537546ad0f83e40de032 -r c529c3593197291538b5b55f59dd1da608433e6c _pytest/pytester.py
--- a/_pytest/pytester.py
+++ b/_pytest/pytester.py
@@ -318,7 +318,7 @@
# used from runner functional tests
item = self.getitem(source)
# 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()
return runner(item)
diff -r 5bcd225c3b5eb502209b537546ad0f83e40de032 -r c529c3593197291538b5b55f59dd1da608433e6c _pytest/python.py
--- a/_pytest/python.py
+++ b/_pytest/python.py
@@ -4,11 +4,27 @@
import sys
import pytest
from py._code.code import TerminalRepr
-from _pytest.monkeypatch import monkeypatch
+from _pytest.main import Request, Item
import _pytest
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):
group = parser.getgroup("general")
group.addoption('--funcargs',
@@ -60,13 +76,21 @@
""" the pytest config object with access to command line opts."""
return request.config
+
def pytest_pyfunc_call(__multicall__, pyfuncitem):
if not __multicall__.execute():
testfunction = pyfuncitem.obj
if pyfuncitem._isyieldedfunction():
testfunction(*pyfuncitem._args)
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)
def pytest_collect_file(path, parent):
@@ -110,6 +134,7 @@
return False
class PyobjMixin(object):
+
def obj():
def fget(self):
try:
@@ -232,12 +257,15 @@
gentesthook.pcall(plugins, metafunc=metafunc)
Function = self._getcustomclass("Function")
if not metafunc._calls:
- return Function(name, parent=self)
+ return Function(name, parent=self,
+ funcargnames=metafunc.funcargnames)
l = []
for callspec in metafunc._calls:
subname = "%s[%s]" %(name, callspec.id)
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)
return l
@@ -256,6 +284,7 @@
pytestmark(funcobj)
class Module(pytest.File, PyCollectorMixin):
+ """ Collector for test classes and functions. """
def _getobj(self):
return self._memoizedcall('_obj', self._importtestmodule)
@@ -303,7 +332,7 @@
self.obj.teardown_module()
class Class(PyCollectorMixin, pytest.Collector):
-
+ """ Collector for test methods. """
def collect(self):
return [self._getcustomclass("Instance")(name="()", parent=self)]
@@ -373,7 +402,7 @@
excinfo.traceback = ntraceback.filter()
def _repr_failure_py(self, excinfo, style="long"):
- if excinfo.errisinstance(FuncargRequest.LookupError):
+ if excinfo.errisinstance(Request.LookupError):
fspath, lineno, msg = self.reportinfo()
lines, _ = inspect.getsourcelines(self.obj)
for i, line in enumerate(lines):
@@ -445,77 +474,6 @@
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):
init = getattr(obj, '__init__', None)
if init:
@@ -535,10 +493,20 @@
return argnames[startindex:-numdefaults]
return argnames[startindex:]
-def fillfuncargs(function):
+def fillfuncargs(node):
""" fill missing funcargs. """
- request = FuncargRequest(pyfuncitem=function)
- request._fillfuncargs()
+ if not isinstance(node, Function):
+ 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()
@@ -697,195 +665,6 @@
l.append(str(val))
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):
from _pytest.main import wrap_session
@@ -903,8 +682,8 @@
for plugin in plugins:
available = []
for name, factory in vars(plugin).items():
- if name.startswith(FuncargRequest._argprefix):
- name = name[len(FuncargRequest._argprefix):]
+ if name.startswith(Request._argprefix):
+ name = name[len(Request._argprefix):]
if name not in available:
available.append([name, factory])
if available:
@@ -1009,3 +788,131 @@
self.excinfo.__init__(tp)
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
diff -r 5bcd225c3b5eb502209b537546ad0f83e40de032 -r c529c3593197291538b5b55f59dd1da608433e6c _pytest/resultlog.py
--- a/_pytest/resultlog.py
+++ b/_pytest/resultlog.py
@@ -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
diff -r 5bcd225c3b5eb502209b537546ad0f83e40de032 -r c529c3593197291538b5b55f59dd1da608433e6c _pytest/tmpdir.py
--- a/_pytest/tmpdir.py
+++ b/_pytest/tmpdir.py
@@ -54,15 +54,15 @@
mp.setattr(config, '_tmpdirhandler', t, 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
which is unique to each test function invocation,
created as a sub directory of the base temporary
directory. The returned object is a `py.path.local`_
path object.
"""
- name = request._pyfuncitem.name
+ name = item.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
diff -r 5bcd225c3b5eb502209b537546ad0f83e40de032 -r c529c3593197291538b5b55f59dd1da608433e6c doc/en/conf.py
--- a/doc/en/conf.py
+++ b/doc/en/conf.py
@@ -17,7 +17,7 @@
#
# The full version, including alpha/beta/rc tags.
# The short X.Y version.
-version = release = "2.2.4.3"
+version = release = "2.3.0.dev1"
import sys, os
@@ -26,6 +26,8 @@
# documentation root, use os.path.abspath to make it absolute, like shown here.
#sys.path.insert(0, os.path.abspath('.'))
+autodoc_member_order = "bysource"
+
# -- General configuration -----------------------------------------------------
# If your documentation needs a minimal Sphinx version, state it here.
@@ -53,6 +55,7 @@
copyright = u'2011, holger krekel et alii'
+
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
#language = None
@@ -78,7 +81,7 @@
# If true, the current module name will be prepended to all description
# unit titles (such as .. function::).
-#add_module_names = True
+add_module_names = False
# If true, sectionauthor and moduleauthor directives will be shown in the
# output. They are ignored by default.
@@ -87,6 +90,8 @@
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = 'sphinx'
+
+
# A list of ignored prefixes for module index sorting.
#modindex_common_prefix = []
diff -r 5bcd225c3b5eb502209b537546ad0f83e40de032 -r c529c3593197291538b5b55f59dd1da608433e6c doc/en/funcargs.txt
--- a/doc/en/funcargs.txt
+++ b/doc/en/funcargs.txt
@@ -11,26 +11,27 @@
Dependency injection through function arguments
=================================================
-py.test lets you inject objects into test functions and precisely
-control their life cycle in relation to the test execution. It is
-also possible to run a test function multiple times with different objects.
+py.test lets you inject objects into test invocations and precisely
+control their life cycle in relation to the overall test execution. Moreover,
+you can run a test function multiple times injecting different objects.
The basic mechanism for injecting objects is also called the
*funcarg mechanism* because objects are ultimately injected
by calling a test function with it as an argument. Unlike the
classical xUnit approach *funcargs* relate more to `Dependency Injection`_
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
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
register finalizers or invoke lifecycle-caching helpers. The factory
-can be implemented in same test class or test module, or in a
-per-directory ``conftest.py`` file or even in an external plugin. This
-allows full de-coupling of test code and objects needed for test
-execution.
+can be implemented in same test class or test module, in a
+per-directory ``conftest.py`` file or in an external plugin. This
+allows total de-coupling of test and setup code.
A test function may be invoked multiple times in which case we
speak of :ref:`parametrized testing <parametrizing-tests>`. This can be
@@ -38,7 +39,7 @@
or with multiple numerical arguments sets and want to reuse the same set
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:
@@ -55,10 +56,8 @@
assert myfuncarg == 17
This test function needs an injected object named ``myfuncarg``.
-py.test will discover and call the factory named
-``pytest_funcarg__myfuncarg`` within the same module in this case.
-
-Running the test looks like this::
+py.test will automatically discover and call the ``pytest_funcarg__myfuncarg``
+factory. Running the test looks like this::
$ py.test test_simplefactory.py
=========================== test session starts ============================
@@ -79,9 +78,9 @@
test_simplefactory.py:5: AssertionError
========================= 1 failed in 0.01 seconds =========================
-This means that indeed the test function was called with a ``myfuncarg``
-argument value of ``42`` and the assert fails. Here is how py.test
-comes to call the test function this way:
+This shows that the test function was called with a ``myfuncarg``
+argument value of ``42`` and the assert fails as expected. Here is
+how py.test comes to call the test function this way:
1. py.test :ref:`finds <test discovery>` the ``test_function`` because
of the ``test_`` prefix. The test function needs a function argument
@@ -99,13 +98,22 @@
to use one that isn't available, you'll see an error
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
-think of as "resources").
+ py.test --funcargs test_simplefactory.py
+ 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/
@@ -116,27 +124,6 @@
.. _`funcarg 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`:
.. _`parametrizing-tests`:
.. _`parametrized test functions`:
diff -r 5bcd225c3b5eb502209b537546ad0f83e40de032 -r c529c3593197291538b5b55f59dd1da608433e6c doc/en/plugins.txt
--- a/doc/en/plugins.txt
+++ b/doc/en/plugins.txt
@@ -296,7 +296,6 @@
The :py:mod:`_pytest.terminal` reported specifically uses
the reporting hook to print information about a test run.
-
Collection hooks
------------------------------
@@ -327,37 +326,44 @@
.. 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:
-.. autoclass:: _pytest.config.Parser
+.. autoclass:: _pytest.config.Config()
:members:
-.. autoclass:: _pytest.main.Node(name, parent)
+.. autoclass:: _pytest.config.Parser()
:members:
-..
- .. 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
+.. autoclass:: _pytest.main.Node()
: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:
+
diff -r 5bcd225c3b5eb502209b537546ad0f83e40de032 -r c529c3593197291538b5b55f59dd1da608433e6c setup.py
--- a/setup.py
+++ b/setup.py
@@ -24,7 +24,7 @@
name='pytest',
description='py.test: simple powerful testing with Python',
long_description = long_description,
- version='2.2.5.dev4',
+ version='2.3.0.dev1',
url='http://pytest.org',
license='MIT license',
platforms=['unix', 'linux', 'osx', 'cygwin', 'win32'],
diff -r 5bcd225c3b5eb502209b537546ad0f83e40de032 -r c529c3593197291538b5b55f59dd1da608433e6c testing/test_assertion.py
--- a/testing/test_assertion.py
+++ b/testing/test_assertion.py
@@ -12,33 +12,25 @@
class TestBinReprIntegration:
pytestmark = needsnewassert
- def pytest_funcarg__hook(self, request):
- class MockHook(object):
- def __init__(self):
- self.called = False
- self.args = tuple()
- self.kwargs = dict()
-
- def __call__(self, op, left, right):
- self.called = True
- self.op = op
- self.left = left
- self.right = right
- mockhook = MockHook()
- monkeypatch = request.getfuncargvalue("monkeypatch")
- monkeypatch.setattr(util, '_reprcompare', mockhook)
- return mockhook
-
- def test_pytest_assertrepr_compare_called(self, hook):
- 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 test_pytest_assertrepr_compare_called(self, testdir):
+ testdir.makeconftest("""
+ l = []
+ def pytest_assertrepr_compare(op, left, right):
+ l.append((op, left, right))
+ def pytest_funcarg__l(request):
+ return l
+ """)
+ testdir.makepyfile("""
+ def test_hello():
+ assert 0 == 1
+ def test_check(l):
+ assert l == [("==", 0, 1)]
+ """)
+ result = testdir.runpytest("-v")
+ result.stdout.fnmatch_lines([
+ "*test_hello*FAIL*",
+ "*test_check*PASS*",
+ ])
def callequal(left, right):
return plugin.pytest_assertrepr_compare('==', left, right)
diff -r 5bcd225c3b5eb502209b537546ad0f83e40de032 -r c529c3593197291538b5b55f59dd1da608433e6c testing/test_python.py
--- a/testing/test_python.py
+++ b/testing/test_python.py
@@ -690,8 +690,8 @@
assert val2 == 2
val2 = req.getfuncargvalue("other") # see about caching
assert val2 == 2
- req._fillfuncargs()
- assert item.funcargs == {'something': 1}
+ pytest._fillfuncargs(item)
+ assert item.funcargs == {'something': 1, "other": 2}
def test_request_addfinalizer(self, testdir):
item = testdir.getitem("""
@@ -700,9 +700,8 @@
request.addfinalizer(lambda: teardownlist.append(1))
def test_func(something): pass
""")
- req = funcargs.FuncargRequest(item)
- req._pyfuncitem.session._setupstate.prepare(item) # XXX
- req._fillfuncargs()
+ item.session._setupstate.prepare(item)
+ pytest._fillfuncargs(item)
# successively check finalization calls
teardownlist = item.getparent(pytest.Module).obj.teardownlist
ss = item.session._setupstate
@@ -799,7 +798,8 @@
req3 = funcargs.FuncargRequest(item3)
ret3a = 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)
ret4 = req4.cached_setup(setup, scope="class")
assert ret4 == ret3a
@@ -830,11 +830,12 @@
ret1 = req1.cached_setup(setup, teardown, scope="function")
assert l == ['setup']
# artificial call of finalizer
- req1._pyfuncitem.session._setupstate._callfinalizers(item1)
+ setupstate = req1._pyfuncitem.session._setupstate
+ setupstate._callfinalizers(item1)
assert l == ["setup", "teardown"]
ret2 = req1.cached_setup(setup, teardown, scope="function")
assert l == ["setup", "teardown", "setup"]
- req1._pyfuncitem.session._setupstate._callfinalizers(item1)
+ setupstate._callfinalizers(item1)
assert l == ["setup", "teardown", "setup", "teardown"]
def test_request_cached_setup_two_args(self, testdir):
@@ -1092,9 +1093,9 @@
def pytest_generate_tests(metafunc):
metafunc.addcall(param=metafunc)
- def pytest_funcarg__metafunc(request):
- assert request._pyfuncitem._genid == "0"
- return request.param
+ def pytest_funcarg__metafunc(item):
+ assert item._genid == "0"
+ return item.param
def test_function(metafunc, pytestconfig):
assert metafunc.config == pytestconfig
@@ -1588,3 +1589,61 @@
"*3/x*",
"*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*",
+ ])
+
+
diff -r 5bcd225c3b5eb502209b537546ad0f83e40de032 -r c529c3593197291538b5b55f59dd1da608433e6c testing/test_tmpdir.py
--- a/testing/test_tmpdir.py
+++ b/testing/test_tmpdir.py
@@ -2,7 +2,6 @@
import os
from _pytest.tmpdir import pytest_funcarg__tmpdir, TempdirHandler
-from _pytest.python import FuncargRequest
def test_funcarg(testdir):
item = testdir.getitem("""
@@ -11,12 +10,12 @@
metafunc.addcall(id='b')
def test_func(tmpdir): pass
""", 'test_func[a]')
- p = pytest_funcarg__tmpdir(FuncargRequest(item))
+ p = pytest_funcarg__tmpdir(item)
assert p.check()
bn = p.basename.strip("0123456789")
assert bn.endswith("test_func_a_")
item.name = "qwe/\\abc"
- p = pytest_funcarg__tmpdir(FuncargRequest(item))
+ p = pytest_funcarg__tmpdir(item)
assert p.check()
bn = p.basename.strip("0123456789")
assert bn == "qwe__abc"
diff -r 5bcd225c3b5eb502209b537546ad0f83e40de032 -r c529c3593197291538b5b55f59dd1da608433e6c tox.ini
--- a/tox.ini
+++ b/tox.ini
@@ -24,7 +24,7 @@
deps=pytest-xdist
commands=
py.test -n3 -rfsxX \
- --ignore .tox --junitxml={envlogdir}/junit-{envname}.xml []
+ --ignore .tox --junitxml={envlogdir}/junit-{envname}.xml testing
[testenv:trial]
changedir=.
Repository URL: https://bitbucket.org/hpk42/pytest/
--
This is a commit notification from bitbucket.org. You are receiving
this because you have the service enabled, addressing the recipient of
this email.
More information about the pytest-commit
mailing list