From 09f53fee839fb9bc8e64a5d837358bb8d534dd43 Mon Sep 17 00:00:00 2001 From: Casey Brooks Date: Fri, 26 Dec 2025 21:10:12 +0000 Subject: [PATCH] fix(extension): use pep 440 version check --- sphinx/extension.py | 18 ++++- tests/roots/test_needs_extensions/__init__.py | 0 tests/roots/test_needs_extensions/ext_dev.py | 1 + .../roots/test_needs_extensions/ext_exact.py | 1 + .../roots/test_needs_extensions/ext_newer.py | 1 + tests/roots/test_needs_extensions/ext_post.py | 1 + .../test_needs_extensions/ext_prerelease.py | 1 + .../test_needs_extensions/ext_unknown.py | 1 + tests/test_extension_needs.py | 73 +++++++++++++++++++ 9 files changed, 96 insertions(+), 1 deletion(-) create mode 100644 tests/roots/test_needs_extensions/__init__.py create mode 100644 tests/roots/test_needs_extensions/ext_dev.py create mode 100644 tests/roots/test_needs_extensions/ext_exact.py create mode 100644 tests/roots/test_needs_extensions/ext_newer.py create mode 100644 tests/roots/test_needs_extensions/ext_post.py create mode 100644 tests/roots/test_needs_extensions/ext_prerelease.py create mode 100644 tests/roots/test_needs_extensions/ext_unknown.py create mode 100644 tests/test_extension_needs.py diff --git a/sphinx/extension.py b/sphinx/extension.py index 7ec6c8518c3..3428bac4200 100644 --- a/sphinx/extension.py +++ b/sphinx/extension.py @@ -10,6 +10,8 @@ from typing import TYPE_CHECKING, Any, Dict +from packaging.version import InvalidVersion, Version + from sphinx.config import Config from sphinx.errors import VersionRequirementError from sphinx.locale import __ @@ -51,7 +53,21 @@ def verify_needs_extensions(app: "Sphinx", config: Config) -> None: 'but it is not loaded.'), extname) continue - if extension.version == 'unknown version' or reqversion > extension.version: + if extension.version == 'unknown version': + raise VersionRequirementError(__('This project needs the extension %s at least in ' + 'version %s and therefore cannot be built with ' + 'the loaded version (%s).') % + (extname, reqversion, extension.version)) + + try: + installed_version = Version(extension.version) + required_version = Version(reqversion) + except InvalidVersion: + version_outdated = reqversion > extension.version + else: + version_outdated = installed_version < required_version + + if version_outdated: raise VersionRequirementError(__('This project needs the extension %s at least in ' 'version %s and therefore cannot be built with ' 'the loaded version (%s).') % diff --git a/tests/roots/test_needs_extensions/__init__.py b/tests/roots/test_needs_extensions/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/roots/test_needs_extensions/ext_dev.py b/tests/roots/test_needs_extensions/ext_dev.py new file mode 100644 index 00000000000..0bd62788148 --- /dev/null +++ b/tests/roots/test_needs_extensions/ext_dev.py @@ -0,0 +1 @@ +__version__ = '0.6.0.dev1' diff --git a/tests/roots/test_needs_extensions/ext_exact.py b/tests/roots/test_needs_extensions/ext_exact.py new file mode 100644 index 00000000000..ef7eb44d9ac --- /dev/null +++ b/tests/roots/test_needs_extensions/ext_exact.py @@ -0,0 +1 @@ +__version__ = '0.6.0' diff --git a/tests/roots/test_needs_extensions/ext_newer.py b/tests/roots/test_needs_extensions/ext_newer.py new file mode 100644 index 00000000000..9d1bb721beb --- /dev/null +++ b/tests/roots/test_needs_extensions/ext_newer.py @@ -0,0 +1 @@ +__version__ = '0.10.0' diff --git a/tests/roots/test_needs_extensions/ext_post.py b/tests/roots/test_needs_extensions/ext_post.py new file mode 100644 index 00000000000..0e0c9373272 --- /dev/null +++ b/tests/roots/test_needs_extensions/ext_post.py @@ -0,0 +1 @@ +__version__ = '0.6.0.post1' diff --git a/tests/roots/test_needs_extensions/ext_prerelease.py b/tests/roots/test_needs_extensions/ext_prerelease.py new file mode 100644 index 00000000000..85e1e58ddf7 --- /dev/null +++ b/tests/roots/test_needs_extensions/ext_prerelease.py @@ -0,0 +1 @@ +__version__ = '0.6.0rc1' diff --git a/tests/roots/test_needs_extensions/ext_unknown.py b/tests/roots/test_needs_extensions/ext_unknown.py new file mode 100644 index 00000000000..9fce20fd64f --- /dev/null +++ b/tests/roots/test_needs_extensions/ext_unknown.py @@ -0,0 +1 @@ +# no __version__ attribute to emulate missing metadata diff --git a/tests/test_extension_needs.py b/tests/test_extension_needs.py new file mode 100644 index 00000000000..6ed4d7e9f54 --- /dev/null +++ b/tests/test_extension_needs.py @@ -0,0 +1,73 @@ +from importlib import import_module +from types import SimpleNamespace + +import pytest + +from sphinx.errors import VersionRequirementError +from sphinx.extension import Extension, verify_needs_extensions + + +EXTENSION_NAME = 'dummy.ext' +EXTENSION_ROOT = 'tests.roots.test_needs_extensions' + + +def make_extension(module_basename: str) -> Extension: + module = import_module(f'{EXTENSION_ROOT}.{module_basename}') + kwargs = {} + if hasattr(module, '__version__'): + kwargs['version'] = module.__version__ + return Extension(EXTENSION_NAME, module, **kwargs) + + +def make_app_with_extension(module_basename: str) -> SimpleNamespace: + extension = make_extension(module_basename) + return SimpleNamespace(extensions={EXTENSION_NAME: extension}) + + +def make_config(requirement: str) -> SimpleNamespace: + return SimpleNamespace(needs_extensions={EXTENSION_NAME: requirement}) + + +def test_needs_extensions_accepts_newer_version() -> None: + app = make_app_with_extension('ext_newer') + config = make_config('0.6.0') + + verify_needs_extensions(app, config) + + +def test_needs_extensions_rejects_prerelease_when_final_required() -> None: + app = make_app_with_extension('ext_prerelease') + config = make_config('0.6.0') + + with pytest.raises(VersionRequirementError) as exc: + verify_needs_extensions(app, config) + + assert 'needs the extension dummy.ext at least in version 0.6.0' in str(exc.value) + + +def test_needs_extensions_rejects_unknown_version() -> None: + app = make_app_with_extension('ext_unknown') + config = make_config('0.6.0') + + with pytest.raises(VersionRequirementError) as exc: + verify_needs_extensions(app, config) + + assert 'needs the extension dummy.ext at least in version 0.6.0' in str(exc.value) + + +def test_needs_extensions_accepts_exact_match() -> None: + app = make_app_with_extension('ext_exact') + config = make_config('0.6.0') + + verify_needs_extensions(app, config) + + +def test_needs_extensions_handles_dev_and_post_releases() -> None: + config = make_config('0.6.0') + + app_with_dev = make_app_with_extension('ext_dev') + with pytest.raises(VersionRequirementError): + verify_needs_extensions(app_with_dev, config) + + app_with_post = make_app_with_extension('ext_post') + verify_needs_extensions(app_with_post, config)