From b74128966fe4edf77a0c3a7936f6a6216833c9ed Mon Sep 17 00:00:00 2001 From: Karolina Surma 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 --- 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