1 new commit in pytest:
https://bitbucket.org/pytest-dev/pytest/commits/24f4d48abeeb/
Changeset: 24f4d48abeeb
User: flub
Date: 2015-04-27 12:17:40+00:00
Summary: Merged in hpk42/pytest-patches/more_plugin (pull request #282)
another major pluginmanager refactor and docs
Affected #: 27 files
diff -r e08baadb3d0b309df91b1189131e79c87583ad5b -r
24f4d48abeeb28d15a7a43249c7329dbea2df1d8 CHANGELOG
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -26,6 +26,23 @@
change but it might still break 3rd party plugins which relied on
details like especially the pluginmanager.add_shutdown() API.
Thanks Holger Krekel.
+
+- pluginmanagement: introduce ``pytest.hookimpl_opts`` and
+ ``pytest.hookspec_opts`` decorators for setting impl/spec
+ specific parameters. This substitutes the previous
+ now deprecated use of ``pytest.mark`` which is meant to
+ contain markers for test functions only.
+
+- write/refine docs for "writing plugins" which now have their
+ own page and are separate from the "using/installing plugins`` page.
+
+- fix issue732: properly unregister plugins from any hook calling
+ sites allowing to have temporary plugins during test execution.
+
+- deprecate and warn about ``__multicall__`` argument in hook
+ implementations. Use the ``hookwrapper`` mechanism instead already
+ introduced with pytest-2.7.
+
2.7.1.dev (compared to 2.7.0)
-----------------------------
diff -r e08baadb3d0b309df91b1189131e79c87583ad5b -r
24f4d48abeeb28d15a7a43249c7329dbea2df1d8 _pytest/capture.py
--- a/_pytest/capture.py
+++ b/_pytest/capture.py
@@ -29,7 +29,7 @@
help="shortcut for --capture=no.")
[email protected]
[email protected]_opts(hookwrapper=True)
def pytest_load_initial_conftests(early_config, parser, args):
ns = early_config.known_args_namespace
pluginmanager = early_config.pluginmanager
@@ -101,7 +101,7 @@
if capfuncarg is not None:
capfuncarg.close()
- @pytest.mark.hookwrapper
+ @pytest.hookimpl_opts(hookwrapper=True)
def pytest_make_collect_report(self, collector):
if isinstance(collector, pytest.File):
self.resumecapture()
@@ -115,13 +115,13 @@
else:
yield
- @pytest.mark.hookwrapper
+ @pytest.hookimpl_opts(hookwrapper=True)
def pytest_runtest_setup(self, item):
self.resumecapture()
yield
self.suspendcapture_item(item, "setup")
- @pytest.mark.hookwrapper
+ @pytest.hookimpl_opts(hookwrapper=True)
def pytest_runtest_call(self, item):
self.resumecapture()
self.activate_funcargs(item)
@@ -129,17 +129,17 @@
#self.deactivate_funcargs() called from suspendcapture()
self.suspendcapture_item(item, "call")
- @pytest.mark.hookwrapper
+ @pytest.hookimpl_opts(hookwrapper=True)
def pytest_runtest_teardown(self, item):
self.resumecapture()
yield
self.suspendcapture_item(item, "teardown")
- @pytest.mark.tryfirst
+ @pytest.hookimpl_opts(tryfirst=True)
def pytest_keyboard_interrupt(self, excinfo):
self.reset_capturings()
- @pytest.mark.tryfirst
+ @pytest.hookimpl_opts(tryfirst=True)
def pytest_internalerror(self, excinfo):
self.reset_capturings()
diff -r e08baadb3d0b309df91b1189131e79c87583ad5b -r
24f4d48abeeb28d15a7a43249c7329dbea2df1d8 _pytest/config.py
--- a/_pytest/config.py
+++ b/_pytest/config.py
@@ -9,7 +9,7 @@
# DON't import pytest here because it causes import cycle troubles
import sys, os
from _pytest import hookspec # the extension point definitions
-from _pytest.core import PluginManager
+from _pytest.core import PluginManager, hookimpl_opts, varnames
# pytest startup
#
@@ -38,6 +38,7 @@
tw.line("ERROR: could not load %s\n" % (e.path), red=True)
return 4
else:
+ config.pluginmanager.check_pending()
return config.hook.pytest_cmdline_main(config=config)
class cmdline: # compatibility namespace
@@ -59,17 +60,17 @@
def _preloadplugins():
assert not _preinit
- _preinit.append(get_plugin_manager())
+ _preinit.append(get_config())
-def get_plugin_manager():
+def get_config():
if _preinit:
return _preinit.pop(0)
# subsequent calls to main will create a fresh instance
pluginmanager = PytestPluginManager()
- pluginmanager.config = Config(pluginmanager) # XXX attr needed?
+ config = Config(pluginmanager)
for spec in default_plugins:
pluginmanager.import_plugin(spec)
- return pluginmanager
+ return config
def _prepareconfig(args=None, plugins=None):
if args is None:
@@ -80,7 +81,7 @@
if not isinstance(args, str):
raise ValueError("not a string or argument list: %r" % (args,))
args = shlex.split(args)
- pluginmanager = get_plugin_manager()
+ pluginmanager = get_config().pluginmanager
if plugins:
for plugin in plugins:
pluginmanager.register(plugin)
@@ -97,8 +98,7 @@
super(PytestPluginManager, self).__init__(prefix="pytest_",
excludefunc=exclude_pytest_names)
self._warnings = []
- self._plugin_distinfo = []
- self._globalplugins = []
+ self._conftest_plugins = set()
# state related to local conftest plugins
self._path2confmods = {}
@@ -114,28 +114,35 @@
err = py.io.dupfile(err, encoding=encoding)
except Exception:
pass
- self.set_tracing(err.write)
+ self.trace.root.setwriter(err.write)
+ self.enable_tracing()
- def register(self, plugin, name=None, conftest=False):
+
+ def _verify_hook(self, hook, plugin):
+ super(PytestPluginManager, self)._verify_hook(hook, plugin)
+ method = getattr(plugin, hook.name)
+ if "__multicall__" in varnames(method):
+ fslineno = py.code.getfslineno(method)
+ warning = dict(code="I1",
+ fslocation=fslineno,
+ message="%r hook uses deprecated __multicall__ "
+ "argument" % (hook.name))
+ self._warnings.append(warning)
+
+ def register(self, plugin, name=None):
ret = super(PytestPluginManager, self).register(plugin, name)
- if ret and not conftest:
- self._globalplugins.append(plugin)
+ if ret:
+ self.hook.pytest_plugin_registered.call_historic(
+ kwargs=dict(plugin=plugin, manager=self))
return ret
- def _do_register(self, plugin, name):
- # called from core PluginManager class
- if hasattr(self, "config"):
- self.config._register_plugin(plugin, name)
- return super(PytestPluginManager, self)._do_register(plugin, name)
-
- def unregister(self, plugin):
- super(PytestPluginManager, self).unregister(plugin)
- try:
- self._globalplugins.remove(plugin)
- except ValueError:
- pass
+ def getplugin(self, name):
+ # support deprecated naming because plugins (xdist e.g.) use it
+ return self.get_plugin(name)
def pytest_configure(self, config):
+ # XXX now that the pluginmanager exposes hookimpl_opts(tryfirst...)
+ # we should remove tryfirst/trylast as markers
config.addinivalue_line("markers",
"tryfirst: mark a hook implementation function such that the "
"plugin machinery will try to call it first/as early as possible.")
@@ -143,7 +150,10 @@
"trylast: mark a hook implementation function such that the "
"plugin machinery will try to call it last/as late as possible.")
for warning in self._warnings:
- config.warn(code="I1", message=warning)
+ if isinstance(warning, dict):
+ config.warn(**warning)
+ else:
+ config.warn(code="I1", message=warning)
#
# internal API for local conftest plugin handling
@@ -186,14 +196,21 @@
try:
return self._path2confmods[path]
except KeyError:
- clist = []
- for parent in path.parts():
- if self._confcutdir and self._confcutdir.relto(parent):
- continue
- conftestpath = parent.join("conftest.py")
- if conftestpath.check(file=1):
- mod = self._importconftest(conftestpath)
- clist.append(mod)
+ if path.isfile():
+ clist = self._getconftestmodules(path.dirpath())
+ else:
+ # XXX these days we may rather want to use config.rootdir
+ # and allow users to opt into looking into the rootdir parent
+ # directories instead of requiring to specify confcutdir
+ clist = []
+ for parent in path.parts():
+ if self._confcutdir and self._confcutdir.relto(parent):
+ continue
+ conftestpath = parent.join("conftest.py")
+ if conftestpath.isfile():
+ mod = self._importconftest(conftestpath)
+ clist.append(mod)
+
self._path2confmods[path] = clist
return clist
@@ -217,6 +234,8 @@
mod = conftestpath.pyimport()
except Exception:
raise ConftestImportFailure(conftestpath, sys.exc_info())
+
+ self._conftest_plugins.add(mod)
self._conftestpath2mod[conftestpath] = mod
dirpath = conftestpath.dirpath()
if dirpath in self._path2confmods:
@@ -233,24 +252,6 @@
#
#
- def consider_setuptools_entrypoints(self):
- try:
- from pkg_resources import iter_entry_points, DistributionNotFound
- except ImportError:
- return # XXX issue a warning
- for ep in iter_entry_points('pytest11'):
- name = ep.name
- if name.startswith("pytest_"):
- name = name[7:]
- if ep.name in self._name2plugin or name in self._name2plugin:
- continue
- try:
- plugin = ep.load()
- except DistributionNotFound:
- continue
- self._plugin_distinfo.append((ep.dist, plugin))
- self.register(plugin, name=name)
-
def consider_preparse(self, args):
for opt1,opt2 in zip(args, args[1:]):
if opt1 == "-p":
@@ -258,18 +259,12 @@
def consider_pluginarg(self, arg):
if arg.startswith("no:"):
- name = arg[3:]
- plugin = self.getplugin(name)
- if plugin is not None:
- self.unregister(plugin)
- self._name2plugin[name] = -1
+ self.set_blocked(arg[3:])
else:
- if self.getplugin(arg) is None:
- self.import_plugin(arg)
+ self.import_plugin(arg)
def consider_conftest(self, conftestmodule):
- if self.register(conftestmodule, name=conftestmodule.__file__,
- conftest=True):
+ if self.register(conftestmodule, name=conftestmodule.__file__):
self.consider_module(conftestmodule)
def consider_env(self):
@@ -291,7 +286,7 @@
# basename for historic purposes but must be imported with the
# _pytest prefix.
assert isinstance(modname, str)
- if self.getplugin(modname) is not None:
+ if self.get_plugin(modname) is not None:
return
if modname in builtin_plugins:
importspec = "_pytest." + modname
@@ -685,6 +680,7 @@
notset = Notset()
FILE_OR_DIR = 'file_or_dir'
+
class Config(object):
""" access to configuration values, pluginmanager and plugin hooks. """
@@ -706,20 +702,11 @@
self._cleanup = []
self.pluginmanager.register(self, "pytestconfig")
self._configured = False
-
- def _register_plugin(self, plugin, name):
- call_plugin = self.pluginmanager.call_plugin
- call_plugin(plugin, "pytest_addhooks",
- {'pluginmanager': self.pluginmanager})
- self.hook.pytest_plugin_registered(plugin=plugin,
- manager=self.pluginmanager)
- dic = call_plugin(plugin, "pytest_namespace", {}) or {}
- if dic:
+ def do_setns(dic):
import pytest
setns(pytest, dic)
- call_plugin(plugin, "pytest_addoption", {'parser': self._parser})
- if self._configured:
- call_plugin(plugin, "pytest_configure", {'config': self})
+ self.hook.pytest_namespace.call_historic(do_setns, {})
+
self.hook.pytest_addoption.call_historic(kwargs=dict(parser=self._parser))
def add_cleanup(self, func):
""" Add a function to be called when the config object gets out of
@@ -729,26 +716,27 @@
def _do_configure(self):
assert not self._configured
self._configured = True
- self.hook.pytest_configure(config=self)
+ self.hook.pytest_configure.call_historic(kwargs=dict(config=self))
def _ensure_unconfigure(self):
if self._configured:
self._configured = False
self.hook.pytest_unconfigure(config=self)
+ self.hook.pytest_configure._call_history = []
while self._cleanup:
fin = self._cleanup.pop()
fin()
- def warn(self, code, message):
+ def warn(self, code, message, fslocation=None):
""" generate a warning for this test session. """
self.hook.pytest_logwarning(code=code, message=message,
- fslocation=None, nodeid=None)
+ fslocation=fslocation, nodeid=None)
def get_terminal_writer(self):
- return self.pluginmanager.getplugin("terminalreporter")._tw
+ return self.pluginmanager.get_plugin("terminalreporter")._tw
def pytest_cmdline_parse(self, pluginmanager, args):
- assert self == pluginmanager.config, (self, pluginmanager.config)
+ # REF1 assert self == pluginmanager.config, (self,
pluginmanager.config)
self.parse(args)
return self
@@ -778,8 +766,7 @@
@classmethod
def fromdictargs(cls, option_dict, args):
""" constructor useable for subprocesses. """
- pluginmanager = get_plugin_manager()
- config = pluginmanager.config
+ config = get_config()
config._preparse(args, addopts=False)
config.option.__dict__.update(option_dict)
for x in config.option.plugins:
@@ -794,13 +781,9 @@
if not hasattr(self.option, opt.dest):
setattr(self.option, opt.dest, opt.default)
- def _getmatchingplugins(self, fspath):
- return self.pluginmanager._globalplugins + \
- self.pluginmanager._getconftestmodules(fspath)
-
+ @hookimpl_opts(trylast=True)
def pytest_load_initial_conftests(self, early_config):
self.pluginmanager._set_initial_conftests(early_config.known_args_namespace)
- pytest_load_initial_conftests.trylast = True
def _initini(self, args):
parsed_args = self._parser.parse_known_args(args)
@@ -817,7 +800,10 @@
args[:] = self.getini("addopts") + args
self._checkversion()
self.pluginmanager.consider_preparse(args)
- self.pluginmanager.consider_setuptools_entrypoints()
+ try:
+ self.pluginmanager.load_setuptools_entrypoints("pytest11")
+ except ImportError as e:
+ self.warn("I2", "could not load setuptools entry import: %s" %
(e,))
self.pluginmanager.consider_env()
self.known_args_namespace = ns = self._parser.parse_known_args(args)
try:
@@ -850,6 +836,8 @@
assert not hasattr(self, 'args'), (
"can only parse cmdline args at most once per Config object")
self._origargs = args
+ self.hook.pytest_addhooks.call_historic(
+
kwargs=dict(pluginmanager=self.pluginmanager))
self._preparse(args)
# XXX deprecated hook:
self.hook.pytest_cmdline_preparse(config=self, args=args)
diff -r e08baadb3d0b309df91b1189131e79c87583ad5b -r
24f4d48abeeb28d15a7a43249c7329dbea2df1d8 _pytest/core.py
--- a/_pytest/core.py
+++ b/_pytest/core.py
@@ -2,11 +2,65 @@
PluginManager, basic initialization and tracing.
"""
import sys
-import inspect
+from inspect import isfunction, ismethod, isclass, formatargspec, getargspec
import py
py3 = sys.version_info > (3,0)
+def hookspec_opts(firstresult=False, historic=False):
+ """ returns a decorator which will define a function as a hook
specfication.
+
+ If firstresult is True the 1:N hook call (N being the number of registered
+ hook implementation functions) will stop at I<=N when the I'th function
+ returns a non-None result.
+
+ If historic is True calls to a hook will be memorized and replayed
+ on later registered plugins.
+ """
+ def setattr_hookspec_opts(func):
+ if historic and firstresult:
+ raise ValueError("cannot have a historic firstresult hook")
+ if firstresult:
+ func.firstresult = firstresult
+ if historic:
+ func.historic = historic
+ return func
+ return setattr_hookspec_opts
+
+
+def hookimpl_opts(hookwrapper=False, optionalhook=False,
+ tryfirst=False, trylast=False):
+ """ Return a decorator which marks a function as a hook implementation.
+
+ If optionalhook is True a missing matching hook specification will not
result
+ in an error (by default it is an error if no matching spec is found).
+
+ If tryfirst is True this hook implementation will run as early as possible
+ in the chain of N hook implementations for a specfication.
+
+ If trylast is True this hook implementation will run as late as possible
+ in the chain of N hook implementations.
+
+ If hookwrapper is True the hook implementations needs to execute exactly
+ one "yield". The code before the yield is run early before any
non-hookwrapper
+ function is run. The code after the yield is run after all non-hookwrapper
+ function have run. The yield receives an ``CallOutcome`` object
representing
+ the exception or result outcome of the inner calls (including other
hookwrapper
+ calls).
+ """
+ def setattr_hookimpl_opts(func):
+ if hookwrapper:
+ func.hookwrapper = True
+ if optionalhook:
+ func.optionalhook = True
+ if tryfirst:
+ func.tryfirst = True
+ if trylast:
+ func.trylast = True
+ return func
+ return setattr_hookimpl_opts
+
+
class TagTracer:
def __init__(self):
self._tag2proc = {}
@@ -53,42 +107,28 @@
assert isinstance(tags, tuple)
self._tag2proc[tags] = processor
+
class TagTracerSub:
def __init__(self, root, tags):
self.root = root
self.tags = tags
+
def __call__(self, *args):
self.root.processmessage(self.tags, args)
+
def setmyprocessor(self, processor):
self.root.setprocessor(self.tags, processor)
+
def get(self, name):
return self.__class__(self.root, self.tags + (name,))
-def add_method_wrapper(cls, wrapper_func):
- """ Substitute the function named "wrapperfunc.__name__" at class
- "cls" with a function that wraps the call to the original function.
- Return an undo function which can be called to reset the class to use
- the old method again.
-
- wrapper_func is called with the same arguments as the method
- it wraps and its result is used as a wrap_controller for
- calling the original function.
- """
- name = wrapper_func.__name__
- oldcall = getattr(cls, name)
- def wrap_exec(*args, **kwargs):
- gen = wrapper_func(*args, **kwargs)
- return wrapped_call(gen, lambda: oldcall(*args, **kwargs))
-
- setattr(cls, name, wrap_exec)
- return lambda: setattr(cls, name, oldcall)
-
def raise_wrapfail(wrap_controller, msg):
co = wrap_controller.gi_code
raise RuntimeError("wrap_controller at %r %s:%d %s" %
(co.co_name, co.co_filename, co.co_firstlineno, msg))
+
def wrapped_call(wrap_controller, func):
""" Wrap calling to a function with a generator which needs to yield
exactly once. The yield point will trigger calling the wrapped function
@@ -133,6 +173,25 @@
py.builtin._reraise(*ex)
+class TracedHookExecution:
+ def __init__(self, pluginmanager, before, after):
+ self.pluginmanager = pluginmanager
+ self.before = before
+ self.after = after
+ self.oldcall = pluginmanager._inner_hookexec
+ assert not isinstance(self.oldcall, TracedHookExecution)
+ self.pluginmanager._inner_hookexec = self
+
+ def __call__(self, hook, methods, kwargs):
+ self.before(hook, methods, kwargs)
+ outcome = CallOutcome(lambda: self.oldcall(hook, methods, kwargs))
+ self.after(outcome, hook, methods, kwargs)
+ return outcome.get_result()
+
+ def undo(self):
+ self.pluginmanager._inner_hookexec = self.oldcall
+
+
class PluginManager(object):
""" Core Pluginmanager class which manages registration
of plugin objects and 1:N hook calling.
@@ -144,197 +203,228 @@
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.
+ For debugging purposes you can call ``enable_tracing()``
+ which will subsequently send debug information to the trace helper.
"""
def __init__(self, prefix, excludefunc=None):
self._prefix = prefix
self._excludefunc = excludefunc
self._name2plugin = {}
- self._plugins = []
self._plugin2hookcallers = {}
+ self._plugin_distinfo = []
self.trace = TagTracer().get("pluginmanage")
- self.hook = HookRelay(pm=self)
+ self.hook = HookRelay(self.trace.root.get("hook"))
+ self._inner_hookexec = lambda hook, methods, kwargs: \
+ MultiCall(methods, kwargs,
hook.firstresult).execute()
- 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")
- self._wrapping = True
+ def _hookexec(self, hook, methods, kwargs):
+ # called from all hookcaller instances.
+ # enable_tracing will set its own wrapping function at
self._inner_hookexec
+ return self._inner_hookexec(hook, methods, kwargs)
- hooktrace = self.hook.trace
+ def enable_tracing(self):
+ """ enable tracing of hook calls and return an undo function. """
+ hooktrace = self.hook._trace
- def _docall(self, methods, kwargs):
+ def before(hook, methods, kwargs):
hooktrace.root.indent += 1
- hooktrace(self.name, kwargs)
- box = yield
- if box.excinfo is None:
- hooktrace("finish", self.name, "-->", box.result)
+ hooktrace(hook.name, kwargs)
+
+ def after(outcome, hook, methods, kwargs):
+ if outcome.excinfo is None:
+ hooktrace("finish", hook.name, "-->", outcome.result)
hooktrace.root.indent -= 1
- return add_method_wrapper(HookCaller, _docall)
+ return TracedHookExecution(self, before, after).undo
- def make_hook_caller(self, name, plugins):
- caller = getattr(self.hook, name)
- methods = self.listattr(name, plugins=plugins)
- return HookCaller(caller.name, caller.firstresult,
- argnames=caller.argnames, methods=methods)
+ def subset_hook_caller(self, name, remove_plugins):
+ """ Return a new HookCaller instance for the named method
+ which manages calls to all registered plugins except the
+ ones from remove_plugins. """
+ orig = getattr(self.hook, name)
+ plugins_to_remove = [plugin for plugin in remove_plugins
+ if hasattr(plugin, name)]
+ if plugins_to_remove:
+ hc = HookCaller(orig.name, orig._hookexec,
orig._specmodule_or_class)
+ for plugin in orig._plugins:
+ if plugin not in plugins_to_remove:
+ hc._add_plugin(plugin)
+ # we also keep track of this hook caller so it
+ # gets properly removed on plugin unregistration
+ self._plugin2hookcallers.setdefault(plugin, []).append(hc)
+ return hc
+ return orig
def register(self, plugin, name=None):
- """ Register a plugin with the given name and ensure that all its
- hook implementations are integrated. If the name is not specified
- we use the ``__name__`` attribute of the plugin object or, if that
- doesn't exist, the id of the plugin. This method will raise a
- ValueError if the eventual name is already registered. """
- name = name or self._get_canonical_name(plugin)
- if self._name2plugin.get(name, None) == -1:
- return
- if self.hasplugin(name):
+ """ Register a plugin and return its canonical name or None if the name
+ is blocked from registering. Raise a ValueError if the plugin is
already
+ registered. """
+ plugin_name = name or self.get_canonical_name(plugin)
+
+ if plugin_name in self._name2plugin or plugin in
self._plugin2hookcallers:
+ if self._name2plugin.get(plugin_name, -1) is None:
+ return # blocked plugin, return None to indicate no
registration
raise ValueError("Plugin already registered: %s=%s\n%s" %(
- name, plugin, self._name2plugin))
- #self.trace("registering", name, plugin)
- # allow subclasses to intercept here by calling a helper
- return self._do_register(plugin, name)
+ plugin_name, plugin, self._name2plugin))
- def _do_register(self, plugin, name):
- hookcallers = list(self._scan_plugin(plugin))
- self._plugin2hookcallers[plugin] = hookcallers
- self._name2plugin[name] = plugin
- self._plugins.append(plugin)
- # rescan all methods for the hookcallers we found
- for hookcaller in hookcallers:
- self._scan_methods(hookcaller)
- return True
+ self._name2plugin[plugin_name] = plugin
- def unregister(self, plugin):
- """ unregister the plugin object and all its contained hook
implementations
+ # register prefix-matching hook specs of the plugin
+ self._plugin2hookcallers[plugin] = hookcallers = []
+ for name in dir(plugin):
+ if name.startswith(self._prefix):
+ hook = getattr(self.hook, name, None)
+ if hook is None:
+ if self._excludefunc is not None and
self._excludefunc(name):
+ continue
+ hook = HookCaller(name, self._hookexec)
+ setattr(self.hook, name, hook)
+ elif hook.has_spec():
+ self._verify_hook(hook, plugin)
+ hook._maybe_apply_history(getattr(plugin, name))
+ hookcallers.append(hook)
+ hook._add_plugin(plugin)
+ return plugin_name
+
+ def unregister(self, plugin=None, name=None):
+ """ unregister a plugin object and all its contained hook
implementations
from internal data structures. """
- self._plugins.remove(plugin)
- for name, value in list(self._name2plugin.items()):
- if value == plugin:
- del self._name2plugin[name]
- hookcallers = self._plugin2hookcallers.pop(plugin)
- for hookcaller in hookcallers:
- self._scan_methods(hookcaller)
+ if name is None:
+ assert plugin is not None, "one of name or plugin needs to be
specified"
+ name = self.get_name(plugin)
+
+ if plugin is None:
+ plugin = self.get_plugin(name)
+
+ # if self._name2plugin[name] == None registration was blocked: ignore
+ if self._name2plugin.get(name):
+ del self._name2plugin[name]
+
+ for hookcaller in self._plugin2hookcallers.pop(plugin, []):
+ hookcaller._remove_plugin(plugin)
+
+ return plugin
+
+ def set_blocked(self, name):
+ """ block registrations of the given name, unregister if already
registered. """
+ self.unregister(name=name)
+ self._name2plugin[name] = None
def addhooks(self, module_or_class):
""" add new hook definitions from the given module_or_class using
the prefix/excludefunc with which the PluginManager was initialized.
"""
- isclass = int(inspect.isclass(module_or_class))
names = []
for name in dir(module_or_class):
if name.startswith(self._prefix):
- method = module_or_class.__dict__[name]
- firstresult = getattr(method, 'firstresult', False)
- hc = HookCaller(name, firstresult=firstresult,
- argnames=varnames(method, startindex=isclass))
- setattr(self.hook, name, hc)
+ hc = getattr(self.hook, name, None)
+ if hc is None:
+ hc = HookCaller(name, self._hookexec, module_or_class)
+ setattr(self.hook, name, hc)
+ else:
+ # plugins registered this hook without knowing the spec
+ hc.set_specification(module_or_class)
+ for plugin in hc._plugins:
+ self._verify_hook(hc, plugin)
names.append(name)
+
if not names:
raise ValueError("did not find new %r hooks in %r"
%(self._prefix, module_or_class))
- def getplugins(self):
- """ return the complete list of registered plugins. NOTE that
- you will get the internal list and need to make a copy if you
- modify the list."""
- return self._plugins
+ def get_plugins(self):
+ """ return the set of registered plugins. """
+ return set(self._plugin2hookcallers)
- def isregistered(self, plugin):
- """ Return True if the plugin is already registered under its
- canonical name. """
- return self.hasplugin(self._get_canonical_name(plugin)) or \
- plugin in self._plugins
+ def is_registered(self, plugin):
+ """ Return True if the plugin is already registered. """
+ return plugin in self._plugin2hookcallers
- def hasplugin(self, name):
- """ Return True if there is a registered with the given name. """
- return name in self._name2plugin
+ def get_canonical_name(self, plugin):
+ """ Return canonical name for a plugin object. Note that a plugin
+ may be registered under a different name which was specified
+ by the caller of register(plugin, name). To obtain the name
+ of an registered plugin use ``get_name(plugin)`` instead."""
+ return getattr(plugin, "__name__", None) or str(id(plugin))
- def getplugin(self, name):
+ def get_plugin(self, name):
""" Return a plugin or None for the given name. """
return self._name2plugin.get(name)
- def listattr(self, attrname, plugins=None):
- if plugins is None:
- plugins = self._plugins
- l = []
- last = []
- wrappers = []
- for plugin in plugins:
+ def get_name(self, plugin):
+ """ Return name for registered plugin or None if not registered. """
+ for name, val in self._name2plugin.items():
+ if plugin == val:
+ return name
+
+ def _verify_hook(self, hook, plugin):
+ method = getattr(plugin, hook.name)
+ pluginname = self.get_name(plugin)
+
+ if hook.is_historic() and hasattr(method, "hookwrapper"):
+ raise PluginValidationError(
+ "Plugin %r\nhook %r\nhistoric incompatible to hookwrapper" %(
+ pluginname, hook.name))
+
+ for arg in varnames(method):
+ if arg not in hook.argnames:
+ raise PluginValidationError(
+ "Plugin %r\nhook %r\nargument %r not available\n"
+ "plugin definition: %s\n"
+ "available hookargs: %s" %(
+ pluginname, hook.name, arg, formatdef(method),
+ ", ".join(hook.argnames)))
+
+ def check_pending(self):
+ """ Verify that all hooks which have not been verified against
+ a hook specification are optional, otherwise raise
PluginValidationError"""
+ for name in self.hook.__dict__:
+ if name.startswith(self._prefix):
+ hook = getattr(self.hook, name)
+ if not hook.has_spec():
+ for plugin in hook._plugins:
+ method = getattr(plugin, hook.name)
+ if not getattr(method, "optionalhook", False):
+ raise PluginValidationError(
+ "unknown hook %r in plugin %r" %(name, plugin))
+
+ def load_setuptools_entrypoints(self, entrypoint_name):
+ """ Load modules from querying the specified setuptools entrypoint
name.
+ Return the number of loaded plugins. """
+ from pkg_resources import iter_entry_points, DistributionNotFound
+ for ep in iter_entry_points(entrypoint_name):
+ # is the plugin registered or blocked?
+ if self.get_plugin(ep.name) or ep.name in self._name2plugin:
+ continue
try:
- meth = getattr(plugin, attrname)
- except AttributeError:
+ plugin = ep.load()
+ except DistributionNotFound:
continue
- if hasattr(meth, 'hookwrapper'):
- wrappers.append(meth)
- elif hasattr(meth, 'tryfirst'):
- last.append(meth)
- elif hasattr(meth, 'trylast'):
- l.insert(0, meth)
- else:
- l.append(meth)
- l.extend(last)
- l.extend(wrappers)
- return l
-
- def _scan_methods(self, hookcaller):
- hookcaller.methods = self.listattr(hookcaller.name)
-
- def call_plugin(self, plugin, methname, kwargs):
- return MultiCall(methods=self.listattr(methname, plugins=[plugin]),
- kwargs=kwargs, firstresult=True).execute()
-
-
- def _scan_plugin(self, plugin):
- def fail(msg, *args):
- name = getattr(plugin, '__name__', plugin)
- raise PluginValidationError("plugin %r\n%s" %(name, msg % args))
-
- for name in dir(plugin):
- if name[0] == "_" or not name.startswith(self._prefix):
- continue
- hook = getattr(self.hook, name, None)
- method = getattr(plugin, name)
- if hook is None:
- if self._excludefunc is not None and self._excludefunc(name):
- continue
- if getattr(method, 'optionalhook', False):
- continue
- fail("found unknown hook: %r", name)
- for arg in varnames(method):
- if arg not in hook.argnames:
- fail("argument %r not available\n"
- "actual definition: %s\n"
- "available hookargs: %s",
- arg, formatdef(method),
- ", ".join(hook.argnames))
- yield hook
-
- def _get_canonical_name(self, plugin):
- return getattr(plugin, "__name__", None) or str(id(plugin))
-
+ self.register(plugin, name=ep.name)
+ self._plugin_distinfo.append((ep.dist, plugin))
+ return len(self._plugin_distinfo)
class MultiCall:
""" execute a call into multiple python functions/methods. """
+ # XXX note that the __multicall__ argument is supported only
+ # for pytest compatibility reasons. It was never officially
+ # supported there and is explicitely deprecated since 2.8
+ # so we can remove it soon, allowing to avoid the below recursion
+ # in execute() and simplify/speed up the execute loop.
+
def __init__(self, methods, kwargs, firstresult=False):
- self.methods = list(methods)
+ self.methods = methods
self.kwargs = kwargs
self.kwargs["__multicall__"] = self
- self.results = []
self.firstresult = firstresult
- def __repr__(self):
- status = "%d results, %d meths" % (len(self.results),
len(self.methods))
- return "<MultiCall %s, kwargs=%r>" %(status, self.kwargs)
-
def execute(self):
all_kwargs = self.kwargs
+ self.results = results = []
+ firstresult = self.firstresult
+
while self.methods:
method = self.methods.pop()
args = [all_kwargs[argname] for argname in varnames(method)]
@@ -342,11 +432,19 @@
return wrapped_call(method(*args), self.execute)
res = method(*args)
if res is not None:
- self.results.append(res)
- if self.firstresult:
+ if firstresult:
return res
- if not self.firstresult:
- return self.results
+ results.append(res)
+
+ if not firstresult:
+ return results
+
+ def __repr__(self):
+ status = "%d meths" % (len(self.methods),)
+ if hasattr(self, "results"):
+ status = ("%d results, " % len(self.results)) + status
+ return "<MultiCall %s, kwargs=%r>" %(status, self.kwargs)
+
def varnames(func, startindex=None):
@@ -361,17 +459,17 @@
return cache["_varnames"]
except KeyError:
pass
- if inspect.isclass(func):
+ if isclass(func):
try:
func = func.__init__
except AttributeError:
return ()
startindex = 1
else:
- if not inspect.isfunction(func) and not inspect.ismethod(func):
+ if not isfunction(func) and not ismethod(func):
func = getattr(func, '__call__', func)
if startindex is None:
- startindex = int(inspect.ismethod(func))
+ startindex = int(ismethod(func))
rawcode = py.code.getrawcode(func)
try:
@@ -390,32 +488,95 @@
class HookRelay:
- def __init__(self, pm):
- self._pm = pm
- self.trace = pm.trace.root.get("hook")
+ def __init__(self, trace):
+ self._trace = trace
-class HookCaller:
- def __init__(self, name, firstresult, argnames, methods=()):
+class HookCaller(object):
+ def __init__(self, name, hook_execute, specmodule_or_class=None):
self.name = name
- self.firstresult = firstresult
- self.argnames = ["__multicall__"]
- self.argnames.extend(argnames)
+ self._plugins = []
+ self._wrappers = []
+ self._nonwrappers = []
+ self._hookexec = hook_execute
+ if specmodule_or_class is not None:
+ self.set_specification(specmodule_or_class)
+
+ def has_spec(self):
+ return hasattr(self, "_specmodule_or_class")
+
+ def set_specification(self, specmodule_or_class):
+ assert not self.has_spec()
+ self._specmodule_or_class = specmodule_or_class
+ specfunc = getattr(specmodule_or_class, self.name)
+ argnames = varnames(specfunc, startindex=isclass(specmodule_or_class))
assert "self" not in argnames # sanity check
- self.methods = methods
+ self.argnames = ["__multicall__"] + list(argnames)
+ self.firstresult = getattr(specfunc, 'firstresult', False)
+ if hasattr(specfunc, "historic"):
+ self._call_history = []
+
+ def is_historic(self):
+ return hasattr(self, "_call_history")
+
+ def _remove_plugin(self, plugin):
+ self._plugins.remove(plugin)
+ meth = getattr(plugin, self.name)
+ try:
+ self._nonwrappers.remove(meth)
+ except ValueError:
+ self._wrappers.remove(meth)
+
+ def _add_plugin(self, plugin):
+ self._plugins.append(plugin)
+ self._add_method(getattr(plugin, self.name))
+
+ def _add_method(self, meth):
+ if hasattr(meth, 'hookwrapper'):
+ methods = self._wrappers
+ else:
+ methods = self._nonwrappers
+
+ if hasattr(meth, 'trylast'):
+ methods.insert(0, meth)
+ elif hasattr(meth, 'tryfirst'):
+ methods.append(meth)
+ else:
+ # find last non-tryfirst method
+ i = len(methods) - 1
+ while i >= 0 and hasattr(methods[i], "tryfirst"):
+ i -= 1
+ methods.insert(i + 1, meth)
def __repr__(self):
return "<HookCaller %r>" %(self.name,)
def __call__(self, **kwargs):
- return self._docall(self.methods, kwargs)
+ assert not self.is_historic()
+ return self._hookexec(self, self._nonwrappers + self._wrappers, kwargs)
- def callextra(self, methods, **kwargs):
- return self._docall(self.methods + methods, kwargs)
+ def call_historic(self, proc=None, kwargs=None):
+ self._call_history.append((kwargs or {}, proc))
+ # historizing hooks don't return results
+ self._hookexec(self, self._nonwrappers + self._wrappers, kwargs)
- def _docall(self, methods, kwargs):
- return MultiCall(methods, kwargs,
- firstresult=self.firstresult).execute()
+ def call_extra(self, methods, kwargs):
+ """ Call the hook with some additional temporarily participating
+ methods using the specified kwargs as call parameters. """
+ old = list(self._nonwrappers), list(self._wrappers)
+ for method in methods:
+ self._add_method(method)
+ try:
+ return self(**kwargs)
+ finally:
+ self._nonwrappers, self._wrappers = old
+
+ def _maybe_apply_history(self, method):
+ if self.is_historic():
+ for kwargs, proc in self._call_history:
+ res = self._hookexec(self, [method], kwargs)
+ if res and proc is not None:
+ proc(res[0])
class PluginValidationError(Exception):
@@ -425,5 +586,5 @@
def formatdef(func):
return "%s%s" % (
func.__name__,
- inspect.formatargspec(*inspect.getargspec(func))
+ formatargspec(*getargspec(func))
)
diff -r e08baadb3d0b309df91b1189131e79c87583ad5b -r
24f4d48abeeb28d15a7a43249c7329dbea2df1d8 _pytest/helpconfig.py
--- a/_pytest/helpconfig.py
+++ b/_pytest/helpconfig.py
@@ -22,7 +22,7 @@
help="store internal tracing debug information in
'pytestdebug.log'.")
[email protected]
[email protected]_opts(hookwrapper=True)
def pytest_cmdline_parse():
outcome = yield
config = outcome.get_result()
@@ -34,13 +34,15 @@
pytest.__version__, py.__version__,
".".join(map(str, sys.version_info)),
os.getcwd(), config._origargs))
- config.pluginmanager.set_tracing(debugfile.write)
+ config.trace.root.setwriter(debugfile.write)
+ undo_tracing = config.pluginmanager.enable_tracing()
sys.stderr.write("writing pytestdebug information to %s\n" % path)
def unset_tracing():
debugfile.close()
sys.stderr.write("wrote pytestdebug information to %s\n" %
debugfile.name)
config.trace.root.setwriter(None)
+ undo_tracing()
config.add_cleanup(unset_tracing)
def pytest_cmdline_main(config):
diff -r e08baadb3d0b309df91b1189131e79c87583ad5b -r
24f4d48abeeb28d15a7a43249c7329dbea2df1d8 _pytest/hookspec.py
--- a/_pytest/hookspec.py
+++ b/_pytest/hookspec.py
@@ -1,27 +1,30 @@
""" hook specifications for pytest plugins, invoked from main.py and builtin
plugins. """
+from _pytest.core import hookspec_opts
+
# -------------------------------------------------------------------------
-# Initialization
+# Initialization hooks called for every plugin
# -------------------------------------------------------------------------
+@hookspec_opts(historic=True)
def pytest_addhooks(pluginmanager):
- """called at plugin load time to allow adding new hooks via a call to
+ """called at plugin registration time to allow adding new hooks via a call
to
pluginmanager.addhooks(module_or_class, prefix)."""
+@hookspec_opts(historic=True)
def pytest_namespace():
"""return dict of name->object to be made globally available in
- the pytest namespace. This hook is called before command line options
- are parsed.
+ the pytest namespace. This hook is called at plugin registration
+ time.
"""
-def pytest_cmdline_parse(pluginmanager, args):
- """return initialized config object, parsing the specified args. """
-pytest_cmdline_parse.firstresult = True
+@hookspec_opts(historic=True)
+def pytest_plugin_registered(plugin, manager):
+ """ a new pytest plugin got registered. """
-def pytest_cmdline_preparse(config, args):
- """(deprecated) modify command line arguments before option parsing. """
+@hookspec_opts(historic=True)
def pytest_addoption(parser):
"""register argparse-style options and ini-style config values.
@@ -47,35 +50,43 @@
via (deprecated) ``pytest.config``.
"""
+@hookspec_opts(historic=True)
+def pytest_configure(config):
+ """ called after command line options have been parsed
+ and all plugins and initial conftest files been loaded.
+ This hook is called for every plugin.
+ """
+
+# -------------------------------------------------------------------------
+# Bootstrapping hooks called for plugins registered early enough:
+# internal and 3rd party plugins as well as directly
+# discoverable conftest.py local plugins.
+# -------------------------------------------------------------------------
+
+@hookspec_opts(firstresult=True)
+def pytest_cmdline_parse(pluginmanager, args):
+ """return initialized config object, parsing the specified args. """
+
+def pytest_cmdline_preparse(config, args):
+ """(deprecated) modify command line arguments before option parsing. """
+
+@hookspec_opts(firstresult=True)
def pytest_cmdline_main(config):
""" called for performing the main command line action. The default
implementation will invoke the configure hooks and runtest_mainloop. """
-pytest_cmdline_main.firstresult = True
def pytest_load_initial_conftests(args, early_config, parser):
""" implements the loading of initial conftest files ahead
of command line option parsing. """
-def pytest_configure(config):
- """ called after command line options have been parsed
- and all plugins and initial conftest files been loaded.
- """
-
-def pytest_unconfigure(config):
- """ called before test process is exited. """
-
-def pytest_runtestloop(session):
- """ called for performing the main runtest loop
- (after collection finished). """
-pytest_runtestloop.firstresult = True
# -------------------------------------------------------------------------
# collection hooks
# -------------------------------------------------------------------------
+@hookspec_opts(firstresult=True)
def pytest_collection(session):
""" perform the collection protocol for the given session. """
-pytest_collection.firstresult = True
def pytest_collection_modifyitems(session, config, items):
""" called after collection has been performed, may filter or re-order
@@ -84,16 +95,16 @@
def pytest_collection_finish(session):
""" called after collection has been performed and modified. """
+@hookspec_opts(firstresult=True)
def pytest_ignore_collect(path, config):
""" return True to prevent considering this path for collection.
This hook is consulted for all files and directories prior to calling
more specific hooks.
"""
-pytest_ignore_collect.firstresult = True
+@hookspec_opts(firstresult=True)
def pytest_collect_directory(path, parent):
""" called before traversing a directory for collection files. """
-pytest_collect_directory.firstresult = True
def pytest_collect_file(path, parent):
""" return collection Node or None for the given path. Any new node
@@ -112,29 +123,29 @@
def pytest_deselected(items):
""" called for test items deselected by keyword. """
+@hookspec_opts(firstresult=True)
def pytest_make_collect_report(collector):
""" perform ``collector.collect()`` and return a CollectReport. """
-pytest_make_collect_report.firstresult = True
# -------------------------------------------------------------------------
# Python test function related hooks
# -------------------------------------------------------------------------
+@hookspec_opts(firstresult=True)
def pytest_pycollect_makemodule(path, parent):
""" return a Module collector or None for the given path.
This hook will be called for each matching test module path.
The pytest_collect_file hook needs to be used if you want to
create test modules for files that do not match as a test module.
"""
-pytest_pycollect_makemodule.firstresult = True
+@hookspec_opts(firstresult=True)
def pytest_pycollect_makeitem(collector, name, obj):
""" return custom item/collector for a python object in a module, or None.
"""
-pytest_pycollect_makeitem.firstresult = True
+@hookspec_opts(firstresult=True)
def pytest_pyfunc_call(pyfuncitem):
""" call underlying test function. """
-pytest_pyfunc_call.firstresult = True
def pytest_generate_tests(metafunc):
""" generate (multiple) parametrized calls to a test function."""
@@ -142,9 +153,16 @@
# -------------------------------------------------------------------------
# generic runtest related hooks
# -------------------------------------------------------------------------
+
+@hookspec_opts(firstresult=True)
+def pytest_runtestloop(session):
+ """ called for performing the main runtest loop
+ (after collection finished). """
+
def pytest_itemstart(item, node):
""" (deprecated, use pytest_runtest_logstart). """
+@hookspec_opts(firstresult=True)
def pytest_runtest_protocol(item, nextitem):
""" implements the runtest_setup/call/teardown protocol for
the given test item, including capturing exceptions and calling
@@ -158,7 +176,6 @@
:return boolean: True if no further hook implementations should be invoked.
"""
-pytest_runtest_protocol.firstresult = True
def pytest_runtest_logstart(nodeid, location):
""" signal the start of running a single test item. """
@@ -178,12 +195,12 @@
so that nextitem only needs to call setup-functions.
"""
+@hookspec_opts(firstresult=True)
def pytest_runtest_makereport(item, call):
""" return a :py:class:`_pytest.runner.TestReport` object
for the given :py:class:`pytest.Item` and
:py:class:`_pytest.runner.CallInfo`.
"""
-pytest_runtest_makereport.firstresult = True
def pytest_runtest_logreport(report):
""" process a test setup/call/teardown report relating to
@@ -199,6 +216,9 @@
def pytest_sessionfinish(session, exitstatus):
""" whole test run finishes. """
+def pytest_unconfigure(config):
+ """ called before test process is exited. """
+
# -------------------------------------------------------------------------
# hooks for customising the assert methods
@@ -220,9 +240,9 @@
def pytest_report_header(config, startdir):
""" return a string to be displayed as header info for terminal
reporting."""
+@hookspec_opts(firstresult=True)
def pytest_report_teststatus(report):
""" return result-category, shortletter and verbose word for reporting."""
-pytest_report_teststatus.firstresult = True
def pytest_terminal_summary(terminalreporter):
""" add additional section in terminal summary reporting. """
@@ -236,17 +256,14 @@
# doctest hooks
# -------------------------------------------------------------------------
+@hookspec_opts(firstresult=True)
def pytest_doctest_prepare_content(content):
""" return processed content for a given doctest"""
-pytest_doctest_prepare_content.firstresult = True
# -------------------------------------------------------------------------
# error handling and internal debugging hooks
# -------------------------------------------------------------------------
-def pytest_plugin_registered(plugin, manager):
- """ a new pytest plugin got registered. """
-
def pytest_internalerror(excrepr, excinfo):
""" called for internal errors. """
diff -r e08baadb3d0b309df91b1189131e79c87583ad5b -r
24f4d48abeeb28d15a7a43249c7329dbea2df1d8 _pytest/main.py
--- a/_pytest/main.py
+++ b/_pytest/main.py
@@ -151,18 +151,17 @@
ignore_paths.extend([py.path.local(x) for x in excludeopt])
return path in ignore_paths
-class FSHookProxy(object):
- def __init__(self, fspath, config):
+class FSHookProxy:
+ def __init__(self, fspath, pm, remove_mods):
self.fspath = fspath
- self.config = config
+ self.pm = pm
+ self.remove_mods = remove_mods
def __getattr__(self, name):
- plugins = self.config._getmatchingplugins(self.fspath)
- x = self.config.pluginmanager.make_hook_caller(name, plugins)
+ x = self.pm.subset_hook_caller(name, remove_plugins=self.remove_mods)
self.__dict__[name] = x
return x
-
def compatproperty(name):
def fget(self):
# deprecated - use pytest.name
@@ -362,9 +361,6 @@
def listnames(self):
return [x.name for x in self.listchain()]
- def getplugins(self):
- return self.config._getmatchingplugins(self.fspath)
-
def addfinalizer(self, fin):
""" register a function to be called when this node is finalized.
@@ -519,12 +515,12 @@
def _makeid(self):
return ""
- @pytest.mark.tryfirst
+ @pytest.hookimpl_opts(tryfirst=True)
def pytest_collectstart(self):
if self.shouldstop:
raise self.Interrupted(self.shouldstop)
- @pytest.mark.tryfirst
+ @pytest.hookimpl_opts(tryfirst=True)
def pytest_runtest_logreport(self, report):
if report.failed and not hasattr(report, 'wasxfail'):
self._testsfailed += 1
@@ -541,8 +537,20 @@
try:
return self._fs2hookproxy[fspath]
except KeyError:
- self._fs2hookproxy[fspath] = x = FSHookProxy(fspath, self.config)
- return x
+ # check if we have the common case of running
+ # hooks with all conftest.py filesall conftest.py
+ pm = self.config.pluginmanager
+ my_conftestmodules = pm._getconftestmodules(fspath)
+ remove_mods = pm._conftest_plugins.difference(my_conftestmodules)
+ if remove_mods:
+ # one or more conftests are not in use at this fspath
+ proxy = FSHookProxy(fspath, pm, remove_mods)
+ else:
+ # all plugis are active for this fspath
+ proxy = self.config.hook
+
+ self._fs2hookproxy[fspath] = proxy
+ return proxy
def perform_collect(self, args=None, genitems=True):
hook = self.config.hook
diff -r e08baadb3d0b309df91b1189131e79c87583ad5b -r
24f4d48abeeb28d15a7a43249c7329dbea2df1d8 _pytest/nose.py
--- a/_pytest/nose.py
+++ b/_pytest/nose.py
@@ -24,7 +24,7 @@
call.excinfo = call2.excinfo
[email protected]
[email protected]_opts(trylast=True)
def pytest_runtest_setup(item):
if is_potential_nosetest(item):
if isinstance(item.parent, pytest.Generator):
diff -r e08baadb3d0b309df91b1189131e79c87583ad5b -r
24f4d48abeeb28d15a7a43249c7329dbea2df1d8 _pytest/pastebin.py
--- a/_pytest/pastebin.py
+++ b/_pytest/pastebin.py
@@ -11,7 +11,7 @@
choices=['failed', 'all'],
help="send failed|all info to bpaste.net pastebin service.")
[email protected]
[email protected]_opts(trylast=True)
def pytest_configure(config):
if config.option.pastebin == "all":
tr = config.pluginmanager.getplugin('terminalreporter')
diff -r e08baadb3d0b309df91b1189131e79c87583ad5b -r
24f4d48abeeb28d15a7a43249c7329dbea2df1d8 _pytest/pytester.py
--- a/_pytest/pytester.py
+++ b/_pytest/pytester.py
@@ -11,7 +11,7 @@
import py
import pytest
from py.builtin import print_
-from _pytest.core import HookCaller, add_method_wrapper
+from _pytest.core import TracedHookExecution
from _pytest.main import Session, EXIT_OK
@@ -79,12 +79,12 @@
self._pluginmanager = pluginmanager
self.calls = []
- def _docall(hookcaller, methods, kwargs):
- self.calls.append(ParsedCall(hookcaller.name, kwargs))
- yield
- self._undo_wrapping = add_method_wrapper(HookCaller, _docall)
- #if hasattr(pluginmanager, "config"):
- # pluginmanager.add_shutdown(self._undo_wrapping)
+ def before(hook, method, kwargs):
+ self.calls.append(ParsedCall(hook.name, kwargs))
+ def after(outcome, hook, method, kwargs):
+ pass
+ executor = TracedHookExecution(pluginmanager, before, after)
+ self._undo_wrapping = executor.undo
def finish_recording(self):
self._undo_wrapping()
diff -r e08baadb3d0b309df91b1189131e79c87583ad5b -r
24f4d48abeeb28d15a7a43249c7329dbea2df1d8 _pytest/python.py
--- a/_pytest/python.py
+++ b/_pytest/python.py
@@ -172,7 +172,7 @@
def pytest_sessionstart(session):
session._fixturemanager = FixtureManager(session)
[email protected]
[email protected]_opts(trylast=True)
def pytest_namespace():
raises.Exception = pytest.fail.Exception
return {
@@ -191,7 +191,7 @@
return request.config
[email protected]
[email protected]_opts(trylast=True)
def pytest_pyfunc_call(pyfuncitem):
testfunction = pyfuncitem.obj
if pyfuncitem._isyieldedfunction():
@@ -219,7 +219,7 @@
def pytest_pycollect_makemodule(path, parent):
return Module(path, parent)
[email protected]
[email protected]_opts(hookwrapper=True)
def pytest_pycollect_makeitem(collector, name, obj):
outcome = yield
res = outcome.get_result()
@@ -375,13 +375,16 @@
fixtureinfo = fm.getfixtureinfo(self, funcobj, cls)
metafunc = Metafunc(funcobj, fixtureinfo, self.config,
cls=cls, module=module)
- try:
- methods = [module.pytest_generate_tests]
- except AttributeError:
- methods = []
+ methods = []
+ if hasattr(module, "pytest_generate_tests"):
+ methods.append(module.pytest_generate_tests)
if hasattr(cls, "pytest_generate_tests"):
methods.append(cls().pytest_generate_tests)
- self.ihook.pytest_generate_tests.callextra(methods, metafunc=metafunc)
+ if methods:
+ self.ihook.pytest_generate_tests.call_extra(methods,
+
dict(metafunc=metafunc))
+ else:
+ self.ihook.pytest_generate_tests(metafunc=metafunc)
Function = self._getcustomclass("Function")
if not metafunc._calls:
@@ -1621,7 +1624,6 @@
self.session = session
self.config = session.config
self._arg2fixturedefs = {}
- self._seenplugins = set()
self._holderobjseen = set()
self._arg2finish = {}
self._nodeid_and_autousenames = [("",
self.config.getini("usefixtures"))]
@@ -1646,11 +1648,7 @@
node)
return FuncFixtureInfo(argnames, names_closure, arg2fixturedefs)
- ### XXX this hook should be called for historic events like
pytest_configure
- ### so that we don't have to do the below pytest_configure hook
def pytest_plugin_registered(self, plugin):
- if plugin in self._seenplugins:
- return
nodeid = None
try:
p = py.path.local(plugin.__file__)
@@ -1665,13 +1663,6 @@
if p.sep != "/":
nodeid = nodeid.replace(p.sep, "/")
self.parsefactories(plugin, nodeid)
- self._seenplugins.add(plugin)
-
- @pytest.mark.tryfirst
- def pytest_configure(self, config):
- plugins = config.pluginmanager.getplugins()
- for plugin in plugins:
- self.pytest_plugin_registered(plugin)
def _getautousenames(self, nodeid):
""" return a tuple of fixture names to be used. """
diff -r e08baadb3d0b309df91b1189131e79c87583ad5b -r
24f4d48abeeb28d15a7a43249c7329dbea2df1d8 _pytest/skipping.py
--- a/_pytest/skipping.py
+++ b/_pytest/skipping.py
@@ -133,7 +133,7 @@
return expl
[email protected]
[email protected]_opts(tryfirst=True)
def pytest_runtest_setup(item):
evalskip = MarkEvaluator(item, 'skipif')
if evalskip.istrue():
@@ -151,7 +151,7 @@
if not evalxfail.get('run', True):
pytest.xfail("[NOTRUN] " + evalxfail.getexplanation())
[email protected]
[email protected]_opts(hookwrapper=True)
def pytest_runtest_makereport(item, call):
outcome = yield
rep = outcome.get_result()
diff -r e08baadb3d0b309df91b1189131e79c87583ad5b -r
24f4d48abeeb28d15a7a43249c7329dbea2df1d8 _pytest/terminal.py
--- a/_pytest/terminal.py
+++ b/_pytest/terminal.py
@@ -164,6 +164,8 @@
def pytest_logwarning(self, code, fslocation, message, nodeid):
warnings = self.stats.setdefault("warnings", [])
+ if isinstance(fslocation, tuple):
+ fslocation = "%s:%d" % fslocation
warning = WarningReport(code=code, fslocation=fslocation,
message=message, nodeid=nodeid)
warnings.append(warning)
@@ -265,7 +267,7 @@
def pytest_collection_modifyitems(self):
self.report_collect(True)
- @pytest.mark.trylast
+ @pytest.hookimpl_opts(trylast=True)
def pytest_sessionstart(self, session):
self._sessionstarttime = time.time()
if not self.showheader:
@@ -350,7 +352,7 @@
indent = (len(stack) - 1) * " "
self._tw.line("%s%s" % (indent, col))
- @pytest.mark.hookwrapper
+ @pytest.hookimpl_opts(hookwrapper=True)
def pytest_sessionfinish(self, exitstatus):
outcome = yield
outcome.get_result()
diff -r e08baadb3d0b309df91b1189131e79c87583ad5b -r
24f4d48abeeb28d15a7a43249c7329dbea2df1d8 _pytest/unittest.py
--- a/_pytest/unittest.py
+++ b/_pytest/unittest.py
@@ -140,7 +140,7 @@
if traceback:
excinfo.traceback = traceback
[email protected]
[email protected]_opts(tryfirst=True)
def pytest_runtest_makereport(item, call):
if isinstance(item, TestCaseFunction):
if item._excinfo:
@@ -152,7 +152,7 @@
# twisted trial support
[email protected]
[email protected]_opts(hookwrapper=True)
def pytest_runtest_protocol(item):
if isinstance(item, TestCaseFunction) and \
'twisted.trial.unittest' in sys.modules:
diff -r e08baadb3d0b309df91b1189131e79c87583ad5b -r
24f4d48abeeb28d15a7a43249c7329dbea2df1d8 doc/en/example/markers.txt
--- a/doc/en/example/markers.txt
+++ b/doc/en/example/markers.txt
@@ -201,9 +201,9 @@
@pytest.mark.usefixtures(fixturename1, fixturename2, ...): mark tests as
needing all of the specified fixtures. see
http://pytest.org/latest/fixture.html#usefixtures
- @pytest.mark.tryfirst: mark a hook implementation function such that the
plugin machinery will try to call it first/as early as possible.
+ @pytest.hookimpl_opts(tryfirst=True): mark a hook implementation function
such that the plugin machinery will try to call it first/as early as possible.
- @pytest.mark.trylast: mark a hook implementation function such that the
plugin machinery will try to call it last/as late as possible.
+ @pytest.hookimpl_opts(trylast=True): mark a hook implementation function
such that the plugin machinery will try to call it last/as late as possible.
For an example on how to add and work with markers from a plugin, see
@@ -375,9 +375,9 @@
@pytest.mark.usefixtures(fixturename1, fixturename2, ...): mark tests as
needing all of the specified fixtures. see
http://pytest.org/latest/fixture.html#usefixtures
- @pytest.mark.tryfirst: mark a hook implementation function such that the
plugin machinery will try to call it first/as early as possible.
+ @pytest.hookimpl_opts(tryfirst=True): mark a hook implementation function
such that the plugin machinery will try to call it first/as early as possible.
- @pytest.mark.trylast: mark a hook implementation function such that the
plugin machinery will try to call it last/as late as possible.
+ @pytest.hookimpl_opts(trylast=True): mark a hook implementation function
such that the plugin machinery will try to call it last/as late as possible.
Reading markers which were set from multiple places
diff -r e08baadb3d0b309df91b1189131e79c87583ad5b -r
24f4d48abeeb28d15a7a43249c7329dbea2df1d8 doc/en/example/simple.txt
--- a/doc/en/example/simple.txt
+++ b/doc/en/example/simple.txt
@@ -534,7 +534,7 @@
import pytest
import os.path
- @pytest.mark.tryfirst
+ @pytest.hookimpl_opts(tryfirst=True)
def pytest_runtest_makereport(item, call, __multicall__):
# execute all other hooks to obtain the report object
rep = __multicall__.execute()
@@ -607,7 +607,7 @@
import pytest
- @pytest.mark.tryfirst
+ @pytest.hookimpl_opts(tryfirst=True)
def pytest_runtest_makereport(item, call, __multicall__):
# execute all other hooks to obtain the report object
rep = __multicall__.execute()
diff -r e08baadb3d0b309df91b1189131e79c87583ad5b -r
24f4d48abeeb28d15a7a43249c7329dbea2df1d8 doc/en/index.txt
--- a/doc/en/index.txt
+++ b/doc/en/index.txt
@@ -56,6 +56,7 @@
- all collection, reporting, running aspects are delegated to hook functions
- customizations can be per-directory, per-project or per PyPI released plugin
- it is easy to add command line options or customize existing behaviour
+ - :ref:`easy to write your own plugins <writing-plugins>`
.. _`easy`:
http://bruynooghe.blogspot.com/2009/12/skipping-slow-test-by-default-in-pytest.html
This diff is so big that we needed to truncate the remainder.
Repository URL: https://bitbucket.org/pytest-dev/pytest/
--
This is a commit notification from bitbucket.org. You are receiving
this because you have the service enabled, addressing the recipient of
this email.
_______________________________________________
pytest-commit mailing list
[email protected]
https://mail.python.org/mailman/listinfo/pytest-commit