[py-svn] py-trunk commit 2f5788d1819f: only consider matching conftest plugins for discovering hooks related to collection nodes.

commits-noreply at bitbucket.org commits-noreply at bitbucket.org
Wed Dec 30 10:42:44 CET 2009


# HG changeset patch -- Bitbucket.org
# Project py-trunk
# URL http://bitbucket.org/hpk42/py-trunk/overview/
# User holger krekel <holger at merlinux.eu>
# Date 1262137018 -3600
# Node ID 2f5788d1819f792a110fa646a51c70e6326e9390
# Parent 4b97998e25d97dd9441dd2fed22d8ae835fb45bb
only consider matching conftest plugins for discovering hooks related to collection nodes.

--- a/py/impl/test/dist/dsession.py
+++ b/py/impl/test/dist/dsession.py
@@ -223,7 +223,7 @@ class DSession(Session):
                     nodes = self.item2nodes.setdefault(item, [])
                     assert node not in nodes
                     nodes.append(node)
-                    self.config.hook.pytest_itemstart(item=item, node=node)
+                    item.ihook.pytest_itemstart(item=item, node=node)
             tosend[:] = tosend[room:]  # update inplace
         if tosend:
             # we have some left, give it to the main loop
@@ -242,7 +242,7 @@ class DSession(Session):
                     #    "sending same item %r to multiple "
                     #    "not implemented" %(item,))
                     self.item2nodes.setdefault(item, []).append(node)
-                    self.config.hook.pytest_itemstart(item=item, node=node)
+                    item.ihook.pytest_itemstart(item=item, node=node)
                 pending.extend(sending)
                 tosend[:] = tosend[room:]  # update inplace
                 if not tosend:
@@ -267,7 +267,7 @@ class DSession(Session):
         info = "!!! Node %r crashed during running of test %r" %(node, item)
         rep = runner.ItemTestReport(item=item, excinfo=info, when="???")
         rep.node = node
-        self.config.hook.pytest_runtest_logreport(report=rep)
+        item.ihook.pytest_runtest_logreport(report=rep)
 
     def setup(self):
         """ setup any neccessary resources ahead of the test run. """

--- a/CHANGELOG
+++ b/CHANGELOG
@@ -9,6 +9,10 @@ Changes between 1.X and 1.1.1
 
 - new "pytestconfig" funcarg allows access to test config object
 
+- collection/item node specific runtest/collect hooks are only called exactly
+  on matching conftest.py files, i.e. ones which are exactly below
+  the filesystem path of an item
+
 - robustify capturing to survive if custom pytest_runtest_setup 
   code failed and prevented the capturing setup code from running. 
 

