only consider matching conftest plugins for discovering hooks related to collection nodes.

--HG--
branch : trunk
This commit is contained in:
holger krekel 2009-12-30 02:36:58 +01:00
parent 9d01975c78
commit 631dfe9f13
12 changed files with 135 additions and 70 deletions

View File

@ -9,6 +9,10 @@ Changes between 1.X and 1.1.1
- new "pytestconfig" funcarg allows access to test config object - 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 - robustify capturing to survive if custom pytest_runtest_setup
code failed and prevented the capturing setup code from running. code failed and prevented the capturing setup code from running.

View File

@ -11,6 +11,18 @@ def configproperty(name):
return self.config._getcollectclass(name, self.fspath) return self.config._getcollectclass(name, self.fspath)
return property(fget) 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): class Node(object):
""" base class for Nodes in the collection tree. """ base class for Nodes in the collection tree.
Collector nodes have children and Collector nodes have children and
@ -29,6 +41,7 @@ class Node(object):
self.parent = parent self.parent = parent
self.config = getattr(parent, 'config', None) self.config = getattr(parent, 'config', None)
self.fspath = getattr(parent, 'fspath', None) self.fspath = getattr(parent, 'fspath', None)
self.ihook = HookProxy(self)
def _checkcollectable(self): def _checkcollectable(self):
if not hasattr(self, 'fspath'): if not hasattr(self, 'fspath'):
@ -426,13 +439,12 @@ class Directory(FSCollector):
return res return res
def consider_file(self, path): 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): def consider_dir(self, path, usefilters=None):
if usefilters is not None: if usefilters is not None:
py.log._apiwarn("0.99", "usefilters argument not needed") py.log._apiwarn("0.99", "usefilters argument not needed")
return self.config.hook.pytest_collect_directory( return self.ihook.pytest_collect_directory(path=path, parent=self)
path=path, parent=self)
class Item(Node): class Item(Node):
""" a basic test item. """ """ a basic test item. """

View File

@ -45,6 +45,13 @@ class Config(object):
self.trace("loaded conftestmodule %r" %(conftestmodule,)) self.trace("loaded conftestmodule %r" %(conftestmodule,))
self.pluginmanager.consider_conftest(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): def trace(self, msg):
if getattr(self.option, 'traceconfig', None): if getattr(self.option, 'traceconfig', None):
self.hook.pytest_trace(category="config", msg=msg) self.hook.pytest_trace(category="config", msg=msg)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -482,3 +482,25 @@ class TestGenfuncFunctional:
"*test_myfunc*world*FAIL*", "*test_myfunc*world*FAIL*",
"*1 failed, 1 passed*" "*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*"
])

View File

@ -395,12 +395,6 @@ class TestRegistry:
l = list(plugins.listattr('x', reverse=True)) l = list(plugins.listattr('x', reverse=True))
assert l == [43, 42, 41] 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: class TestHookRelay:
def test_happypath(self): def test_happypath(self):
registry = Registry() registry = Registry()
@ -441,23 +435,3 @@ class TestHookRelay:
res = mcm.hello(arg=3) res = mcm.hello(arg=3)
assert res == 4 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]

View File

@ -461,3 +461,49 @@ class TestReportinfo:
def test_method(self): def test_method(self):
pass 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*"
])