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. from ``inline_run()`` to allow temporary modules to be reloaded.
Thanks Eduardo Schettino. 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) 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 = AssertionState(config, mode)
config._assertstate.hook = hook config._assertstate.hook = hook
config._assertstate.trace("configured with mode set to %r" % (mode,)) config._assertstate.trace("configured with mode set to %r" % (mode,))
def undo():
def pytest_unconfigure(config):
hook = config._assertstate.hook hook = config._assertstate.hook
if hook is not None and hook in sys.meta_path: if hook is not None and hook in sys.meta_path:
sys.meta_path.remove(hook) sys.meta_path.remove(hook)
config.add_cleanup(undo)
def pytest_collection(session): def pytest_collection(session):

View File

@ -37,13 +37,13 @@ def pytest_load_initial_conftests(early_config, parser, args):
pluginmanager.register(capman, "capturemanager") pluginmanager.register(capman, "capturemanager")
# make sure that capturemanager is properly reset at final shutdown # 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 # make sure logging does not raise exceptions at the end
def silence_logging_at_shutdown(): def silence_logging_at_shutdown():
if "logging" in sys.modules: if "logging" in sys.modules:
sys.modules["logging"].raiseExceptions = False 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) # finally trigger conftest loading but while capturing (issue93)
capman.init_capturings() 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,)) raise ValueError("not a string or argument list: %r" % (args,))
args = shlex.split(args) args = shlex.split(args)
pluginmanager = get_plugin_manager() pluginmanager = get_plugin_manager()
try:
if plugins: if plugins:
for plugin in plugins: for plugin in plugins:
pluginmanager.register(plugin) pluginmanager.register(plugin)
return pluginmanager.hook.pytest_cmdline_parse( return pluginmanager.hook.pytest_cmdline_parse(
pluginmanager=pluginmanager, args=args) pluginmanager=pluginmanager, args=args)
except Exception:
pluginmanager.ensure_shutdown()
raise
def exclude_pytest_names(name): def exclude_pytest_names(name):
return not name.startswith(name) or name == "pytest_plugins" or \ return not name.startswith(name) or name == "pytest_plugins" or \
name.startswith("pytest_funcarg__") name.startswith("pytest_funcarg__")
class PytestPluginManager(PluginManager): class PytestPluginManager(PluginManager):
def __init__(self): def __init__(self):
super(PytestPluginManager, self).__init__(prefix="pytest_", super(PytestPluginManager, self).__init__(prefix="pytest_",
@ -723,16 +720,23 @@ class Config(object):
if self._configured: if self._configured:
call_plugin(plugin, "pytest_configure", {'config': self}) 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 assert not self._configured
self._configured = True self._configured = True
self.hook.pytest_configure(config=self) self.hook.pytest_configure(config=self)
def do_unconfigure(self): def _ensure_unconfigure(self):
assert self._configured if self._configured:
self._configured = False self._configured = False
self.hook.pytest_unconfigure(config=self) self.hook.pytest_unconfigure(config=self)
self.pluginmanager.ensure_shutdown() while self._cleanup:
fin = self._cleanup.pop()
fin()
def warn(self, code, message): def warn(self, code, message):
""" generate a warning for this test session. """ """ generate a warning for this test session. """
@ -747,11 +751,6 @@ class Config(object):
self.parse(args) self.parse(args)
return self return self
def pytest_unconfigure(config):
while config._cleanup:
fin = config._cleanup.pop()
fin()
def notify_exception(self, excinfo, option=None): def notify_exception(self, excinfo, option=None):
if option and option.fulltrace: if option and option.fulltrace:
style = "long" style = "long"

View File

@ -135,6 +135,21 @@ class CallOutcome:
class PluginManager(object): 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): def __init__(self, prefix, excludefunc=None):
self._prefix = prefix self._prefix = prefix
self._excludefunc = excludefunc self._excludefunc = excludefunc
@ -142,10 +157,11 @@ class PluginManager(object):
self._plugins = [] self._plugins = []
self._plugin2hookcallers = {} self._plugin2hookcallers = {}
self.trace = TagTracer().get("pluginmanage") self.trace = TagTracer().get("pluginmanage")
self._shutdown = []
self.hook = HookRelay(pm=self) self.hook = HookRelay(pm=self)
def set_tracing(self, writer): def set_tracing(self, writer):
""" turn on tracing to the given writer method and
return an undo function. """
self.trace.root.setwriter(writer) self.trace.root.setwriter(writer)
# reconfigure HookCalling to perform tracing # reconfigure HookCalling to perform tracing
assert not hasattr(self, "_wrapping") assert not hasattr(self, "_wrapping")
@ -160,12 +176,7 @@ class PluginManager(object):
trace("finish", self.name, "-->", box.result) trace("finish", self.name, "-->", box.result)
trace.root.indent -= 1 trace.root.indent -= 1
undo = add_method_wrapper(HookCaller, _docall) return add_method_wrapper(HookCaller, _docall)
self.add_shutdown(undo)
def do_configure(self, config):
# backward compatibility
config.do_configure()
def make_hook_caller(self, name, plugins): def make_hook_caller(self, name, plugins):
caller = getattr(self.hook, name) caller = getattr(self.hook, name)
@ -233,16 +244,6 @@ class PluginManager(object):
for hookcaller in hookcallers: for hookcaller in hookcallers:
hookcaller.scan_methods() 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): def addhooks(self, module_or_class):
isclass = int(inspect.isclass(module_or_class)) isclass = int(inspect.isclass(module_or_class))
names = [] names = []

