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
This commit is contained in:
holger krekel 2015-04-22 16:33:20 +02:00
parent f746c190ac
commit 715a235b45
12 changed files with 78 additions and 79 deletions

View File

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

View File

@ -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):
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):

View File

@ -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()

View File

@ -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
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
def _ensure_unconfigure(self):
if self._configured:
self._configured = False
self.hook.pytest_unconfigure(config=self)
self.pluginmanager.ensure_shutdown()
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"

View File

@ -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 = []

View File

@ -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()
def unset_tracing():
debugfile.close()
sys.stderr.write("wrote pytestdebug information to %s\n" %
config._debugfile.name)
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):

View File

@ -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):

View File

@ -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

View File

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

View File

@ -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'):

View File

@ -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

View File

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