[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