From 715a235b45debf5e71e1409a826d91833014f4b5 Mon Sep 17 00:00:00 2001 From: holger krekel Date: Wed, 22 Apr 2015 16:33:20 +0200 Subject: [PATCH] remove shutdown logic from PluginManager and add a add_cleanup() API for the already existing cleanup logic of the config object. This simplifies lifecycle management as we don't keep two layers of shutdown functions and also simplifies the pluginmanager interface. also add some docstrings. --HG-- branch : plugin_no_pytest --- CHANGELOG | 9 ++++++++ _pytest/assertion/__init__.py | 11 +++++----- _pytest/capture.py | 4 ++-- _pytest/config.py | 39 +++++++++++++++++------------------ _pytest/core.py | 35 ++++++++++++++++--------------- _pytest/helpconfig.py | 26 ++++++++++------------- _pytest/main.py | 6 ++---- _pytest/mark.py | 4 ++-- _pytest/pytester.py | 14 +++++-------- testing/conftest.py | 1 + testing/test_core.py | 4 ++-- testing/test_session.py | 4 ++-- 12 files changed, 78 insertions(+), 79 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 5bf604f73..3fae02046 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -18,6 +18,15 @@ from ``inline_run()`` to allow temporary modules to be reloaded. Thanks Eduardo Schettino. +- internally refactor pluginmanager API and code so that there + is a clear distinction between a pytest-agnostic rather simple + pluginmanager and the PytestPluginManager which adds a lot of + behaviour, among it handling of the local conftest files. + In terms of documented methods this is a backward compatible + change but it might still break 3rd party plugins which relied on + details like especially the pluginmanager.add_shutdown() API. + Thanks Holger Krekel. + 2.7.1.dev (compared to 2.7.0) ----------------------------- diff --git a/_pytest/assertion/__init__.py b/_pytest/assertion/__init__.py index ef3a63f95..aa37378f3 100644 --- a/_pytest/assertion/__init__.py +++ b/_pytest/assertion/__init__.py @@ -70,12 +70,11 @@ def pytest_configure(config): config._assertstate = AssertionState(config, mode) config._assertstate.hook = hook config._assertstate.trace("configured with mode set to %r" % (mode,)) - - -def pytest_unconfigure(config): - hook = config._assertstate.hook - if hook is not None and hook in sys.meta_path: - sys.meta_path.remove(hook) + def undo(): + hook = config._assertstate.hook + if hook is not None and hook in sys.meta_path: + sys.meta_path.remove(hook) + config.add_cleanup(undo) def pytest_collection(session): diff --git a/_pytest/capture.py b/_pytest/capture.py index 0042b274b..047e1ca7e 100644 --- a/_pytest/capture.py +++ b/_pytest/capture.py @@ -37,13 +37,13 @@ def pytest_load_initial_conftests(early_config, parser, args): pluginmanager.register(capman, "capturemanager") # make sure that capturemanager is properly reset at final shutdown - pluginmanager.add_shutdown(capman.reset_capturings) + early_config.add_cleanup(capman.reset_capturings) # make sure logging does not raise exceptions at the end def silence_logging_at_shutdown(): if "logging" in sys.modules: sys.modules["logging"].raiseExceptions = False - pluginmanager.add_shutdown(silence_logging_at_shutdown) + early_config.add_cleanup(silence_logging_at_shutdown) # finally trigger conftest loading but while capturing (issue93) capman.init_capturings() diff --git a/_pytest/config.py b/_pytest/config.py index b174ab073..7644e6698 100644 --- a/_pytest/config.py +++ b/_pytest/config.py @@ -77,20 +77,17 @@ def _prepareconfig(args=None, plugins=None): raise ValueError("not a string or argument list: %r" % (args,)) args = shlex.split(args) pluginmanager = get_plugin_manager() - try: - if plugins: - for plugin in plugins: - pluginmanager.register(plugin) - return pluginmanager.hook.pytest_cmdline_parse( - pluginmanager=pluginmanager, args=args) - except Exception: - pluginmanager.ensure_shutdown() - raise + if plugins: + for plugin in plugins: + pluginmanager.register(plugin) + return pluginmanager.hook.pytest_cmdline_parse( + pluginmanager=pluginmanager, args=args) def exclude_pytest_names(name): return not name.startswith(name) or name == "pytest_plugins" or \ name.startswith("pytest_funcarg__") + class PytestPluginManager(PluginManager): def __init__(self): super(PytestPluginManager, self).__init__(prefix="pytest_", @@ -723,16 +720,23 @@ class Config(object): if self._configured: call_plugin(plugin, "pytest_configure", {'config': self}) - def do_configure(self): + def add_cleanup(self, func): + """ Add a function to be called when the config object gets out of + use (usually coninciding with pytest_unconfigure).""" + self._cleanup.append(func) + + def _do_configure(self): assert not self._configured self._configured = True self.hook.pytest_configure(config=self) - def do_unconfigure(self): - assert self._configured - self._configured = False - self.hook.pytest_unconfigure(config=self) - self.pluginmanager.ensure_shutdown() + def _ensure_unconfigure(self): + if self._configured: + self._configured = False + self.hook.pytest_unconfigure(config=self) + while self._cleanup: + fin = self._cleanup.pop() + fin() def warn(self, code, message): """ generate a warning for this test session. """ @@ -747,11 +751,6 @@ class Config(object): self.parse(args) return self - def pytest_unconfigure(config): - while config._cleanup: - fin = config._cleanup.pop() - fin() - def notify_exception(self, excinfo, option=None): if option and option.fulltrace: style = "long" diff --git a/_pytest/core.py b/_pytest/core.py index 8400fe53c..492450061 100644 --- a/_pytest/core.py +++ b/_pytest/core.py @@ -135,6 +135,21 @@ class CallOutcome: class PluginManager(object): + """ Core Pluginmanager class which manages registration + of plugin objects and 1:N hook calling. + + You can register new hooks by calling ``addhooks(module_or_class)``. + You can register plugin objects (which contain hooks) by calling + ``register(plugin)``. The Pluginmanager is initialized with a + prefix that is searched for in the names of the dict of registered + plugin objects. An optional excludefunc allows to blacklist names which + are not considered as hooks despite a matching prefix. + + For debugging purposes you can call ``set_tracing(writer)`` + which will subsequently send debug information to the specified + write function. + """ + def __init__(self, prefix, excludefunc=None): self._prefix = prefix self._excludefunc = excludefunc @@ -142,10 +157,11 @@ class PluginManager(object): self._plugins = [] self._plugin2hookcallers = {} self.trace = TagTracer().get("pluginmanage") - self._shutdown = [] self.hook = HookRelay(pm=self) def set_tracing(self, writer): + """ turn on tracing to the given writer method and + return an undo function. """ self.trace.root.setwriter(writer) # reconfigure HookCalling to perform tracing assert not hasattr(self, "_wrapping") @@ -160,12 +176,7 @@ class PluginManager(object): trace("finish", self.name, "-->", box.result) trace.root.indent -= 1 - undo = add_method_wrapper(HookCaller, _docall) - self.add_shutdown(undo) - - def do_configure(self, config): - # backward compatibility - config.do_configure() + return add_method_wrapper(HookCaller, _docall) def make_hook_caller(self, name, plugins): caller = getattr(self.hook, name) @@ -233,16 +244,6 @@ class PluginManager(object): for hookcaller in hookcallers: hookcaller.scan_methods() - def add_shutdown(self, func): - self._shutdown.append(func) - - def ensure_shutdown(self): - while self._shutdown: - func = self._shutdown.pop() - func() - self._plugins = [] - self._name2plugin.clear() - def addhooks(self, module_or_class): isclass = int(inspect.isclass(module_or_class)) names = [] diff --git a/_pytest/helpconfig.py b/_pytest/helpconfig.py index d79fc671a..7976ae826 100644 --- a/_pytest/helpconfig.py +++ b/_pytest/helpconfig.py @@ -28,24 +28,20 @@ def pytest_cmdline_parse(): config = outcome.get_result() if config.option.debug: path = os.path.abspath("pytestdebug.log") - f = open(path, 'w') - config._debugfile = f - f.write("versions pytest-%s, py-%s, " + debugfile = open(path, 'w') + debugfile.write("versions pytest-%s, py-%s, " "python-%s\ncwd=%s\nargs=%s\n\n" %( pytest.__version__, py.__version__, ".".join(map(str, sys.version_info)), os.getcwd(), config._origargs)) - config.pluginmanager.set_tracing(f.write) + config.pluginmanager.set_tracing(debugfile.write) sys.stderr.write("writing pytestdebug information to %s\n" % path) - -@pytest.mark.trylast -def pytest_unconfigure(config): - if hasattr(config, '_debugfile'): - config._debugfile.close() - sys.stderr.write("wrote pytestdebug information to %s\n" % - config._debugfile.name) - config.trace.root.setwriter(None) - + def unset_tracing(): + debugfile.close() + sys.stderr.write("wrote pytestdebug information to %s\n" % + debugfile.name) + config.trace.root.setwriter(None) + config.add_cleanup(unset_tracing) def pytest_cmdline_main(config): if config.option.version: @@ -58,9 +54,9 @@ def pytest_cmdline_main(config): sys.stderr.write(line + "\n") return 0 elif config.option.help: - config.do_configure() + config._do_configure() showhelp(config) - config.do_unconfigure() + config._ensure_unconfigure() return 0 def showhelp(config): diff --git a/_pytest/main.py b/_pytest/main.py index 96505e0da..ed7d6aad9 100644 --- a/_pytest/main.py +++ b/_pytest/main.py @@ -77,7 +77,7 @@ def wrap_session(config, doit): initstate = 0 try: try: - config.do_configure() + config._do_configure() initstate = 1 config.hook.pytest_sessionstart(session=session) initstate = 2 @@ -107,9 +107,7 @@ def wrap_session(config, doit): config.hook.pytest_sessionfinish( session=session, exitstatus=session.exitstatus) - if initstate >= 1: - config.do_unconfigure() - config.pluginmanager.ensure_shutdown() + config._ensure_unconfigure() return session.exitstatus def pytest_cmdline_main(config): diff --git a/_pytest/mark.py b/_pytest/mark.py index 1d5043578..817dc72fe 100644 --- a/_pytest/mark.py +++ b/_pytest/mark.py @@ -44,14 +44,14 @@ def pytest_addoption(parser): def pytest_cmdline_main(config): if config.option.markers: - config.do_configure() + config._do_configure() tw = py.io.TerminalWriter() for line in config.getini("markers"): name, rest = line.split(":", 1) tw.write("@pytest.mark.%s:" % name, bold=True) tw.line(rest) tw.line() - config.do_unconfigure() + config._ensure_unconfigure() return 0 pytest_cmdline_main.tryfirst = True diff --git a/_pytest/pytester.py b/_pytest/pytester.py index fea5aff2a..c22beb8f4 100644 --- a/_pytest/pytester.py +++ b/_pytest/pytester.py @@ -65,7 +65,8 @@ class HookRecorder: self.calls.append(ParsedCall(hookcaller.name, kwargs)) yield self._undo_wrapping = add_method_wrapper(HookCaller, _docall) - pluginmanager.add_shutdown(self._undo_wrapping) + #if hasattr(pluginmanager, "config"): + # pluginmanager.add_shutdown(self._undo_wrapping) def finish_recording(self): self._undo_wrapping() @@ -571,12 +572,7 @@ class TmpTestdir: # we don't know what the test will do with this half-setup config # object and thus we make sure it gets unconfigured properly in any # case (otherwise capturing could still be active, for example) - def ensure_unconfigure(): - if hasattr(config.pluginmanager, "_config"): - config.pluginmanager.do_unconfigure(config) - config.pluginmanager.ensure_shutdown() - - self.request.addfinalizer(ensure_unconfigure) + self.request.addfinalizer(config._ensure_unconfigure) return config def parseconfigure(self, *args): @@ -588,8 +584,8 @@ class TmpTestdir: """ config = self.parseconfig(*args) - config.do_configure() - self.request.addfinalizer(config.do_unconfigure) + config._do_configure() + self.request.addfinalizer(config._ensure_unconfigure) return config def getitem(self, source, funcname="test_func"): diff --git a/testing/conftest.py b/testing/conftest.py index 8bf467866..08aefbbd5 100644 --- a/testing/conftest.py +++ b/testing/conftest.py @@ -66,6 +66,7 @@ def check_open_files(config): error.append(error[0]) raise AssertionError("\n".join(error)) +@pytest.mark.trylast def pytest_runtest_teardown(item, __multicall__): item.config._basedir.chdir() if hasattr(item.config, '_openfiles'): diff --git a/testing/test_core.py b/testing/test_core.py index 14631a48c..147d3fc8f 100644 --- a/testing/test_core.py +++ b/testing/test_core.py @@ -155,13 +155,13 @@ class TestPytestPluginInteractions: config.pluginmanager.register(A()) assert len(l) == 0 - config.do_configure() + config._do_configure() assert len(l) == 1 config.pluginmanager.register(A()) # leads to a configured() plugin assert len(l) == 2 assert l[0] != l[1] - config.do_unconfigure() + config._ensure_unconfigure() config.pluginmanager.register(A()) assert len(l) == 2 diff --git a/testing/test_session.py b/testing/test_session.py index 4b38c7efd..a3006b52b 100644 --- a/testing/test_session.py +++ b/testing/test_session.py @@ -214,8 +214,8 @@ def test_plugin_specify(testdir): def test_plugin_already_exists(testdir): config = testdir.parseconfig("-p", "terminal") assert config.option.plugins == ['terminal'] - config.do_configure() - config.do_unconfigure() + config._do_configure() + config._ensure_unconfigure() def test_exclude(testdir): hellodir = testdir.mkdir("hello")