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 | - fix issue732: properly unregister plugins from any hook calling | ||||||
|   sites allowing to have temporary plugins during test execution. |   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) | 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 | # DON't import pytest here because it causes import cycle troubles | ||||||
| import sys, os | import sys, os | ||||||
| from _pytest import hookspec # the extension point definitions | 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 | # pytest startup | ||||||
| # | # | ||||||
|  | @ -117,6 +117,18 @@ class PytestPluginManager(PluginManager): | ||||||
|             self.trace.root.setwriter(err.write) |             self.trace.root.setwriter(err.write) | ||||||
|             self.enable_tracing() |             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): |     def register(self, plugin, name=None): | ||||||
|         ret = super(PytestPluginManager, self).register(plugin, name) |         ret = super(PytestPluginManager, self).register(plugin, name) | ||||||
|         if ret: |         if ret: | ||||||
|  | @ -138,6 +150,9 @@ class PytestPluginManager(PluginManager): | ||||||
|             "trylast: mark a hook implementation function such that the " |             "trylast: mark a hook implementation function such that the " | ||||||
|             "plugin machinery will try to call it last/as late as possible.") |             "plugin machinery will try to call it last/as late as possible.") | ||||||
|         for warning in self._warnings: |         for warning in self._warnings: | ||||||
|  |             if isinstance(warning, dict): | ||||||
|  |                 config.warn(**warning) | ||||||
|  |             else: | ||||||
|                 config.warn(code="I1", message=warning) |                 config.warn(code="I1", message=warning) | ||||||
| 
 | 
 | ||||||
|     # |     # | ||||||
|  | @ -712,10 +727,10 @@ class Config(object): | ||||||
|             fin = self._cleanup.pop() |             fin = self._cleanup.pop() | ||||||
|             fin() |             fin() | ||||||
| 
 | 
 | ||||||
|     def warn(self, code, message): |     def warn(self, code, message, fslocation=None): | ||||||
|         """ generate a warning for this test session. """ |         """ generate a warning for this test session. """ | ||||||
|         self.hook.pytest_logwarning(code=code, message=message, |         self.hook.pytest_logwarning(code=code, message=message, | ||||||
|                                     fslocation=None, nodeid=None) |                                     fslocation=fslocation, nodeid=None) | ||||||
| 
 | 
 | ||||||
