From 7a96d94fd4f98f725ce04f7ac45de041b521b86d Mon Sep 17 00:00:00 2001 From: Joshua Storck Date: Wed, 30 Oct 2019 14:18:13 -0400 Subject: [PATCH] Making it possible to access the pluginmanager in the pytest_addoption hook --- changelog/6061.feature.rst | 5 ++++ doc/en/example/markers.rst | 2 +- doc/en/example/parametrize.rst | 2 +- doc/en/example/simple.rst | 4 +-- doc/en/writing_plugins.rst | 52 +++++++++++++++++++++++++++++++++- src/_pytest/config/__init__.py | 4 ++- src/_pytest/hookspec.py | 7 ++++- testing/test_pluginmanager.py | 30 ++++++++++++++++++++ 8 files changed, 99 insertions(+), 7 deletions(-) create mode 100644 changelog/6061.feature.rst diff --git a/changelog/6061.feature.rst b/changelog/6061.feature.rst new file mode 100644 index 000000000..b7804a008 --- /dev/null +++ b/changelog/6061.feature.rst @@ -0,0 +1,5 @@ +Adding the pluginmanager as an option to :py:func:`~hookspec.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. See +:ref:`Using hooks in pytest_addoption ` for more details. diff --git a/doc/en/example/markers.rst b/doc/en/example/markers.rst index ccddb1f66..8f81ff4d2 100644 --- a/doc/en/example/markers.rst +++ b/doc/en/example/markers.rst @@ -364,7 +364,7 @@ specifies via named environments: import pytest - def pytest_addoption(parser): + def pytest_addoption(parser, pluginmanager): parser.addoption( "-E", action="store", diff --git a/doc/en/example/parametrize.rst b/doc/en/example/parametrize.rst index 1220cfb4d..6f3e8031b 100644 --- a/doc/en/example/parametrize.rst +++ b/doc/en/example/parametrize.rst @@ -36,7 +36,7 @@ Now we add a test configuration like this: # content of conftest.py - def pytest_addoption(parser): + def pytest_addoption(parser, pluginmanager): parser.addoption("--all", action="store_true", help="run all combinations") diff --git a/doc/en/example/simple.rst b/doc/en/example/simple.rst index a7cd06d31..85000b46d 100644 --- a/doc/en/example/simple.rst +++ b/doc/en/example/simple.rst @@ -33,7 +33,7 @@ provide the ``cmdopt`` through a :ref:`fixture function `: import pytest - def pytest_addoption(parser): + def pytest_addoption(parser, pluginmanager): parser.addoption( "--cmdopt", action="store", default="type1", help="my option: type1 or type2" ) @@ -151,7 +151,7 @@ line option to control skipping of ``pytest.mark.slow`` marked tests: import pytest - def pytest_addoption(parser): + def pytest_addoption(parser, pluginmanager): parser.addoption( "--runslow", action="store_true", default=False, help="run slow tests" ) diff --git a/doc/en/writing_plugins.rst b/doc/en/writing_plugins.rst index 5f429c219..818974d0b 100644 --- a/doc/en/writing_plugins.rst +++ b/doc/en/writing_plugins.rst @@ -338,7 +338,7 @@ string value of ``Hello World!`` if we do not supply a value or ``Hello import pytest - def pytest_addoption(parser): + def pytest_addoption(parser, pluginmanager): group = parser.getgroup("helloworld") group.addoption( "--name", @@ -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 1bab9877c..d09f80043 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -697,7 +697,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 10a9857d7..3340b2153 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")