--- a/testing/pytest/test_pycollect.py
+++ b/testing/pytest/test_pycollect.py
@@ -461,3 +461,49 @@ class TestReportinfo:
                 def test_method(self):
                     pass
        """
+
+def test_setup_only_available_in_subdir(testdir):
+    sub1 = testdir.mkpydir("sub1")
+    sub2 = testdir.mkpydir("sub2")
+    sub1.join("conftest.py").write(py.code.Source("""
+        import py
+        def pytest_runtest_setup(item):
+            assert item.fspath.purebasename == "test_in_sub1"
+        def pytest_runtest_call(item):
+            assert item.fspath.purebasename == "test_in_sub1"
+        def pytest_runtest_teardown(item):
+            assert item.fspath.purebasename == "test_in_sub1"
+    """))
+    sub2.join("conftest.py").write(py.code.Source("""
+        import py
+        def pytest_runtest_setup(item):
+            assert item.fspath.purebasename == "test_in_sub2"
+        def pytest_runtest_call(item):
+            assert item.fspath.purebasename == "test_in_sub2"
+        def pytest_runtest_teardown(item):
+            assert item.fspath.purebasename == "test_in_sub2"
+    """))
+    sub1.join("test_in_sub1.py").write("def test_1(): pass")
+    sub2.join("test_in_sub2.py").write("def test_2(): pass")
+    result = testdir.runpytest("-v", "-s")
+    result.stdout.fnmatch_lines([
+        "*2 passed*"
+    ])
+
+def test_generate_tests_only_done_in_subdir(testdir):
+    sub1 = testdir.mkpydir("sub1")
+    sub2 = testdir.mkpydir("sub2")
+    sub1.join("conftest.py").write(py.code.Source("""
+        def pytest_generate_tests(metafunc):
+            assert metafunc.function.__name__ == "test_1"
+    """))
+    sub2.join("conftest.py").write(py.code.Source("""
+        def pytest_generate_tests(metafunc):
+            assert metafunc.function.__name__ == "test_2"
+    """))
+    sub1.join("test_in_sub1.py").write("def test_1(): pass")
+    sub2.join("test_in_sub2.py").write("def test_2(): pass")
+    result = testdir.runpytest("-v", "-s", sub1, sub2, sub1)
+    result.stdout.fnmatch_lines([
+        "*3 passed*"
+    ])

--- a/py/impl/test/pluginmanager.py
+++ b/py/impl/test/pluginmanager.py
@@ -136,8 +136,8 @@ class PluginManager(object):
     # API for interacting with registered and instantiated plugin objects 
     #
     # 
-    def listattr(self, attrname, plugins=None, extra=()):
-        return self.registry.listattr(attrname, plugins=plugins, extra=extra)
+    def listattr(self, attrname, plugins=None):
+        return self.registry.listattr(attrname, plugins=plugins)
 
     def notify_exception(self, excinfo=None):
         if excinfo is None:
@@ -271,12 +271,11 @@ class Registry:
     def __iter__(self):
         return iter(self._plugins)
 
-    def listattr(self, attrname, plugins=None, extra=(), reverse=False):
+    def listattr(self, attrname, plugins=None, reverse=False):
         l = []
         if plugins is None:
             plugins = self._plugins
-        candidates = list(plugins) + list(extra)
-        for plugin in candidates:
+        for plugin in plugins:
             try:
                 l.append(getattr(plugin, attrname))
             except AttributeError:
@@ -291,32 +290,29 @@ class HookRelay:
         self._registry = registry
         for name, method in vars(hookspecs).items():
             if name[:1] != "_":
-                setattr(self, name, self._makecall(name))
-
-    def _makecall(self, name, extralookup=None):
-        hookspecmethod = getattr(self._hookspecs, name)
-        firstresult = getattr(hookspecmethod, 'firstresult', False)
-        return HookCaller(self, name, firstresult=firstresult,
-            extralookup=extralookup)
-
-    def _getmethods(self, name, extralookup=()):
-        return self._registry.listattr(name, extra=extralookup)
+                firstresult = getattr(method, 'firstresult', False)
+                hc = HookCaller(self, name, firstresult=firstresult)
+                setattr(self, name, hc)
 
     def _performcall(self, name, multicall):
         return multicall.execute()
         
 class HookCaller:
-    def __init__(self, hookrelay, name, firstresult, extralookup=None):
+    def __init__(self, hookrelay, name, firstresult):
         self.hookrelay = hookrelay 
         self.name = name 
         self.firstresult = firstresult 
-        self.extralookup = extralookup and [extralookup] or ()
 
     def __repr__(self):
         return "<HookCaller %r>" %(self.name,)
 
     def __call__(self, **kwargs):
-        methods = self.hookrelay._getmethods(self.name, self.extralookup)
+        methods = self.hookrelay._registry.listattr(self.name)
+        mc = MultiCall(methods, kwargs, firstresult=self.firstresult)
+        return self.hookrelay._performcall(self.name, mc)
+
+    def pcall(self, plugins, **kwargs):
+        methods = self.hookrelay._registry.listattr(self.name, plugins=plugins)
         mc = MultiCall(methods, kwargs, firstresult=self.firstresult)
         return self.hookrelay._performcall(self.name, mc)
    

--- a/py/plugin/pytest_runner.py
+++ b/py/plugin/pytest_runner.py
@@ -39,7 +39,7 @@ def pytest_runtest_protocol(item):
     if item.config.getvalue("boxed"):
         reports = forked_run_report(item) 
         for rep in reports:
-            item.config.hook.pytest_runtest_logreport(report=rep)
+            item.ihook.pytest_runtest_logreport(report=rep)
     else:
         runtestprotocol(item)
     return True
@@ -85,7 +85,7 @@ def pytest_report_teststatus(report):
 
 def call_and_report(item, when, log=True):
     call = call_runtest_hook(item, when)
-    hook = item.config.hook
+    hook = item.ihook
     report = hook.pytest_runtest_makereport(item=item, call=call)
     if log and (when == "call" or not report.passed):
         hook.pytest_runtest_logreport(report=report) 
@@ -93,8 +93,8 @@ def call_and_report(item, when, log=True
 
 def call_runtest_hook(item, when):
     hookname = "pytest_runtest_" + when 
-    hook = getattr(item.config.hook, hookname)
-    return CallInfo(lambda: hook(item=item), when=when)
+    ihook = getattr(item.ihook, hookname)
+    return CallInfo(lambda: ihook(item=item), when=when)
 
 class CallInfo:
     excinfo = None 

--- a/py/impl/test/conftesthandle.py
+++ b/py/impl/test/conftesthandle.py
@@ -11,6 +11,7 @@ class Conftest(object):
     def __init__(self, onimport=None):
         self._path2confmods = {}
         self._onimport = onimport
+        self._conftestpath2mod = {}
 
     def setinitial(self, args):
         """ try to find a first anchor path for looking up global values
