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")