Blob Blame History Raw
From b74128966fe4edf77a0c3a7936f6a6216833c9ed Mon Sep 17 00:00:00 2001
From: Karolina Surma <ksurma@redhat.com>
Date: Thu, 25 Apr 2024 15:58:03 +0200
Subject: [PATCH] Make the first party extensions optional, add [extensions]
 extra


Co-authored-by: Miro HronĨok <miro@hroncok.cz>
---
 pyproject.toml                               | 33 ++++++++++++++++----
 sphinx/application.py                        |  6 ++--
 sphinx/registry.py                           |  9 ++++--
 sphinx/testing/fixtures.py                   |  7 +++++
 tests/test_builders/test_build_html_maths.py |  3 ++
 tests/test_writers/test_api_translator.py    |  2 ++
 6 files changed, 48 insertions(+), 12 deletions(-)

diff --git a/pyproject.toml b/pyproject.toml
index 8aa49aa..10fa20e 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -56,12 +56,6 @@ classifiers = [
     "Topic :: Utilities",
 ]
 dependencies = [
-    "sphinxcontrib-applehelp",
-    "sphinxcontrib-devhelp",
-    "sphinxcontrib-jsmath",
-    "sphinxcontrib-htmlhelp>=2.0.0",
-    "sphinxcontrib-serializinghtml>=1.1.9",
-    "sphinxcontrib-qthelp",
     "Jinja2>=3.0",
     "Pygments>=2.14",
     "docutils>=0.18.1,<0.22",
@@ -78,8 +72,35 @@ dependencies = [
 dynamic = ["version"]
 
 [project.optional-dependencies]
+applehelp = [
+    "sphinxcontrib-applehelp",
+]
+devhelp = [
+    "sphinxcontrib-devhelp",
+]
+jsmath = [
+    "sphinxcontrib-jsmath",
+]
+htmlhelp = [
+    "sphinxcontrib-htmlhelp>=2.0.0",
+]
+serializinghtml = [
+    "sphinxcontrib-serializinghtml>=1.1.9",
+]
+qthelp = [
+    "sphinxcontrib-qthelp",
+]
+extensions = [
+    "sphinx[applehelp]",
+    "sphinx[devhelp]",
+    "sphinx[jsmath]",
+    "sphinx[htmlhelp]",
+    "sphinx[serializinghtml]",
+    "sphinx[qthelp]",
+]
 docs = [
     "sphinxcontrib-websupport",
+    "sphinx[extensions]",
 ]
 lint = [
     "flake8>=3.5.0",
diff --git a/sphinx/application.py b/sphinx/application.py
index 7d16d9a..2a71074 100644
--- a/sphinx/application.py
+++ b/sphinx/application.py
@@ -222,7 +222,7 @@ class Sphinx:
         # load all built-in extension modules, first-party extension modules,
         # and first-party themes
         for extension in builtin_extensions:
-            self.setup_extension(extension)
+            self.setup_extension(extension, skip_nonimportable=extension in _first_party_extensions)
 
         # load all user-given extension modules
         for extension in self.config.extensions:
@@ -391,7 +391,7 @@ class Sphinx:
 
     # ---- general extensibility interface -------------------------------------
 
-    def setup_extension(self, extname: str) -> None:
+    def setup_extension(self, extname: str, skip_nonimportable: bool = False) -> None:
         """Import and setup a Sphinx extension module.
 
         Load the extension given by the module *name*.  Use this if your
@@ -399,7 +399,7 @@ class Sphinx:
         called twice.
         """
         logger.debug('[app] setting up extension: %r', extname)
-        self.registry.load_extension(self, extname)
+        self.registry.load_extension(self, extname, skip_nonimportable=skip_nonimportable)
 
     @staticmethod
     def require_sphinx(version: tuple[int, int] | str) -> None:
diff --git a/sphinx/registry.py b/sphinx/registry.py
index 7887858..ca95960 100644
--- a/sphinx/registry.py
+++ b/sphinx/registry.py
@@ -436,7 +436,7 @@ class SphinxComponentRegistry:
     def add_html_theme(self, name: str, theme_path: str) -> None:
         self.html_themes[name] = theme_path
 
-    def load_extension(self, app: Sphinx, extname: str) -> None:
+    def load_extension(self, app: Sphinx, extname: str, skip_nonimportable: bool = False) -> None:
         """Load a Sphinx extension."""
         if extname in app.extensions:  # already loaded
             return
@@ -452,9 +452,12 @@ class SphinxComponentRegistry:
             try:
                 mod = import_module(extname)
             except ImportError as err:
+                msg = __('Could not import extension %s')
+                if skip_nonimportable:
+                    logger.debug(msg % extname)
+                    return
                 logger.verbose(__('Original exception:\n') + traceback.format_exc())
-                raise ExtensionError(__('Could not import extension %s') % extname,
-                                     err) from err
+                raise ExtensionError(msg % extname, err) from err
 
             setup: _ExtensionSetupFunc | None = getattr(mod, 'setup', None)
             if setup is None:
diff --git a/sphinx/testing/fixtures.py b/sphinx/testing/fixtures.py
index 6e1a122..f3fe743 100644
--- a/sphinx/testing/fixtures.py
+++ b/sphinx/testing/fixtures.py
@@ -31,6 +31,7 @@ DEFAULT_ENABLED_MARKERS = [
         'keep_going=False, builddir=None, docutils_conf=None'
         '): arguments to initialize the sphinx test application.'
     ),
+    'sphinxcontrib(...): required sphinxcontrib.* extensions',
     'test_params(shared_result=...): test parameters.',
 ]
 
@@ -80,6 +81,12 @@ def app_params(
     Parameters that are specified by 'pytest.mark.sphinx' for
     sphinx.application.Sphinx initialization
     """
+
+    # ##### process pytest.mark.sphinxcontrib
+    for info in reversed(list(request.node.iter_markers("sphinxcontrib"))):
+        for arg in info.args:
+            pytest.importorskip("sphinxcontrib." + arg)
+
     # ##### process pytest.mark.sphinx
 
     pargs: dict[int, Any] = {}
diff --git a/tests/test_builders/test_build_html_maths.py b/tests/test_builders/test_build_html_maths.py
index 900846b..664c86e 100644
--- a/tests/test_builders/test_build_html_maths.py
+++ b/tests/test_builders/test_build_html_maths.py
@@ -20,6 +20,7 @@ def test_html_math_renderer_is_imgmath(app, status, warning):
     assert app.builder.math_renderer_name == 'imgmath'
 
 
+@pytest.mark.sphinxcontrib('serializinghtml', 'jsmath')
 @pytest.mark.sphinx('html', testroot='basic',
                     confoverrides={'extensions': ['sphinxcontrib.jsmath',
                                                   'sphinx.ext.imgmath']})
@@ -40,6 +41,7 @@ def test_html_math_renderer_is_duplicated2(app, status, warning):
     assert app.builder.math_renderer_name == 'imgmath'  # The another one is chosen
 
 
+@pytest.mark.sphinxcontrib('jsmath')
 @pytest.mark.sphinx('html', testroot='basic',
                     confoverrides={'extensions': ['sphinxcontrib.jsmath',
                                                   'sphinx.ext.imgmath'],
@@ -48,6 +50,7 @@ def test_html_math_renderer_is_chosen(app, status, warning):
     assert app.builder.math_renderer_name == 'imgmath'
 
 
+@pytest.mark.sphinxcontrib('jsmath')
 @pytest.mark.sphinx('html', testroot='basic',
                     confoverrides={'extensions': ['sphinxcontrib.jsmath',
                                                   'sphinx.ext.mathjax'],
diff --git a/tests/test_writers/test_api_translator.py b/tests/test_writers/test_api_translator.py
index 9f2bd44..81575b7 100644
--- a/tests/test_writers/test_api_translator.py
+++ b/tests/test_writers/test_api_translator.py
@@ -36,6 +36,7 @@ def test_singlehtml_set_translator_for_singlehtml(app, status, warning):
     assert translator_class.__name__ == 'ConfSingleHTMLTranslator'
 
 
+@pytest.mark.sphinxcontrib('serializinghtml')
 @pytest.mark.sphinx('pickle', testroot='api-set-translator')
 def test_pickle_set_translator_for_pickle(app, status, warning):
     translator_class = app.builder.get_translator_class()
@@ -43,6 +44,7 @@ def test_pickle_set_translator_for_pickle(app, status, warning):
     assert translator_class.__name__ == 'ConfPickleTranslator'
 
 
+@pytest.mark.sphinxcontrib('serializinghtml')
 @pytest.mark.sphinx('json', testroot='api-set-translator')
 def test_json_set_translator_for_json(app, status, warning):
     translator_class = app.builder.get_translator_class()
-- 
2.44.0