@@ -65,17 +66,20 @@ class Conftest(object):
         raise KeyError(name)
 
     def importconftest(self, conftestpath):
-        # Using caching here looks redundant since ultimately
-        # sys.modules caches already 
         assert conftestpath.check(), conftestpath
-        if not conftestpath.dirpath('__init__.py').check(file=1): 
-            # HACK: we don't want any "globally" imported conftest.py, 
-            #       prone to conflicts and subtle problems 
-            modname = str(conftestpath).replace('.', conftestpath.sep)
-            mod = conftestpath.pyimport(modname=modname)
-        else:
-            mod = conftestpath.pyimport()
-        return self._postimport(mod)
+        try:
+            return self._conftestpath2mod[conftestpath]
+        except KeyError:
+            if not conftestpath.dirpath('__init__.py').check(file=1): 
+                # HACK: we don't want any "globally" imported conftest.py, 
+                #       prone to conflicts and subtle problems 
+                modname = str(conftestpath).replace('.', conftestpath.sep)
+                mod = conftestpath.pyimport(modname=modname)
+            else:
+                mod = conftestpath.pyimport()
+            self._postimport(mod)
+            self._conftestpath2mod[conftestpath] = mod
+            return mod
 
     def _postimport(self, mod):
         if self._onimport:

--- a/py/impl/test/collect.py
+++ b/py/impl/test/collect.py
@@ -11,6 +11,18 @@ def configproperty(name):
         return self.config._getcollectclass(name, self.fspath)
     return property(fget)
 
