diff --git a/changelog/7305.feature.rst b/changelog/7305.feature.rst index b8c0ca693..25978a396 100644 --- a/changelog/7305.feature.rst +++ b/changelog/7305.feature.rst @@ -1,3 +1 @@ -New `require_plugins` configuration option allows the user to specify a list of plugins required for pytest to run. Warnings are raised if these plugins are not found when running pytest. - -The `--strict-config` flag can be used to treat these warnings as errors. +New `required_plugins` configuration option allows the user to specify a list of plugins required for pytest to run. An error is raised if any required plugins are not found when running pytest. diff --git a/doc/en/reference.rst b/doc/en/reference.rst index 6b270796c..f58881d02 100644 --- a/doc/en/reference.rst +++ b/doc/en/reference.rst @@ -1561,16 +1561,15 @@ passed multiple times. The expected format is ``name=value``. For example:: See :ref:`change naming conventions` for more detailed examples. -.. confval:: require_plugins +.. confval:: required_plugins A space separated list of plugins that must be present for pytest to run. - If any one of the plugins is not found, emit a warning. - If pytest is run with ``--strict-config`` exceptions are raised in place of warnings. + If any one of the plugins is not found, emit a error. .. code-block:: ini [pytest] - require_plugins = html xdist + required_plugins = pytest-html pytest-xdist .. confval:: testpaths diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index d55a5cdd7..483bc617f 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -953,7 +953,7 @@ class Config: self._parser.addini("addopts", "extra command line options", "args") self._parser.addini("minversion", "minimally required pytest version") self._parser.addini( - "require_plugins", + "required_plugins", "plugins that must be present for pytest to run", type="args", default=[], @@ -1090,11 +1090,21 @@ class Config: self._emit_warning_or_fail("Unknown config ini key: {}\n".format(key)) def _validate_plugins(self) -> None: - for plugin in self.getini("require_plugins"): - if not self.pluginmanager.hasplugin(plugin): - self._emit_warning_or_fail( - "Missing required plugin: {}\n".format(plugin) - ) + plugin_info = self.pluginmanager.list_plugin_distinfo() + plugin_dist_names = [ + "{dist.project_name}".format(dist=dist) for _, dist in plugin_info + ] + + required_plugin_list = [] + for plugin in sorted(self.getini("required_plugins")): + if plugin not in plugin_dist_names: + required_plugin_list.append(plugin) + + if required_plugin_list: + fail( + "Missing required plugins: {}".format(", ".join(required_plugin_list)), + pytrace=False, + ) def _emit_warning_or_fail(self, message: str) -> None: if self.known_args_namespace.strict_config: diff --git a/testing/test_config.py b/testing/test_config.py index f88a9a0ce..ab7f50ee5 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -213,51 +213,36 @@ class TestParseIni: testdir.runpytest("--strict-config") @pytest.mark.parametrize( - "ini_file_text, stderr_output, exception_text", + "ini_file_text, exception_text", [ ( """ [pytest] - require_plugins = fakePlugin1 fakePlugin2 + required_plugins = fakePlugin1 fakePlugin2 """, - [ - "WARNING: Missing required plugin: fakePlugin1", - "WARNING: Missing required plugin: fakePlugin2", - ], - "Missing required plugin: fakePlugin1", + "Missing required plugins: fakePlugin1, fakePlugin2", ), ( """ [pytest] - require_plugins = a monkeypatch z + required_plugins = a pytest-xdist z """, - [ - "WARNING: Missing required plugin: a", - "WARNING: Missing required plugin: z", - ], - "Missing required plugin: a", + "Missing required plugins: a, z", ), ( """ [pytest] - require_plugins = a monkeypatch z - addopts = -p no:monkeypatch + required_plugins = a q j b c z """, - [ - "WARNING: Missing required plugin: a", - "WARNING: Missing required plugin: monkeypatch", - "WARNING: Missing required plugin: z", - ], - "Missing required plugin: a", + "Missing required plugins: a, b, c, j, q, z", ), ( """ [some_other_header] - require_plugins = wont be triggered + required_plugins = wont be triggered [pytest] minversion = 5.0.0 """, - [], "", ), ( @@ -265,23 +250,21 @@ class TestParseIni: [pytest] minversion = 5.0.0 """, - [], "", ), ], ) - def test_missing_required_plugins( - self, testdir, ini_file_text, stderr_output, exception_text - ): + def test_missing_required_plugins(self, testdir, ini_file_text, exception_text): + pytest.importorskip("xdist") + testdir.tmpdir.join("pytest.ini").write(textwrap.dedent(ini_file_text)) - testdir.parseconfig() + testdir.monkeypatch.delenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD") - result = testdir.runpytest() - result.stderr.fnmatch_lines(stderr_output) - - if stderr_output: + if exception_text: with pytest.raises(pytest.fail.Exception, match=exception_text): - testdir.runpytest("--strict-config") + testdir.parseconfig() + else: + testdir.parseconfig() class TestConfigCmdlineParsing: @@ -681,6 +664,7 @@ def test_preparse_ordering_with_setuptools(testdir, monkeypatch): class Dist: files = () + metadata = {"name": "foo"} entry_points = (EntryPoint(),) def my_dists(): @@ -711,6 +695,7 @@ def test_setuptools_importerror_issue1479(testdir, monkeypatch): class Distribution: version = "1.0" files = ("foo.txt",) + metadata = {"name": "foo"} entry_points = (DummyEntryPoint(),) def distributions(): @@ -735,6 +720,7 @@ def test_importlib_metadata_broken_distribution(testdir, monkeypatch): class Distribution: version = "1.0" files = None + metadata = {"name": "foo"} entry_points = (DummyEntryPoint(),) def distributions(): @@ -760,6 +746,7 @@ def test_plugin_preparse_prevents_setuptools_loading(testdir, monkeypatch, block class Distribution: version = "1.0" files = ("foo.txt",) + metadata = {"name": "foo"} entry_points = (DummyEntryPoint(),) def distributions(): @@ -791,6 +778,7 @@ def test_disable_plugin_autoload(testdir, monkeypatch, parse_args, should_load): return sys.modules[self.name] class Distribution: + metadata = {"name": "foo"} entry_points = (DummyEntryPoint(),) files = ()