deprecate and warn about __multicall__ usage in hooks, refine docs about hook ordering,
make hookwrappers respect tryfirst/trylast --HG-- branch : more_plugin
This commit is contained in:
parent
dea1c96031
commit
c54afbe42e
|
@ -39,6 +39,10 @@
|
|||
- 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)
|
||||
-----------------------------
|
||||
|
|
|
@ -9,7 +9,7 @@ import py
|
|||
# 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, hookimpl_opts
|
||||
from _pytest.core import PluginManager, hookimpl_opts, varnames
|
||||
|
||||
# pytest startup
|
||||
#
|
||||
|
@ -117,6 +117,18 @@ class PytestPluginManager(PluginManager):
|
|||
self.trace.root.setwriter(err.write)
|
||||
self.enable_tracing()
|
||||
|
||||
|
||||
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:
|
||||
|
@ -138,6 +150,9 @@ class PytestPluginManager(PluginManager):
|
|||
"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:
|
||||
if isinstance(warning, dict):
|
||||
config.warn(**warning)
|
||||
else:
|
||||
config.warn(code="I1", message=warning)
|
||||
|
||||
#
|
||||
|
@ -712,10 +727,10 @@ class Config(object):
|
|||
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.get_plugin("terminalreporter")._tw
|
||||
|
|
|
@ -408,6 +408,12 @@ class PluginManager(object):
|
|||
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 = methods
|
||||
self.kwargs = kwargs
|
||||
|
@ -527,20 +533,20 @@ class HookCaller(object):
|
|||
|
||||
def _add_method(self, meth):
|
||||
if hasattr(meth, 'hookwrapper'):
|
||||
self._wrappers.append(meth)
|
||||
elif hasattr(meth, 'trylast'):
|
||||
self._nonwrappers.insert(0, meth)
|
||||
elif hasattr(meth, 'tryfirst'):
|
||||
self._nonwrappers.append(meth)
|
||||
methods = self._wrappers
|
||||
else:
|
||||
# find the last nonwrapper which is not tryfirst marked
|
||||
nonwrappers = self._nonwrappers
|
||||
i = len(nonwrappers) - 1
|
||||
while i >= 0 and hasattr(nonwrappers[i], "tryfirst"):
|
||||
i -= 1
|
||||
methods = self._nonwrappers
|
||||
|
||||
# and insert right in front of the tryfirst ones
|
||||
nonwrappers.insert(i+1, meth)
|
||||
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,)
|
||||
|
|
|
@ -164,6 +164,8 @@ class TerminalReporter:
|
|||
|
||||
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)
|
||||
|
|
|
@ -221,36 +221,21 @@ be "future-compatible": we can introduce new hook named parameters without
|
|||
breaking the signatures of existing hook implementations. It is one of
|
||||
the reasons for the general long-lived compatibility of pytest plugins.
|
||||
|
||||
Hook function results
|
||||
---------------------
|
||||
Note that hook functions other than ``pytest_runtest_*`` are not
|
||||
allowed to raise exceptions. Doing so will break the pytest run.
|
||||
|
||||
|
||||
|
||||
firstresult: stop at first non-None result
|
||||
-------------------------------------------
|
||||
|
||||
Most calls to ``pytest`` hooks result in a **list of results** which contains
|
||||
all non-None results of the called hook functions.
|
||||
|
||||
Some hooks are specified so that the hook call only executes until the
|
||||
first function returned a non-None value which is then also the
|
||||
result of the overall hook call. The remaining hook functions will
|
||||
not be called in this case.
|
||||
|
||||
Note that hook functions other than ``pytest_runtest_*`` are not
|
||||
allowed to raise exceptions. Doing so will break the pytest run.
|
||||
|
||||
Hook function ordering
|
||||
----------------------
|
||||
|
||||
For any given hook there may be more than one implementation and we thus
|
||||
generally view ``hook`` execution as a ``1:N`` function call where ``N``
|
||||
is the number of registered functions. There are ways to
|
||||
influence if a hook implementation comes before or after others, i.e.
|
||||
the position in the ``N``-sized list of functions::
|
||||
|
||||
@pytest.hookimpl_spec(tryfirst=True)
|
||||
def pytest_collection_modifyitems(items):
|
||||
# will execute as early as possible
|
||||
|
||||
@pytest.hookimpl_spec(trylast=True)
|
||||
def pytest_collection_modifyitems(items):
|
||||
# will execute as late as possible
|
||||
Some hook specifications use the ``firstresult=True`` option so that the hook
|
||||
call only executes until the first of N registered functions returns a
|
||||
non-None result which is then taken as result of the overall hook call.
|
||||
The remaining hook functions will not be called in this case.
|
||||
|
||||
|
||||
hookwrapper: executing around other hooks
|
||||
|
@ -290,6 +275,47 @@ perform tracing or other side effects around the actual hook implementations.
|
|||
If the result of the underlying hook is a mutable object, they may modify
|
||||
that result, however.
|
||||
|
||||
|
||||
|
||||
Hook function ordering / call example
|
||||
-------------------------------------
|
||||
|
||||
For any given hook specification there may be more than one
|
||||
implementation and we thus generally view ``hook`` execution as a
|
||||
``1:N`` function call where ``N`` is the number of registered functions.
|
||||
There are ways to influence if a hook implementation comes before or
|
||||
after others, i.e. the position in the ``N``-sized list of functions::
|
||||
|
||||
# Plugin 1
|
||||
@pytest.hookimpl_spec(tryfirst=True)
|
||||
def pytest_collection_modifyitems(items):
|
||||
# will execute as early as possible
|
||||
|
||||
# Plugin 2
|
||||
@pytest.hookimpl_spec(trylast=True)
|
||||
def pytest_collection_modifyitems(items):
|
||||
# will execute as late as possible
|
||||
|
||||
# Plugin 3
|
||||
@pytest.hookimpl_spec(hookwrapper=True)
|
||||
def pytest_collection_modifyitems(items):
|
||||
# will execute even before the tryfirst one above!
|
||||
outcome = yield
|
||||
# will execute after all non-hookwrappers executed
|
||||
|
||||
Here is the order of execution:
|
||||
|
||||
1. Plugin3's pytest_collection_modifyitems called until the yield point
|
||||
2. Plugin1's pytest_collection_modifyitems is called
|
||||
3. Plugin2's pytest_collection_modifyitems is called
|
||||
4. Plugin3's pytest_collection_modifyitems called for executing after the yield
|
||||
The yield receives a :py:class:`CallOutcome` instance which encapsulates
|
||||
the result from calling the non-wrappers. Wrappers cannot modify the result.
|
||||
|
||||
It's possible to use ``tryfirst`` and ``trylast`` also in conjunction with
|
||||
``hookwrapper=True`` in which case it will influence the ordering of hookwrappers
|
||||
among each other.
|
||||
|
||||
Declaring new hooks
|
||||
------------------------
|
||||
|
||||
|
|
|
@ -66,13 +66,12 @@ def check_open_files(config):
|
|||
error.append(error[0])
|
||||
raise AssertionError("\n".join(error))
|
||||
|
||||
@pytest.hookimpl_opts(trylast=True)
|
||||
def pytest_runtest_teardown(item, __multicall__):
|
||||
@pytest.hookimpl_opts(hookwrapper=True, trylast=True)
|
||||
def pytest_runtest_teardown(item):
|
||||
yield
|
||||
item.config._basedir.chdir()
|
||||
if hasattr(item.config, '_openfiles'):
|
||||
x = __multicall__.execute()
|
||||
check_open_files(item.config)
|
||||
return x
|
||||
|
||||
# XXX copied from execnet's conftest.py - needs to be merged
|
||||
winpymap = {
|
||||
|
|
|
@ -336,6 +336,19 @@ class TestAddMethodOrdering:
|
|||
assert hc._nonwrappers == [he_method1_middle]
|
||||
assert hc._wrappers == [he_method1, he_method3]
|
||||
|
||||
def test_adding_wrappers_ordering_tryfirst(self, hc, addmeth):
|
||||
@addmeth(hookwrapper=True, tryfirst=True)
|
||||
def he_method1():
|
||||
pass
|
||||
|
||||
@addmeth(hookwrapper=True)
|
||||
def he_method2():
|
||||
pass
|
||||
|
||||
assert hc._nonwrappers == []
|
||||
assert hc._wrappers == [he_method2, he_method1]
|
||||
|
||||
|
||||
def test_hookspec_opts(self, pm):
|
||||
class HookSpec:
|
||||
@hookspec_opts()
|
||||
|
@ -530,6 +543,16 @@ class TestPytestPluginInteractions:
|
|||
finally:
|
||||
undo()
|
||||
|
||||
def test_warn_on_deprecated_multicall(self, pytestpm):
|
||||
class Plugin:
|
||||
def pytest_configure(self, __multicall__):
|
||||
pass
|
||||
|
||||
before = list(pytestpm._warnings)
|
||||
pytestpm.register(Plugin())
|
||||
assert len(pytestpm._warnings) == len(before) + 1
|
||||
assert "deprecated" in pytestpm._warnings[-1]["message"]
|
||||
|
||||
|
||||
def test_namespace_has_default_and_env_plugins(testdir):
|
||||
p = testdir.makepyfile("""
|
||||
|
@ -969,7 +992,7 @@ class TestPytestPluginManager:
|
|||
monkeypatch.setenv('PYTEST_PLUGINS', 'pytest_x500', prepend=",")
|
||||
result = testdir.runpytest(p)
|
||||
assert result.ret == 0
|
||||
result.stdout.fnmatch_lines(["*1 passed in*"])
|
||||
result.stdout.fnmatch_lines(["*1 passed*"])
|
||||
|
||||
def test_import_plugin_importname(self, testdir, pytestpm):
|
||||
pytest.raises(ImportError, 'pytestpm.import_plugin("qweqwex.y")')
|
||||
|
|
Loading…
Reference in New Issue