From ea5fb0c153e09f772255fbc156826f4415b02122 Mon Sep 17 00:00:00 2001 From: holger krekel Date: Wed, 1 Oct 2014 12:19:11 +0200 Subject: [PATCH] refine internal management of plugins and conftest files --- _pytest/config.py | 7 ++---- _pytest/core.py | 60 ++++++++++++++++++++++++++++++++--------------- _pytest/main.py | 41 +++++++++++++++++++++++++------- _pytest/python.py | 14 ++++++----- 4 files changed, 84 insertions(+), 38 deletions(-) diff --git a/_pytest/config.py b/_pytest/config.py index 89d38f8af..450693036 100644 --- a/_pytest/config.py +++ b/_pytest/config.py @@ -682,11 +682,8 @@ class Config(object): setattr(self.option, opt.dest, opt.default) def _getmatchingplugins(self, fspath): - allconftests = self._conftest._conftestpath2mod.values() - plugins = [x for x in self.pluginmanager.getplugins() - if x not in allconftests] - plugins += self._conftest.getconftestmodules(fspath) - return plugins + return self.pluginmanager._plugins + \ + self._conftest.getconftestmodules(fspath) def pytest_load_initial_conftests(self, early_config): self._conftest.setinitial(early_config.known_args_namespace) diff --git a/_pytest/core.py b/_pytest/core.py index de422e149..d1fdf1dba 100644 --- a/_pytest/core.py +++ b/_pytest/core.py @@ -72,6 +72,7 @@ class PluginManager(object): self._name2plugin = {} self._listattrcache = {} self._plugins = [] + self._conftestplugins = [] self._warnings = [] self.trace = TagTracer().get("pluginmanage") self._plugin_distinfo = [] @@ -86,7 +87,7 @@ class PluginManager(object): assert not hasattr(self, "_registercallback") self._registercallback = callback - def register(self, plugin, name=None, prepend=False): + def register(self, plugin, name=None, prepend=False, conftest=False): if self._name2plugin.get(name, None) == -1: return name = name or getattr(plugin, '__name__', str(id(plugin))) @@ -98,16 +99,22 @@ class PluginManager(object): reg = getattr(self, "_registercallback", None) if reg is not None: reg(plugin, name) - if not prepend: - self._plugins.append(plugin) + if conftest: + self._conftestplugins.append(plugin) else: - self._plugins.insert(0, plugin) + if not prepend: + self._plugins.append(plugin) + else: + self._plugins.insert(0, plugin) return True def unregister(self, plugin=None, name=None): if plugin is None: plugin = self.getplugin(name=name) - self._plugins.remove(plugin) + try: + self._plugins.remove(plugin) + except KeyError: + self._conftestplugins.remove(plugin) for name, value in list(self._name2plugin.items()): if value == plugin: del self._name2plugin[name] @@ -119,7 +126,7 @@ class PluginManager(object): while self._shutdown: func = self._shutdown.pop() func() - self._plugins = [] + self._plugins = self._conftestplugins = [] self._name2plugin.clear() self._listattrcache.clear() @@ -134,7 +141,7 @@ class PluginManager(object): self.hook._addhooks(spec, prefix=prefix) def getplugins(self): - return list(self._plugins) + return self._plugins + self._conftestplugins def skipifmissing(self, name): if not self.hasplugin(name): @@ -198,7 +205,8 @@ class PluginManager(object): self.import_plugin(arg) def consider_conftest(self, conftestmodule): - if self.register(conftestmodule, name=conftestmodule.__file__): + if self.register(conftestmodule, name=conftestmodule.__file__, + conftest=True): self.consider_module(conftestmodule) def consider_module(self, mod): @@ -233,12 +241,7 @@ class PluginManager(object): def listattr(self, attrname, plugins=None): if plugins is None: - plugins = self._plugins - key = (attrname,) + tuple(plugins) - try: - return list(self._listattrcache[key]) - except KeyError: - pass + plugins = self._plugins + self._conftestplugins l = [] last = [] wrappers = [] @@ -257,7 +260,7 @@ class PluginManager(object): l.append(meth) l.extend(last) l.extend(wrappers) - self._listattrcache[key] = list(l) + #self._listattrcache[key] = list(l) return l def call_plugin(self, plugin, methname, kwargs): @@ -397,6 +400,29 @@ class HookRelay: raise ValueError("did not find new %r hooks in %r" %( prefix, hookspecs,)) + def _getcaller(self, name, plugins): + caller = getattr(self, name) + methods = self._pm.listattr(name, plugins=plugins) + return CachedHookCaller(caller, methods) + + +class CachedHookCaller: + def __init__(self, hookmethod, methods): + self.hookmethod = hookmethod + self.methods = methods + + def __call__(self, **kwargs): + return self.hookmethod._docall(self.methods, kwargs) + + def callextra(self, methods, **kwargs): + # XXX in theory we should respect "tryfirst/trylast" if set + # on the added methods but we currently only use it for + # pytest_generate_tests and it doesn't make sense there i'd think + all = self.methods + if methods: + all = all + methods + return self.hookmethod._docall(all, kwargs) + class HookCaller: def __init__(self, hookrelay, name, firstresult): @@ -412,10 +438,6 @@ class HookCaller: methods = self.hookrelay._pm.listattr(self.name) return self._docall(methods, kwargs) - def pcall(self, plugins, **kwargs): - methods = self.hookrelay._pm.listattr(self.name, plugins=plugins) - return self._docall(methods, kwargs) - def _docall(self, methods, kwargs): self.trace(self.name, kwargs) self.trace.root.indent += 1 diff --git a/_pytest/main.py b/_pytest/main.py index 2d46c56e8..5377764de 100644 --- a/_pytest/main.py +++ b/_pytest/main.py @@ -153,19 +153,39 @@ def pytest_ignore_collect(path, config): ignore_paths.extend([py.path.local(x) for x in excludeopt]) return path in ignore_paths -class HookProxy(object): +class FSHookProxy(object): def __init__(self, fspath, config): self.fspath = fspath self.config = config def __getattr__(self, name): - config = object.__getattribute__(self, "config") - hookmethod = getattr(config.hook, name) + plugins = self.config._getmatchingplugins(self.fspath) + x = self.config.hook._getcaller(name, plugins) + self.__dict__[name] = x + return x - def call_matching_hooks(**kwargs): - plugins = self.config._getmatchingplugins(self.fspath) - return hookmethod.pcall(plugins, **kwargs) - return call_matching_hooks + hookmethod = getattr(self.config.hook, name) + methods = self.config.pluginmanager.listattr(name, plugins=plugins) + self.__dict__[name] = x = HookCaller(hookmethod, methods) + return x + + +class HookCaller: + def __init__(self, hookmethod, methods): + self.hookmethod = hookmethod + self.methods = methods + + def __call__(self, **kwargs): + return self.hookmethod._docall(self.methods, kwargs) + + def callextra(self, methods, **kwargs): + # XXX in theory we should respect "tryfirst/trylast" if set + # on the added methods but we currently only use it for + # pytest_generate_tests and it doesn't make sense there i'd think + all = self.methods + if methods: + all = all + methods + return self.hookmethod._docall(all, kwargs) def compatproperty(name): def fget(self): @@ -520,6 +540,7 @@ class Session(FSCollector): self.trace = config.trace.root.get("collection") self._norecursepatterns = config.getini("norecursedirs") self.startdir = py.path.local() + self._fs2hookproxy = {} def pytest_collectstart(self): if self.shouldstop: @@ -538,7 +559,11 @@ class Session(FSCollector): return path in self._initialpaths def gethookproxy(self, fspath): - return HookProxy(fspath, self.config) + try: + return self._fs2hookproxy[fspath] + except KeyError: + self._fs2hookproxy[fspath] = x = FSHookProxy(fspath, self.config) + return x def perform_collect(self, args=None, genitems=True): hook = self.config.hook diff --git a/_pytest/python.py b/_pytest/python.py index 9ce1d2170..997793f76 100644 --- a/_pytest/python.py +++ b/_pytest/python.py @@ -353,12 +353,14 @@ class PyCollector(PyobjMixin, pytest.Collector): fixtureinfo = fm.getfixtureinfo(self, funcobj, cls) metafunc = Metafunc(funcobj, fixtureinfo, self.config, cls=cls, module=module) - gentesthook = self.config.hook.pytest_generate_tests - extra = [module] - if cls is not None: - extra.append(cls()) - plugins = self.getplugins() + extra - gentesthook.pcall(plugins, metafunc=metafunc) + try: + methods = [module.pytest_generate_tests] + except AttributeError: + methods = [] + if hasattr(cls, "pytest_generate_tests"): + methods.append(cls().pytest_generate_tests) + self.ihook.pytest_generate_tests.callextra(methods, metafunc=metafunc) + Function = self._getcustomclass("Function") if not metafunc._calls: yield Function(name, parent=self)