From 2a3c21645e5c303a71694c0ff68d0a56c2d734d5 Mon Sep 17 00:00:00 2001 From: Gleb Nikonorov Date: Sat, 6 Jun 2020 02:38:18 -0400 Subject: [PATCH 01/10] Commit solution thus far, needs to be polished up pre PR --- doc/en/reference.rst | 10 +++++ src/_pytest/config/__init__.py | 33 +++++++++++++--- testing/test_config.py | 71 ++++++++++++++++++++++++++++++++++ 3 files changed, 108 insertions(+), 6 deletions(-) diff --git a/doc/en/reference.rst b/doc/en/reference.rst index 7348636a2..d84d9d405 100644 --- a/doc/en/reference.rst +++ b/doc/en/reference.rst @@ -1604,3 +1604,13 @@ passed multiple times. The expected format is ``name=value``. For example:: [pytest] xfail_strict = True + + +.. confval:: required_plugins + + A space seperated list of plugins that must be present for pytest to run + + .. code-block:: ini + + [pytest] + require_plugins = pluginA pluginB pluginC diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 27083900d..83878a486 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -952,6 +952,12 @@ class Config: self._parser.extra_info["inifile"] = self.inifile self._parser.addini("addopts", "extra command line options", "args") self._parser.addini("minversion", "minimally required pytest version") + self._parser.addini( + "require_plugins", + "plugins that must be present for pytest to run", + type="args", + default=[], + ) self._override_ini = ns.override_ini or () def _consider_importhook(self, args: Sequence[str]) -> None: @@ -1035,7 +1041,8 @@ class Config: self.known_args_namespace = ns = self._parser.parse_known_args( args, namespace=copy.copy(self.option) ) - self._validatekeys() + self._validate_keys() + self._validate_plugins() if self.known_args_namespace.confcutdir is None and self.inifile: confcutdir = py.path.local(self.inifile).dirname self.known_args_namespace.confcutdir = confcutdir @@ -1078,12 +1085,26 @@ class Config: ) ) - def _validatekeys(self): + def _validate_keys(self) -> None: for key in sorted(self._get_unknown_ini_keys()): - message = "Unknown config ini key: {}\n".format(key) - if self.known_args_namespace.strict_config: - fail(message, pytrace=False) - sys.stderr.write("WARNING: {}".format(message)) + self._emit_warning_or_fail("Unknown config ini key: {}\n".format(key)) + + def _validate_plugins(self) -> None: + # so iterate over all required plugins and see if pluginmanager hasplugin + # NOTE: This also account for -p no: ( e.g: -p no:celery ) + # raise ValueError(self._parser._inidict['requiredplugins']) + # raise ValueError(self.getini("requiredplugins")) + # raise ValueError(self.pluginmanager.hasplugin('debugging')) + for plugin in self.getini("require_plugins"): + if not self.pluginmanager.hasplugin(plugin): + self._emit_warning_or_fail( + "Missing required plugin: {}\n".format(plugin) + ) + + def _emit_warning_or_fail(self, message: str) -> None: + if self.known_args_namespace.strict_config: + fail(message, pytrace=False) + sys.stderr.write("WARNING: {}".format(message)) def _get_unknown_ini_keys(self) -> List[str]: parser_inicfg = self._parser._inidict diff --git a/testing/test_config.py b/testing/test_config.py index 867012e93..f88a9a0ce 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -212,6 +212,77 @@ class TestParseIni: with pytest.raises(pytest.fail.Exception, match=exception_text): testdir.runpytest("--strict-config") + @pytest.mark.parametrize( + "ini_file_text, stderr_output, exception_text", + [ + ( + """ + [pytest] + require_plugins = fakePlugin1 fakePlugin2 + """, + [ + "WARNING: Missing required plugin: fakePlugin1", + "WARNING: Missing required plugin: fakePlugin2", + ], + "Missing required plugin: fakePlugin1", + ), + ( + """ + [pytest] + require_plugins = a monkeypatch z + """, + [ + "WARNING: Missing required plugin: a", + "WARNING: Missing required plugin: z", + ], + "Missing required plugin: a", + ), + ( + """ + [pytest] + require_plugins = a monkeypatch z + addopts = -p no:monkeypatch + """, + [ + "WARNING: Missing required plugin: a", + "WARNING: Missing required plugin: monkeypatch", + "WARNING: Missing required plugin: z", + ], + "Missing required plugin: a", + ), + ( + """ + [some_other_header] + require_plugins = wont be triggered + [pytest] + minversion = 5.0.0 + """, + [], + "", + ), + ( + """ + [pytest] + minversion = 5.0.0 + """, + [], + "", + ), + ], + ) + def test_missing_required_plugins( + self, testdir, ini_file_text, stderr_output, exception_text + ): + testdir.tmpdir.join("pytest.ini").write(textwrap.dedent(ini_file_text)) + testdir.parseconfig() + + result = testdir.runpytest() + result.stderr.fnmatch_lines(stderr_output) + + if stderr_output: + with pytest.raises(pytest.fail.Exception, match=exception_text): + testdir.runpytest("--strict-config") + class TestConfigCmdlineParsing: def test_parsing_again_fails(self, testdir): From f760b105efa12ebc14adccda3c840ad3a61936ef Mon Sep 17 00:00:00 2001 From: Gleb Nikonorov Date: Sat, 6 Jun 2020 11:05:32 -0400 Subject: [PATCH 02/10] Touchup pre-PR --- changelog/7305.feature.rst | 3 +++ doc/en/reference.rst | 22 ++++++++++++---------- src/_pytest/config/__init__.py | 5 ----- 3 files changed, 15 insertions(+), 15 deletions(-) create mode 100644 changelog/7305.feature.rst diff --git a/changelog/7305.feature.rst b/changelog/7305.feature.rst new file mode 100644 index 000000000..cf5a48c6e --- /dev/null +++ b/changelog/7305.feature.rst @@ -0,0 +1,3 @@ +A new INI key `require_plugins` has been added that allows the user to specify a list of plugins required for pytest to run. + +The `--strict-config` flag can be used to treat these warnings as errors. diff --git a/doc/en/reference.rst b/doc/en/reference.rst index d84d9d405..1f1f2c423 100644 --- a/doc/en/reference.rst +++ b/doc/en/reference.rst @@ -1561,6 +1561,18 @@ passed multiple times. The expected format is ``name=value``. For example:: See :ref:`change naming conventions` for more detailed examples. +.. confval:: require_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. + + .. code-block:: ini + + [pytest] + require_plugins = pluginA pluginB pluginC + + .. confval:: testpaths @@ -1604,13 +1616,3 @@ passed multiple times. The expected format is ``name=value``. For example:: [pytest] xfail_strict = True - - -.. confval:: required_plugins - - A space seperated list of plugins that must be present for pytest to run - - .. code-block:: ini - - [pytest] - require_plugins = pluginA pluginB pluginC diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 83878a486..7d077d297 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -1090,11 +1090,6 @@ class Config: self._emit_warning_or_fail("Unknown config ini key: {}\n".format(key)) def _validate_plugins(self) -> None: - # so iterate over all required plugins and see if pluginmanager hasplugin - # NOTE: This also account for -p no: ( e.g: -p no:celery ) - # raise ValueError(self._parser._inidict['requiredplugins']) - # raise ValueError(self.getini("requiredplugins")) - # raise ValueError(self.pluginmanager.hasplugin('debugging')) for plugin in self.getini("require_plugins"): if not self.pluginmanager.hasplugin(plugin): self._emit_warning_or_fail( From 3f6b3e7faa49c891e0b3036f07873296a73c8618 Mon Sep 17 00:00:00 2001 From: Gleb Nikonorov Date: Sat, 6 Jun 2020 11:33:28 -0400 Subject: [PATCH 03/10] update help for --strict-config --- src/_pytest/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_pytest/main.py b/src/_pytest/main.py index 1c1cda18b..fd39b6ad7 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -78,7 +78,7 @@ def pytest_addoption(parser: Parser) -> None: group._addoption( "--strict-config", action="store_true", - help="invalid ini keys for the `pytest` section of the configuration file raise errors.", + help="any warnings encountered while parsing the `pytest` section of the configuration file raise errors.", ) group._addoption( "--strict-markers", From 42deba59e7d6cfe596414d0beff6fafaa14b02a3 Mon Sep 17 00:00:00 2001 From: Gleb Nikonorov Date: Sat, 6 Jun 2020 22:34:15 -0400 Subject: [PATCH 04/10] Update documentation as suggested --- changelog/7305.feature.rst | 2 +- doc/en/reference.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/changelog/7305.feature.rst b/changelog/7305.feature.rst index cf5a48c6e..8e8ae85ae 100644 --- a/changelog/7305.feature.rst +++ b/changelog/7305.feature.rst @@ -1,3 +1,3 @@ -A new INI key `require_plugins` has been added that allows the user to specify a list of plugins required for pytest to run. +New `require_plugins` configuration option allows the user to specify a list of plugins required for pytest to run. The `--strict-config` flag can be used to treat these warnings as errors. diff --git a/doc/en/reference.rst b/doc/en/reference.rst index 1f1f2c423..dc82fe239 100644 --- a/doc/en/reference.rst +++ b/doc/en/reference.rst @@ -1570,7 +1570,7 @@ passed multiple times. The expected format is ``name=value``. For example:: .. code-block:: ini [pytest] - require_plugins = pluginA pluginB pluginC + require_plugins = pytest-xdist pytest-mock .. confval:: testpaths From d2bb67bfdafcbadd39f9551a52635188f54954e0 Mon Sep 17 00:00:00 2001 From: Gleb Nikonorov Date: Sun, 7 Jun 2020 14:10:20 -0400 Subject: [PATCH 05/10] validate plugins before keys in config files --- src/_pytest/config/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 7d077d297..d55a5cdd7 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -1041,8 +1041,8 @@ class Config: self.known_args_namespace = ns = self._parser.parse_known_args( args, namespace=copy.copy(self.option) ) - self._validate_keys() self._validate_plugins() + self._validate_keys() if self.known_args_namespace.confcutdir is None and self.inifile: confcutdir = py.path.local(self.inifile).dirname self.known_args_namespace.confcutdir = confcutdir From 13add4df43eef412bf7369926345e62eca0624b1 Mon Sep 17 00:00:00 2001 From: Gleb Nikonorov Date: Sun, 7 Jun 2020 15:37:50 -0400 Subject: [PATCH 06/10] documentation fixes --- changelog/7305.feature.rst | 2 +- doc/en/reference.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/changelog/7305.feature.rst b/changelog/7305.feature.rst index 8e8ae85ae..b8c0ca693 100644 --- a/changelog/7305.feature.rst +++ b/changelog/7305.feature.rst @@ -1,3 +1,3 @@ -New `require_plugins` configuration option allows the user to specify a list of plugins required for pytest to run. +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. diff --git a/doc/en/reference.rst b/doc/en/reference.rst index dc82fe239..6b270796c 100644 --- a/doc/en/reference.rst +++ b/doc/en/reference.rst @@ -1570,7 +1570,7 @@ passed multiple times. The expected format is ``name=value``. For example:: .. code-block:: ini [pytest] - require_plugins = pytest-xdist pytest-mock + require_plugins = html xdist .. confval:: testpaths From 95cb7fb676405fe9281252b68bc80f5de747a4de Mon Sep 17 00:00:00 2001 From: Gleb Nikonorov Date: Wed, 10 Jun 2020 00:44:22 -0400 Subject: [PATCH 07/10] review feedback --- changelog/7305.feature.rst | 4 +-- doc/en/reference.rst | 7 ++--- src/_pytest/config/__init__.py | 22 ++++++++++---- testing/test_config.py | 54 +++++++++++++--------------------- 4 files changed, 41 insertions(+), 46 deletions(-) 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 = () From c18afb59f50d280c34e1a7a1fd4a49831952d860 Mon Sep 17 00:00:00 2001 From: Gleb Nikonorov Date: Wed, 10 Jun 2020 19:09:24 -0400 Subject: [PATCH 08/10] final touches --- src/_pytest/config/__init__.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 483bc617f..16bf75b6e 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -1090,19 +1090,21 @@ class Config: self._emit_warning_or_fail("Unknown config ini key: {}\n".format(key)) def _validate_plugins(self) -> None: + required_plugins = sorted(self.getini("required_plugins")) + if not required_plugins: + return + plugin_info = self.pluginmanager.list_plugin_distinfo() - plugin_dist_names = [ - "{dist.project_name}".format(dist=dist) for _, dist in plugin_info - ] + plugin_dist_names = [dist.project_name for _, dist in plugin_info] - required_plugin_list = [] - for plugin in sorted(self.getini("required_plugins")): + missing_plugins = [] + for plugin in required_plugins: if plugin not in plugin_dist_names: - required_plugin_list.append(plugin) + missing_plugins.append(plugin) - if required_plugin_list: + if missing_plugins: fail( - "Missing required plugins: {}".format(", ".join(required_plugin_list)), + "Missing required plugins: {}".format(", ".join(missing_plugins)), pytrace=False, ) From 57415e68ee6355325410e2326039262f7c605360 Mon Sep 17 00:00:00 2001 From: Gleb Nikonorov Date: Thu, 11 Jun 2020 16:55:25 -0400 Subject: [PATCH 09/10] Update changelog/7305.feature.rst Co-authored-by: Ran Benita --- changelog/7305.feature.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog/7305.feature.rst b/changelog/7305.feature.rst index 25978a396..96b7f72ee 100644 --- a/changelog/7305.feature.rst +++ b/changelog/7305.feature.rst @@ -1 +1 @@ -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. +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. From 2c8e356174d9760a28f5ff6a3b5754417d41b7bc Mon Sep 17 00:00:00 2001 From: Gleb Nikonorov Date: Fri, 12 Jun 2020 08:27:55 -0400 Subject: [PATCH 10/10] rename _emit_warning_or_fail to _warn_or_fail_if_strict and fix a doc typo --- doc/en/reference.rst | 2 +- src/_pytest/config/__init__.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/en/reference.rst b/doc/en/reference.rst index f58881d02..2fab4160c 100644 --- a/doc/en/reference.rst +++ b/doc/en/reference.rst @@ -1564,7 +1564,7 @@ passed multiple times. The expected format is ``name=value``. For example:: .. 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 error. + If any one of the plugins is not found, emit an error. .. code-block:: ini diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 16bf75b6e..07985df2d 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -1087,7 +1087,7 @@ class Config: def _validate_keys(self) -> None: for key in sorted(self._get_unknown_ini_keys()): - self._emit_warning_or_fail("Unknown config ini key: {}\n".format(key)) + self._warn_or_fail_if_strict("Unknown config ini key: {}\n".format(key)) def _validate_plugins(self) -> None: required_plugins = sorted(self.getini("required_plugins")) @@ -1108,7 +1108,7 @@ class Config: pytrace=False, ) - def _emit_warning_or_fail(self, message: str) -> None: + def _warn_or_fail_if_strict(self, message: str) -> None: if self.known_args_namespace.strict_config: fail(message, pytrace=False) sys.stderr.write("WARNING: {}".format(message))