+class HookProxy:
+    def __init__(self, node):
+        self.node = node
+    def __getattr__(self, name):
+        if name[0] == "_":
+            raise AttributeError(name)
+        hookmethod = getattr(self.node.config.hook, name)
+        def call_matching_hooks(**kwargs):
+            plugins = self.node.config.getmatchingplugins(self.node.fspath)
+            return hookmethod.pcall(plugins, **kwargs)
+        return call_matching_hooks
+
 class Node(object): 
     """ base class for Nodes in the collection tree.  
         Collector nodes have children and 
@@ -29,6 +41,7 @@ class Node(object):
         self.parent = parent
         self.config = getattr(parent, 'config', None)
         self.fspath = getattr(parent, 'fspath', None) 
+        self.ihook = HookProxy(self)
 
     def _checkcollectable(self):
         if not hasattr(self, 'fspath'):
@@ -426,13 +439,12 @@ class Directory(FSCollector):
         return res
 
     def consider_file(self, path):
-        return self.config.hook.pytest_collect_file(path=path, parent=self)
+        return self.ihook.pytest_collect_file(path=path, parent=self)
 
     def consider_dir(self, path, usefilters=None):
         if usefilters is not None:
             py.log._apiwarn("0.99", "usefilters argument not needed")
-        return self.config.hook.pytest_collect_directory(
-            path=path, parent=self)
+        return self.ihook.pytest_collect_directory(path=path, parent=self)
 
 class Item(Node): 
     """ a basic test item. """

--- a/testing/pytest/test_pluginmanager.py
+++ b/testing/pytest/test_pluginmanager.py
@@ -395,12 +395,6 @@ class TestRegistry:
         l = list(plugins.listattr('x', reverse=True))
         assert l == [43, 42, 41]
 
-        class api4: 
-            x = 44
-        l = list(plugins.listattr('x', extra=(api4,)))
-        assert l == [41,42,43,44]
-        assert len(list(plugins)) == 3  # otherwise extra added
-
 class TestHookRelay:
     def test_happypath(self):
         registry = Registry()
@@ -441,23 +435,3 @@ class TestHookRelay:
         res = mcm.hello(arg=3)
         assert res == 4
 
-    def test_hooks_extra_plugins(self):
-        registry = Registry()
-        class Api:
-            def hello(self, arg):
-                pass
-        hookrelay = HookRelay(hookspecs=Api, registry=registry)
-        hook_hello = hookrelay.hello
-        class Plugin:
-            def hello(self, arg):
-                return arg + 1
-        registry.register(Plugin())
-        class Plugin2:
-            def hello(self, arg):
-                return arg + 2
-        newhook = hookrelay._makecall("hello", extralookup=Plugin2())
-        l = newhook(arg=3)
-        assert l == [5, 4]
-        l2 = hook_hello(arg=3)
-        assert l2 == [4]
-        

--- a/py/impl/test/pycollect.py
+++ b/py/impl/test/pycollect.py
@@ -120,7 +120,7 @@ class PyCollectorMixin(PyobjMixin, py.te
             return self.join(name)
 
     def makeitem(self, name, obj):
-        return self.config.hook.pytest_pycollect_makeitem(
+        return self.ihook.pytest_pycollect_makeitem(
             collector=self, name=name, obj=obj)
 
     def _istestclasscandidate(self, name, obj):
@@ -137,9 +137,9 @@ class PyCollectorMixin(PyobjMixin, py.te
         cls = clscol and clscol.obj or None
         metafunc = funcargs.Metafunc(funcobj, config=self.config, 
             cls=cls, module=module)
-        gentesthook = self.config.hook._makecall(
-            "pytest_generate_tests", extralookup=module)
-        gentesthook(metafunc=metafunc)
+        gentesthook = self.config.hook.pytest_generate_tests
+        plugins = self.config.getmatchingplugins(self.fspath) + [module]
+        gentesthook.pcall(plugins, metafunc=metafunc)
         if not metafunc._calls:
             return self.Function(name, parent=self)
         return funcargs.FunctionCollector(name=name, 
@@ -338,7 +338,7 @@ class Function(FunctionMixin, py.test.co
 
     def runtest(self):
         """ execute the underlying test function. """
-        self.config.hook.pytest_pyfunc_call(pyfuncitem=self)
+        self.ihook.pytest_pyfunc_call(pyfuncitem=self)
 
     def setup(self):
         super(Function, self).setup()

--- a/py/impl/test/config.py
+++ b/py/impl/test/config.py
@@ -45,6 +45,13 @@ class Config(object):
         self.trace("loaded conftestmodule %r" %(conftestmodule,))
         self.pluginmanager.consider_conftest(conftestmodule)
 
+    def getmatchingplugins(self, fspath):
+        conftests = self._conftest._conftestpath2mod.values()
+        plugins = [x for x in self.pluginmanager.getplugins() 
+                        if x not in conftests]
+        plugins += self._conftest.getconftestmodules(fspath)
+        return plugins
+
     def trace(self, msg):
         if getattr(self.option, 'traceconfig', None):
             self.hook.pytest_trace(category="config", msg=msg)

--- a/py/impl/test/funcargs.py
+++ b/py/impl/test/funcargs.py
@@ -93,7 +93,7 @@ class FuncargRequest:
         self.fspath = pyfuncitem.fspath
         if hasattr(pyfuncitem, '_requestparam'):
             self.param = pyfuncitem._requestparam 
-        self._plugins = self.config.pluginmanager.getplugins()
+        self._plugins = self.config.getmatchingplugins(self.fspath)
         self._plugins.append(self.module)
         if self.instance is not None:
             self._plugins.append(self.instance)

--- a/testing/pytest/test_funcargs.py
+++ b/testing/pytest/test_funcargs.py
@@ -482,3 +482,25 @@ class TestGenfuncFunctional:
             "*test_myfunc*world*FAIL*", 
             "*1 failed, 1 passed*"
         ])
+
+
+def test_conftest_funcargs_only_available_in_subdir(testdir):
+    sub1 = testdir.mkpydir("sub1")
+    sub2 = testdir.mkpydir("sub2")
+    sub1.join("conftest.py").write(py.code.Source("""
+        import py
+        def pytest_funcarg__arg1(request):
+            py.test.raises(Exception, "request.getfuncargvalue('arg2')")
+    """))
+    sub2.join("conftest.py").write(py.code.Source("""
+        import py
+        def pytest_funcarg__arg2(request):
+            py.test.raises(Exception, "request.getfuncargvalue('arg1')")
+    """))
+
+    sub1.join("test_in_sub1.py").write("def test_1(arg1): pass")
+    sub2.join("test_in_sub2.py").write("def test_2(arg2): pass")
+    result = testdir.runpytest("-v")
+    result.stdout.fnmatch_lines([
+        "*2 passed*"
+    ])



More information about the pytest-commit mailing list