diff --git a/changelog/7346.feature.rst b/changelog/7346.feature.rst new file mode 100644 index 000000000..fef0bbb78 --- /dev/null +++ b/changelog/7346.feature.rst @@ -0,0 +1 @@ +Version information as defined by `PEP 440 `_ may now be included when providing plugins to the ``required_plugins`` configuration option. diff --git a/doc/en/reference.rst b/doc/en/reference.rst index bf3d1fbbb..d5580ad65 100644 --- a/doc/en/reference.rst +++ b/doc/en/reference.rst @@ -1562,12 +1562,14 @@ 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. + Plugins can be listed with or without version specifiers directly following + their name. Whitespace between different version specifiers is not allowed. If any one of the plugins is not found, emit an error. .. code-block:: ini [pytest] - required_plugins = pytest-html pytest-xdist + required_plugins = pytest-django>=3.0.0,<4.0.0 pytest-html pytest-xdist>=1.0.0 .. confval:: testpaths diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 712191a54..4ff6ce707 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -1106,13 +1106,26 @@ class Config: if not required_plugins: return + # Imported lazily to improve start-up time. + from packaging.version import Version + from packaging.requirements import InvalidRequirement, Requirement + plugin_info = self.pluginmanager.list_plugin_distinfo() - plugin_dist_names = [dist.project_name for _, dist in plugin_info] + plugin_dist_info = {dist.project_name: dist.version for _, dist in plugin_info} missing_plugins = [] - for plugin in required_plugins: - if plugin not in plugin_dist_names: - missing_plugins.append(plugin) + for required_plugin in required_plugins: + spec = None + try: + spec = Requirement(required_plugin) + except InvalidRequirement: + missing_plugins.append(required_plugin) + continue + + if spec.name not in plugin_dist_info: + missing_plugins.append(required_plugin) + elif Version(plugin_dist_info[spec.name]) not in spec.specifier: + missing_plugins.append(required_plugin) if missing_plugins: fail( diff --git a/testing/test_config.py b/testing/test_config.py index c77ab1a48..d59d641f6 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -250,6 +250,63 @@ class TestParseIni: ), ( """ + [pytest] + required_plugins = pytest-xdist + """, + "", + ), + ( + """ + [pytest] + required_plugins = pytest-xdist==1.32.0 + """, + "", + ), + ( + """ + [pytest] + required_plugins = pytest-xdist>1.0.0,<2.0.0 + """, + "", + ), + ( + """ + [pytest] + required_plugins = pytest-xdist~=1.32.0 pytest-xdist==1.32.0 pytest-xdist!=0.0.1 pytest-xdist<=99.99.0 + pytest-xdist>=1.32.0 pytest-xdist<9.9.9 pytest-xdist>1.30.0 pytest-xdist===1.32.0 + """, + "", + ), + ( + """ + [pytest] + required_plugins = pytest-xdist>9.9.9 pytest-xdist==1.32.0 pytest-xdist==8.8.8 + """, + "Missing required plugins: pytest-xdist==8.8.8, pytest-xdist>9.9.9", + ), + ( + """ + [pytest] + required_plugins = pytest-xdist==aegsrgrsgs pytest-xdist==-1 pytest-xdist>2.1.1,>3.0.0 + """, + "Missing required plugins: pytest-xdist==-1, pytest-xdist==aegsrgrsgs, pytest-xdist>2.1.1,>3.0.0", + ), + ( + """ + [pytest] + required_plugins = pytest-xdist== pytest-xdist<= + """, + "Missing required plugins: pytest-xdist<=, pytest-xdist==", + ), + ( + """ + [pytest] + required_plugins = pytest-xdist= pytest-xdist< + """, + "Missing required plugins: pytest-xdist<, pytest-xdist=", + ), + ( + """ [some_other_header] required_plugins = wont be triggered [pytest]