View File

@ -28,24 +28,20 @@ def pytest_cmdline_parse():
config = outcome.get_result() config = outcome.get_result()
if config.option.debug: if config.option.debug:
path = os.path.abspath("pytestdebug.log") path = os.path.abspath("pytestdebug.log")
f = open(path, 'w') debugfile = open(path, 'w')
config._debugfile = f debugfile.write("versions pytest-%s, py-%s, "
f.write("versions pytest-%s, py-%s, "
"python-%s\ncwd=%s\nargs=%s\n\n" %( "python-%s\ncwd=%s\nargs=%s\n\n" %(
pytest.__version__, py.__version__, pytest.__version__, py.__version__,
".".join(map(str, sys.version_info)), ".".join(map(str, sys.version_info)),
os.getcwd(), config._origargs)) 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) sys.stderr.write("writing pytestdebug information to %s\n" % path)
def unset_tracing():
@pytest.mark.trylast debugfile.close()
def pytest_unconfigure(config):
if hasattr(config, '_debugfile'):
config._debugfile.close()
sys.stderr.write("wrote pytestdebug information to %s\n" % sys.stderr.write("wrote pytestdebug information to %s\n" %
config._debugfile.name) debugfile.name)
config.trace.root.setwriter(None) config.trace.root.setwriter(None)
config.add_cleanup(unset_tracing)
def pytest_cmdline_main(config): def pytest_cmdline_main(config):
if config.option.version: if config.option.version:
@ -58,9 +54,9 @@ def pytest_cmdline_main(config):
sys.stderr.write(line + "\n") sys.stderr.write(line + "\n")
return 0 return 0
elif config.option.help: elif config.option.help:
config.do_configure() config._do_configure()
showhelp(config) showhelp(config)
config.do_unconfigure() config._ensure_unconfigure()
return 0 return 0
def showhelp(config): def showhelp(config):

View File

@ -77,7 +77,7 @@ def wrap_session(config, doit):
initstate = 0 initstate = 0
try: try:
try: try:
config.do_configure() config._do_configure()
initstate = 1 initstate = 1
config.hook.pytest_sessionstart(session=session) config.hook.pytest_sessionstart(session=session)
initstate = 2 initstate = 2
@ -107,9 +107,7 @@ def wrap_session(config, doit):
config.hook.pytest_sessionfinish( config.hook.pytest_sessionfinish(
session=session, session=session,
exitstatus=session.exitstatus) exitstatus=session.exitstatus)
if initstate >= 1: config._ensure_unconfigure()
config.do_unconfigure()
config.pluginmanager.ensure_shutdown()
return session.exitstatus return session.exitstatus
def pytest_cmdline_main(config): def pytest_cmdline_main(config):

View File

@ -44,14 +44,14 @@ def pytest_addoption(parser):
def pytest_cmdline_main(config): def pytest_cmdline_main(config):
if config.option.markers: if config.option.markers:
config.do_configure() config._do_configure()
tw = py.io.TerminalWriter() tw = py.io.TerminalWriter()
for line in config.getini("markers"): for line in config.getini("markers"):
name, rest = line.split(":", 1) name, rest = line.split(":", 1)
tw.write("@pytest.mark.%s:" % name, bold=True) tw.write("@pytest.mark.%s:" % name, bold=True)
tw.line(rest) tw.line(rest)
tw.line() tw.line()
config.do_unconfigure() config._ensure_unconfigure()
return 0 return 0
pytest_cmdline_main.tryfirst = True pytest_cmdline_main.tryfirst = True

View File

@ -65,7 +65,8 @@ class HookRecorder:
self.calls.append(ParsedCall(hookcaller.name, kwargs)) self.calls.append(ParsedCall(hookcaller.name, kwargs))
yield yield
self._undo_wrapping = add_method_wrapper(HookCaller, _docall) 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): def finish_recording(self):
self._undo_wrapping() self._undo_wrapping()
@ -571,12 +572,7 @@ class TmpTestdir:
# we don't know what the test will do with this half-setup config # 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 # object and thus we make sure it gets unconfigured properly in any
# case (otherwise capturing could still be active, for example) # case (otherwise capturing could still be active, for example)
def ensure_unconfigure(): self.request.addfinalizer(config._ensure_unconfigure)
if hasattr(config.pluginmanager, "_config"):
config.pluginmanager.do_unconfigure(config)
config.pluginmanager.ensure_shutdown()
self.request.addfinalizer(ensure_unconfigure)
return config return config
def parseconfigure(self, *args): def parseconfigure(self, *args):
@ -588,8 +584,8 @@ class TmpTestdir:
""" """
config = self.parseconfig(*args) config = self.parseconfig(*args)
config.do_configure() config._do_configure()
self.request.addfinalizer(config.do_unconfigure) self.request.addfinalizer(config._ensure_unconfigure)
return config return config
def getitem(self, source, funcname="test_func"): def getitem(self, source, funcname="test_func"):