|     def get_terminal_writer(self): |     def get_terminal_writer(self): | ||||||
|         return self.pluginmanager.get_plugin("terminalreporter")._tw |         return self.pluginmanager.get_plugin("terminalreporter")._tw | ||||||
|  |  | ||||||
|  | @ -408,6 +408,12 @@ class PluginManager(object): | ||||||
| class MultiCall: | class MultiCall: | ||||||
|     """ execute a call into multiple python functions/methods. """ |     """ 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): |     def __init__(self, methods, kwargs, firstresult=False): | ||||||
|         self.methods = methods |         self.methods = methods | ||||||
|         self.kwargs = kwargs |         self.kwargs = kwargs | ||||||
|  | @ -527,20 +533,20 @@ class HookCaller(object): | ||||||
| 
 | 
 | ||||||
|     def _add_method(self, meth): |     def _add_method(self, meth): | ||||||
|         if hasattr(meth, 'hookwrapper'): |         if hasattr(meth, 'hookwrapper'): | ||||||
|             self._wrappers.append(meth) |             methods = self._wrappers | ||||||
|         elif hasattr(meth, 'trylast'): |  | ||||||
|             self._nonwrappers.insert(0, meth) |  | ||||||
|         elif hasattr(meth, 'tryfirst'): |  | ||||||
|             self._nonwrappers.append(meth) |  | ||||||
|         else: |         else: | ||||||
|             # find the last nonwrapper which is not tryfirst marked |             methods = self._nonwrappers | ||||||
|             nonwrappers = self._nonwrappers |  | ||||||
|             i = len(nonwrappers) - 1 |  | ||||||
|             while i >= 0 and hasattr(nonwrappers[i], "tryfirst"): |  | ||||||
|                 i -= 1 |  | ||||||
| 
 | 
 | ||||||
|             # and insert right in front of the tryfirst ones |         if hasattr(meth, 'trylast'): | ||||||
|             nonwrappers.insert(i+1, meth) |             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): |     def __repr__(self): | ||||||
|         return "<HookCaller %r>" %(self.name,) |         return "<HookCaller %r>" %(self.name,) | ||||||
|  |  | ||||||
|  | @ -164,6 +164,8 @@ class TerminalReporter: | ||||||
| 
 | 
 | ||||||
|     def pytest_logwarning(self, code, fslocation, message, nodeid): |     def pytest_logwarning(self, code, fslocation, message, nodeid): | ||||||
|         warnings = self.stats.setdefault("warnings", []) |         warnings = self.stats.setdefault("warnings", []) | ||||||
|  |         if isinstance(fslocation, tuple): | ||||||
|  |             fslocation = "%s:%d" % fslocation | ||||||
|         warning = WarningReport(code=code, fslocation=fslocation, |         warning = WarningReport(code=code, fslocation=fslocation, | ||||||
|                                 message=message, nodeid=nodeid) |                                 message=message, nodeid=nodeid) | ||||||
|         warnings.append(warning) |         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 | breaking the signatures of existing hook implementations.  It is one of | ||||||
| the reasons for the general long-lived compatibility of pytest plugins. | 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 | Most calls to ``pytest`` hooks result in a **list of results** which contains | ||||||
| all non-None results of the called hook functions. | all non-None results of the called hook functions. | ||||||
| 
 | 
 | ||||||
| Some hooks are specified so that the hook call only executes until the | Some hook specifications use the ``firstresult=True`` option so that the hook | ||||||
| first function returned a non-None value which is then also the | call only executes until the first of N registered functions returns a | ||||||
| result of the overall hook call.  The remaining hook functions will | non-None result which is then taken as result of the overall hook call. | ||||||
| not be called in this case. | 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 |  | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| hookwrapper: executing around other hooks | 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 | If the result of the underlying hook is a mutable object, they may modify | ||||||
| that result, however. | 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 | Declaring new hooks | ||||||
| ------------------------ | ------------------------ | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -66,13 +66,12 @@ def check_open_files(config): | ||||||
|         error.append(error[0]) |         error.append(error[0]) | ||||||
|         raise AssertionError("\n".join(error)) |         raise AssertionError("\n".join(error)) | ||||||
| 
 | 
 | ||||||
| @pytest.hookimpl_opts(trylast=True) | @pytest.hookimpl_opts(hookwrapper=True, trylast=True) | ||||||
| def pytest_runtest_teardown(item, __multicall__): | def pytest_runtest_teardown(item): | ||||||
|  |     yield | ||||||
|     item.config._basedir.chdir() |     item.config._basedir.chdir() | ||||||
|     if hasattr(item.config, '_openfiles'): |     if hasattr(item.config, '_openfiles'): | ||||||
|         x = __multicall__.execute() |  | ||||||
|         check_open_files(item.config) |         check_open_files(item.config) | ||||||
|         return x |  | ||||||
| 
 | 
 | ||||||
| # XXX copied from execnet's conftest.py - needs to be merged | # XXX copied from execnet's conftest.py - needs to be merged | ||||||
| winpymap = { | winpymap = { | ||||||
|  |  | ||||||
|  | @ -336,6 +336,19 @@ class TestAddMethodOrdering: | ||||||
|         assert hc._nonwrappers == [he_method1_middle] |         assert hc._nonwrappers == [he_method1_middle] | ||||||
|         assert hc._wrappers == [he_method1, he_method3] |         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): |     def test_hookspec_opts(self, pm): | ||||||
|         class HookSpec: |         class HookSpec: | ||||||
|             @hookspec_opts() |             @hookspec_opts() | ||||||
|  | @ -530,6 +543,16 @@ class TestPytestPluginInteractions: | ||||||
|         finally: |         finally: | ||||||
|             undo() |             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): | def test_namespace_has_default_and_env_plugins(testdir): | ||||||
|     p = testdir.makepyfile(""" |     p = testdir.makepyfile(""" | ||||||
|  | @ -969,7 +992,7 @@ class TestPytestPluginManager: | ||||||
|         monkeypatch.setenv('PYTEST_PLUGINS', 'pytest_x500', prepend=",") |         monkeypatch.setenv('PYTEST_PLUGINS', 'pytest_x500', prepend=",") | ||||||
|         result = testdir.runpytest(p) |         result = testdir.runpytest(p) | ||||||
|         assert result.ret == 0 |         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): |     def test_import_plugin_importname(self, testdir, pytestpm): | ||||||
|         pytest.raises(ImportError, 'pytestpm.import_plugin("qweqwex.y")') |         pytest.raises(ImportError, 'pytestpm.import_plugin("qweqwex.y")') | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue