diff --git a/changelog/6061.feature.rst b/changelog/6061.feature.rst new file mode 100644 index 000000000..11f548625 --- /dev/null +++ b/changelog/6061.feature.rst @@ -0,0 +1,4 @@ +Adding the pluginmanager as an option ``pytest_addoption`` +so that hooks can be invoked when setting up command line options. This is +useful for having one plugin communicate things to another plugin, +such as default values or which set of command line options to add. diff --git a/doc/en/writing_plugins.rst b/doc/en/writing_plugins.rst index 5f429c219..8660746bd 100644 --- a/doc/en/writing_plugins.rst +++ b/doc/en/writing_plugins.rst @@ -677,6 +677,56 @@ Example: print(config.hook) +.. _`addoptionhooks`: + + +Using hooks in pytest_addoption +------------------------------- + +Occasionally, it is necessary to change the way in which command line options +are defined by one plugin based on hooks in another plugin. For example, +a plugin may expose a command line option for which another plugin needs +to define the default value. The pluginmanager can be used to install and +use hooks to accomplish this. The plugin would define and add the hooks +and use pytest_addoption as follows: + +.. code-block:: python + + # contents of hooks.py + + # Use firstresult=True because we only want one plugin to define this + # default value + @hookspec(firstresult=True) + def pytest_config_file_default_value(): + """ Return the default value for the config file command line option. """ + + + # contents of myplugin.py + + + def pytest_addhooks(pluginmanager): + """ This example assumes the hooks are grouped in the 'hooks' module. """ + from . import hook + + pluginmanager.add_hookspecs(hook) + + + def pytest_addoption(parser, pluginmanager): + default_value = pluginmanager.hook.pytest_config_file_default_value() + parser.addoption( + "--config-file", + help="Config file to use, defaults to %(default)s", + default=default_value, + ) + +The conftest.py that is using myplugin would simply define the hook as follows: + +.. code-block:: python + + def pytest_config_file_default_value(): + return "config.yaml" + + Optionally using hooks from 3rd party plugins --------------------------------------------- diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 2b0f48c07..8e11b56e5 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -705,7 +705,9 @@ class Config: self._cleanup = [] # type: List[Callable[[], None]] self.pluginmanager.register(self, "pytestconfig") self._configured = False - self.hook.pytest_addoption.call_historic(kwargs=dict(parser=self._parser)) + self.hook.pytest_addoption.call_historic( + kwargs=dict(parser=self._parser, pluginmanager=self.pluginmanager) + ) @property def invocation_dir(self): diff --git a/src/_pytest/hookspec.py b/src/_pytest/hookspec.py index 7a21837bd..8b45c5f9b 100644 --- a/src/_pytest/hookspec.py +++ b/src/_pytest/hookspec.py @@ -35,7 +35,7 @@ def pytest_plugin_registered(plugin, manager): @hookspec(historic=True) -def pytest_addoption(parser): +def pytest_addoption(parser, pluginmanager): """register argparse-style options and ini-style config values, called once at the beginning of a test run. @@ -50,6 +50,11 @@ def pytest_addoption(parser): To add ini-file values call :py:func:`parser.addini(...) <_pytest.config.Parser.addini>`. + :arg _pytest.config.PytestPluginManager pluginmanager: pytest plugin manager, + which can be used to install :py:func:`hookspec`'s or :py:func:`hookimpl`'s + and allow one plugin to call another plugin's hooks to change how + command line options are added. + Options can later be accessed through the :py:class:`config <_pytest.config.Config>` object, respectively: diff --git a/testing/test_pluginmanager.py b/testing/test_pluginmanager.py index 97f220ca5..836b458c6 100644 --- a/testing/test_pluginmanager.py +++ b/testing/test_pluginmanager.py @@ -135,6 +135,36 @@ class TestPytestPluginInteractions: ihook_b = session.gethookproxy(testdir.tmpdir.join("tests")) assert ihook_a is not ihook_b + def test_hook_with_addoption(self, testdir): + """Test that hooks can be used in a call to pytest_addoption""" + testdir.makepyfile( + newhooks=""" + import pytest + @pytest.hookspec(firstresult=True) + def pytest_default_value(): + pass + """ + ) + testdir.makepyfile( + myplugin=""" + import newhooks + def pytest_addhooks(pluginmanager): + pluginmanager.add_hookspecs(newhooks) + def pytest_addoption(parser, pluginmanager): + default_value = pluginmanager.hook.pytest_default_value() + parser.addoption("--config", help="Config, defaults to %(default)s", default=default_value) + """ + ) + testdir.makeconftest( + """ + pytest_plugins=("myplugin",) + def pytest_default_value(): + return "default_value" + """ + ) + res = testdir.runpytest("--help") + res.stdout.fnmatch_lines(["*--config=CONFIG*default_value*"]) + def test_default_markers(testdir): result = testdir.runpytest("--markers")