Skip to content

Commit aaa6f1c

Browse files
authored
Merge pull request #7330 from gnikonorov/issue_7305
2 parents 0f30103 + 1474f24 commit aaa6f1c

File tree

5 files changed

+106
-7
lines changed

5 files changed

+106
-7
lines changed

changelog/7305.feature.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +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.

doc/en/reference.rst

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1559,6 +1559,17 @@ passed multiple times. The expected format is ``name=value``. For example::
15591559
See :ref:`change naming conventions` for more detailed examples.
15601560

15611561

1562+
.. confval:: required_plugins
1563+
1564+
A space separated list of plugins that must be present for pytest to run.
1565+
If any one of the plugins is not found, emit an error.
1566+
1567+
.. code-block:: ini
1568+
1569+
[pytest]
1570+
required_plugins = pytest-html pytest-xdist
1571+
1572+
15621573
.. confval:: testpaths
15631574

15641575

src/_pytest/config/__init__.py

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -951,6 +951,12 @@ def _initini(self, args: Sequence[str]) -> None:
951951
self._parser.extra_info["inifile"] = self.inifile
952952
self._parser.addini("addopts", "extra command line options", "args")
953953
self._parser.addini("minversion", "minimally required pytest version")
954+
self._parser.addini(
955+
"required_plugins",
956+
"plugins that must be present for pytest to run",
957+
type="args",
958+
default=[],
959+
)
954960
self._override_ini = ns.override_ini or ()
955961

956962
def _consider_importhook(self, args: Sequence[str]) -> None:
@@ -1034,7 +1040,8 @@ def _preparse(self, args: List[str], addopts: bool = True) -> None:
10341040
self.known_args_namespace = ns = self._parser.parse_known_args(
10351041
args, namespace=copy.copy(self.option)
10361042
)
1037-
self._validatekeys()
1043+
self._validate_plugins()
1044+
self._validate_keys()
10381045
if self.known_args_namespace.confcutdir is None and self.inifile:
10391046
confcutdir = py.path.local(self.inifile).dirname
10401047
self.known_args_namespace.confcutdir = confcutdir
@@ -1072,12 +1079,33 @@ def _checkversion(self):
10721079
% (self.inifile, minver, pytest.__version__,)
10731080
)
10741081

1075-
def _validatekeys(self):
1082+
def _validate_keys(self) -> None:
10761083
for key in sorted(self._get_unknown_ini_keys()):
1077-
message = "Unknown config ini key: {}\n".format(key)
1078-
if self.known_args_namespace.strict_config:
1079-
fail(message, pytrace=False)
1080-
sys.stderr.write("WARNING: {}".format(message))
1084+
self._warn_or_fail_if_strict("Unknown config ini key: {}\n".format(key))
1085+
1086+
def _validate_plugins(self) -> None:
1087+
required_plugins = sorted(self.getini("required_plugins"))
1088+
if not required_plugins:
1089+
return
1090+
1091+
plugin_info = self.pluginmanager.list_plugin_distinfo()
1092+
plugin_dist_names = [dist.project_name for _, dist in plugin_info]
1093+
1094+
missing_plugins = []
1095+
for plugin in required_plugins:
1096+
if plugin not in plugin_dist_names:
1097+
missing_plugins.append(plugin)
1098+
1099+
if missing_plugins:
1100+
fail(
1101+
"Missing required plugins: {}".format(", ".join(missing_plugins)),
1102+
pytrace=False,
1103+
)
1104+
1105+
def _warn_or_fail_if_strict(self, message: str) -> None:
1106+
if self.known_args_namespace.strict_config:
1107+
fail(message, pytrace=False)
1108+
sys.stderr.write("WARNING: {}".format(message))
10811109

10821110
def _get_unknown_ini_keys(self) -> List[str]:
10831111
parser_inicfg = self._parser._inidict

src/_pytest/main.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ def pytest_addoption(parser: Parser) -> None:
8080
group._addoption(
8181
"--strict-config",
8282
action="store_true",
83-
help="invalid ini keys for the `pytest` section of the configuration file raise errors.",
83+
help="any warnings encountered while parsing the `pytest` section of the configuration file raise errors.",
8484
)
8585
group._addoption(
8686
"--strict-markers",

testing/test_config.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,60 @@ def test_invalid_ini_keys(
224224
with pytest.raises(pytest.fail.Exception, match=exception_text):
225225
testdir.runpytest("--strict-config")
226226

227+
@pytest.mark.parametrize(
228+
"ini_file_text, exception_text",
229+
[
230+
(
231+
"""
232+
[pytest]
233+
required_plugins = fakePlugin1 fakePlugin2
234+
""",
235+
"Missing required plugins: fakePlugin1, fakePlugin2",
236+
),
237+
(
238+
"""
239+
[pytest]
240+
required_plugins = a pytest-xdist z
241+
""",
242+
"Missing required plugins: a, z",
243+
),
244+
(
245+
"""
246+
[pytest]
247+
required_plugins = a q j b c z
248+
""",
249+
"Missing required plugins: a, b, c, j, q, z",
250+
),
251+
(
252+
"""
253+
[some_other_header]
254+
required_plugins = wont be triggered
255+
[pytest]
256+
minversion = 5.0.0
257+
""",
258+
"",
259+
),
260+
(
261+
"""
262+
[pytest]
263+
minversion = 5.0.0
264+
""",
265+
"",
266+
),
267+
],
268+
)
269+
def test_missing_required_plugins(self, testdir, ini_file_text, exception_text):
270+
pytest.importorskip("xdist")
271+
272+
testdir.tmpdir.join("pytest.ini").write(textwrap.dedent(ini_file_text))
273+
testdir.monkeypatch.delenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD")
274+
275+
if exception_text:
276+
with pytest.raises(pytest.fail.Exception, match=exception_text):
277+
testdir.parseconfig()
278+
else:
279+
testdir.parseconfig()
280+
227281

228282
class TestConfigCmdlineParsing:
229283
def test_parsing_again_fails(self, testdir):
@@ -681,6 +735,7 @@ class PseudoPlugin:
681735

682736
class Dist:
683737
files = ()
738+
metadata = {"name": "foo"}
684739
entry_points = (EntryPoint(),)
685740

686741
def my_dists():
@@ -711,6 +766,7 @@ def load(self):
711766
class Distribution:
712767
version = "1.0"
713768
files = ("foo.txt",)
769+
metadata = {"name": "foo"}
714770
entry_points = (DummyEntryPoint(),)
715771

716772
def distributions():
@@ -735,6 +791,7 @@ def load(self):
735791
class Distribution:
736792
version = "1.0"
737793
files = None
794+
metadata = {"name": "foo"}
738795
entry_points = (DummyEntryPoint(),)
739796

740797
def distributions():
@@ -760,6 +817,7 @@ def load(self):
760817
class Distribution:
761818
version = "1.0"
762819
files = ("foo.txt",)
820+
metadata = {"name": "foo"}
763821
entry_points = (DummyEntryPoint(),)
764822

765823
def distributions():
@@ -791,6 +849,7 @@ def load(self):
791849
return sys.modules[self.name]
792850

793851
class Distribution:
852+
metadata = {"name": "foo"}
794853
entry_points = (DummyEntryPoint(),)
795854
files = ()
796855

0 commit comments

Comments
 (0)