View File

@ -66,6 +66,7 @@ def check_open_files(config):
error.append(error[0]) error.append(error[0])
raise AssertionError("\n".join(error)) raise AssertionError("\n".join(error))
@pytest.mark.trylast
def pytest_runtest_teardown(item, __multicall__): def pytest_runtest_teardown(item, __multicall__):
item.config._basedir.chdir() item.config._basedir.chdir()
if hasattr(item.config, '_openfiles'): if hasattr(item.config, '_openfiles'):

View File

@ -155,13 +155,13 @@ class TestPytestPluginInteractions:
config.pluginmanager.register(A()) config.pluginmanager.register(A())
assert len(l) == 0 assert len(l) == 0
config.do_configure() config._do_configure()
assert len(l) == 1 assert len(l) == 1
config.pluginmanager.register(A()) # leads to a configured() plugin config.pluginmanager.register(A()) # leads to a configured() plugin
assert len(l) == 2 assert len(l) == 2
assert l[0] != l[1] assert l[0] != l[1]
config.do_unconfigure() config._ensure_unconfigure()
config.pluginmanager.register(A()) config.pluginmanager.register(A())
assert len(l) == 2 assert len(l) == 2

View File

@ -214,8 +214,8 @@ def test_plugin_specify(testdir):
def test_plugin_already_exists(testdir): def test_plugin_already_exists(testdir):
config = testdir.parseconfig("-p", "terminal") config = testdir.parseconfig("-p", "terminal")
assert config.option.plugins == ['terminal'] assert config.option.plugins == ['terminal']
config.do_configure() config._do_configure()
config.do_unconfigure() config._ensure_unconfigure()
def test_exclude(testdir): def test_exclude(testdir):
hellodir = testdir.mkdir("hello") hellodir = testdir.mkdir("hello")