From ae62ca527c45372a3a3375a93e36aef800b796f0 Mon Sep 17 00:00:00 2001 From: Adam Turner <9087854+aa-turner@users.noreply.github.com> Date: Sat, 7 Jun 2025 18:05:01 +0100 Subject: [PATCH 1/2] Avoid self.app in builder --- sphinx/application.py | 1 + sphinx/builders/__init__.py | 3 ++- sphinx/builders/_epub_base.py | 2 +- sphinx/builders/changes.py | 7 ++++++- sphinx/builders/gettext.py | 14 +++++++++----- sphinx/builders/html/__init__.py | 15 ++++++++++----- sphinx/builders/latex/__init__.py | 4 ++-- sphinx/builders/latex/theming.py | 9 ++++----- sphinx/builders/linkcheck.py | 6 +++--- sphinx/builders/texinfo.py | 2 +- sphinx/ext/coverage.py | 8 ++++---- sphinx/ext/doctest.py | 4 ++-- sphinx/theming.py | 20 +++++++++++++++----- sphinx/transforms/__init__.py | 2 +- 14 files changed, 61 insertions(+), 36 deletions(-) diff --git a/sphinx/application.py b/sphinx/application.py index d5192eef0b6..25c2b193b0c 100644 --- a/sphinx/application.py +++ b/sphinx/application.py @@ -264,6 +264,7 @@ def __init__( else: self.confdir = _StrPath(confdir).resolve() self.config = Config.read(self.confdir, confoverrides or {}, self.tags) + self.config.verbosity = -1 if self.quiet else self.verbosity # set up translation infrastructure self._init_i18n() diff --git a/sphinx/builders/__init__.py b/sphinx/builders/__init__.py index 70602273747..4e116732e7a 100644 --- a/sphinx/builders/__init__.py +++ b/sphinx/builders/__init__.py @@ -139,8 +139,9 @@ def __init__(self, app: Sphinx, env: BuildEnvironment) -> None: @property def app(self) -> Sphinx: + cls_module = self.__class__.__module__ cls_name = self.__class__.__qualname__ - _deprecation_warning(__name__, f'{cls_name}.app', remove=(10, 0)) + _deprecation_warning(cls_module, f'{cls_name}.app', remove=(10, 0)) return self._app @property diff --git a/sphinx/builders/_epub_base.py b/sphinx/builders/_epub_base.py index 1bd4846bf02..3c7c93dfd1f 100644 --- a/sphinx/builders/_epub_base.py +++ b/sphinx/builders/_epub_base.py @@ -425,7 +425,7 @@ def copy_image_files_pil(self) -> None: __('copying images... '), 'brown', len(self.images), - self.app.verbosity, + self.config.verbosity, ): dest = self.images[src] try: diff --git a/sphinx/builders/changes.py b/sphinx/builders/changes.py index aa926e0809c..059a7d1b055 100644 --- a/sphinx/builders/changes.py +++ b/sphinx/builders/changes.py @@ -30,7 +30,12 @@ class ChangesBuilder(Builder): def init(self) -> None: self.create_template_bridge() - theme_factory = HTMLThemeFactory(self.app) + theme_factory = HTMLThemeFactory( + confdir=self.confdir, + app=self._app, + config=self.config, + registry=self.env._registry, + ) self.theme = theme_factory.create('default') self.templates.init(self, self.theme) diff --git a/sphinx/builders/gettext.py b/sphinx/builders/gettext.py index f5f26ffcc88..659bf218983 100644 --- a/sphinx/builders/gettext.py +++ b/sphinx/builders/gettext.py @@ -165,7 +165,7 @@ class I18nBuilder(Builder): def init(self) -> None: super().init() self.env.set_versioning_method(self.versioning_method, self.config.gettext_uuid) - self.tags = self.app.tags = I18nTags() + self.tags = self._app.tags = I18nTags() self.catalogs: defaultdict[str, Catalog] = defaultdict(Catalog) def get_target_uri(self, docname: str, typ: str | None = None) -> str: @@ -251,7 +251,7 @@ def init(self) -> None: def _collect_templates(self) -> set[str]: template_files = set() for template_path in self.config.templates_path: - tmpl_abs_path = self.app.srcdir / template_path + tmpl_abs_path = self.srcdir / template_path for dirpath, _dirs, files in walk(tmpl_abs_path): for fn in files: if fn.endswith('.html'): @@ -268,7 +268,11 @@ def _extract_from_template(self) -> None: extract_translations = self.templates.environment.extract_translations for template in status_iterator( - files, __('reading templates... '), 'purple', len(files), self.app.verbosity + files, + __('reading templates... '), + 'purple', + len(files), + self.config.verbosity, ): try: with codecs.open(template, encoding='utf-8') as f: @@ -307,7 +311,7 @@ def finish(self) -> None: __('writing message catalogs... '), 'darkgreen', len(self.catalogs), - self.app.verbosity, + self.config.verbosity, operator.itemgetter(0), ): # noop if config.gettext_compact is set @@ -315,7 +319,7 @@ def finish(self) -> None: context['messages'] = list(catalog) template_path = [ - self.app.srcdir / rel_path for rel_path in self.config.templates_path + self.srcdir / rel_path for rel_path in self.config.templates_path ] renderer = GettextRenderer(template_path, outdir=self.outdir) content = renderer.render('message.pot.jinja', context) diff --git a/sphinx/builders/html/__init__.py b/sphinx/builders/html/__init__.py index a5f725e2922..11039099e3f 100644 --- a/sphinx/builders/html/__init__.py +++ b/sphinx/builders/html/__init__.py @@ -228,7 +228,12 @@ def get_theme_config(self) -> tuple[str, dict[str, str | int | bool]]: return self.config.html_theme, self.config.html_theme_options def init_templates(self) -> None: - theme_factory = HTMLThemeFactory(self.app) + theme_factory = HTMLThemeFactory( + confdir=self.confdir, + app=self._app, + config=self.config, + registry=self.env._registry, + ) theme_name, theme_options = self.get_theme_config() self.theme = theme_factory.create(theme_name) self.theme_options = theme_options @@ -255,7 +260,7 @@ def init_highlighter(self) -> None: self.dark_highlighter: PygmentsBridge | None if dark_style is not None: self.dark_highlighter = PygmentsBridge('html', dark_style) - self.app.add_css_file( + self.add_css_file( 'pygments_dark.css', media='(prefers-color-scheme: dark)', id='pygments_dark_css', @@ -780,7 +785,7 @@ def copy_image_files(self) -> None: __('copying images... '), 'brown', len(self.images), - self.app.verbosity, + self.config.verbosity, stringify_func=stringify_func, ): dest = self.images[src] @@ -807,7 +812,7 @@ def to_relpath(f: str) -> str: __('copying downloadable files... '), 'brown', len(self.env.dlfiles), - self.app.verbosity, + self.config.verbosity, stringify_func=to_relpath, ): try: @@ -1128,7 +1133,7 @@ def hasdoc(name: str) -> bool: # 'blah.html' should have content_root = './' not ''. ctx['content_root'] = (f'..{SEP}' * default_baseuri.count(SEP)) or f'.{SEP}' - outdir = self.app.outdir + outdir = self.outdir def css_tag(css: _CascadingStyleSheet) -> str: attrs = [ diff --git a/sphinx/builders/latex/__init__.py b/sphinx/builders/latex/__init__.py index 985620f2023..d5e4a779aa1 100644 --- a/sphinx/builders/latex/__init__.py +++ b/sphinx/builders/latex/__init__.py @@ -132,7 +132,7 @@ def init(self) -> None: self.context: dict[str, Any] = {} self.docnames: Iterable[str] = {} self.document_data: list[tuple[str, str, str, str, str, bool]] = [] - self.themes = ThemeFactory(self.app) + self.themes = ThemeFactory(srcdir=self.srcdir, config=self.config) texescape.init() self.init_context() @@ -481,7 +481,7 @@ def copy_image_files(self) -> None: __('copying images... '), 'brown', len(self.images), - self.app.verbosity, + self.config.verbosity, stringify_func=stringify_func, ): dest = self.images[src] diff --git a/sphinx/builders/latex/theming.py b/sphinx/builders/latex/theming.py index f55c077c9ca..df8eb48ec4f 100644 --- a/sphinx/builders/latex/theming.py +++ b/sphinx/builders/latex/theming.py @@ -12,7 +12,6 @@ if TYPE_CHECKING: from pathlib import Path - from sphinx.application import Sphinx from sphinx.config import Config logger = logging.getLogger(__name__) @@ -102,11 +101,11 @@ def __init__(self, name: str, filename: Path) -> None: class ThemeFactory: """A factory class for LaTeX Themes.""" - def __init__(self, app: Sphinx) -> None: + def __init__(self, *, srcdir: Path, config: Config) -> None: self.themes: dict[str, Theme] = {} - self.theme_paths = [app.srcdir / p for p in app.config.latex_theme_path] - self.config = app.config - self.load_builtin_themes(app.config) + self.theme_paths = [srcdir / p for p in config.latex_theme_path] + self.config = config + self.load_builtin_themes(config) def load_builtin_themes(self, config: Config) -> None: """Load built-in themes.""" diff --git a/sphinx/builders/linkcheck.py b/sphinx/builders/linkcheck.py index de102873036..c1b199c5493 100644 --- a/sphinx/builders/linkcheck.py +++ b/sphinx/builders/linkcheck.py @@ -98,7 +98,7 @@ def finish(self) -> None: self.process_result(result) if self.broken_hyperlinks or self.timed_out_hyperlinks: - self.app.statuscode = 1 + self._app.statuscode = 1 def process_result(self, result: CheckResult) -> None: filename = self.env.doc2path(result.docname, False) @@ -130,7 +130,7 @@ def process_result(self, result: CheckResult) -> None: case _Status.WORKING: logger.info(darkgreen('ok ') + f'{res_uri}{result.message}') # NoQA: G003 case _Status.TIMEOUT: - if self.app.quiet: + if self.config.verbosity < 0: msg = 'timeout ' + f'{res_uri}{result.message}' logger.warning(msg, location=(result.docname, result.lineno)) else: @@ -145,7 +145,7 @@ def process_result(self, result: CheckResult) -> None: ) self.timed_out_hyperlinks += 1 case _Status.BROKEN: - if self.app.quiet: + if self.config.verbosity < 0: logger.warning( __('broken link: %s (%s)'), res_uri, diff --git a/sphinx/builders/texinfo.py b/sphinx/builders/texinfo.py index 79afafab84d..6611be05465 100644 --- a/sphinx/builders/texinfo.py +++ b/sphinx/builders/texinfo.py @@ -198,7 +198,7 @@ def copy_image_files(self, targetname: str) -> None: __('copying images... '), 'brown', len(self.images), - self.app.verbosity, + self.config.verbosity, stringify_func=stringify_func, ): dest = self.images[src] diff --git a/sphinx/ext/coverage.py b/sphinx/ext/coverage.py index b2d08603f38..5c5a8d51ab3 100644 --- a/sphinx/ext/coverage.py +++ b/sphinx/ext/coverage.py @@ -255,7 +255,7 @@ def write_c_coverage(self) -> None: for typ, name in sorted(undoc): op.write(f' * {name:<50} [{typ:>9}]\n') if self.config.coverage_show_missing_items: - if self.app.quiet: + if self.config.verbosity < 0: logger.warning( __('undocumented c api: %s [%s] in file %s'), name, @@ -446,7 +446,7 @@ def write_py_coverage(self) -> None: op.write('Functions:\n') op.writelines(f' * {x}\n' for x in undoc['funcs']) if self.config.coverage_show_missing_items: - if self.app.quiet: + if self.config.verbosity < 0: for func in undoc['funcs']: logger.warning( __('undocumented python function: %s :: %s'), @@ -468,7 +468,7 @@ def write_py_coverage(self) -> None: if not methods: op.write(f' * {class_name}\n') if self.config.coverage_show_missing_items: - if self.app.quiet: + if self.config.verbosity < 0: logger.warning( __('undocumented python class: %s :: %s'), name, @@ -485,7 +485,7 @@ def write_py_coverage(self) -> None: op.write(f' * {class_name} -- missing methods:\n\n') op.writelines(f' - {x}\n' for x in methods) if self.config.coverage_show_missing_items: - if self.app.quiet: + if self.config.verbosity < 0: for meth in methods: logger.warning( __( diff --git a/sphinx/ext/doctest.py b/sphinx/ext/doctest.py index 9610e24d58d..da40a63e781 100644 --- a/sphinx/ext/doctest.py +++ b/sphinx/ext/doctest.py @@ -341,7 +341,7 @@ def _out(self, text: str) -> None: self.outfile.write(text) def _warn_out(self, text: str) -> None: - if self.app.quiet: + if self.config.verbosity < 0: logger.warning(text) else: logger.info(text, nonl=True) @@ -360,7 +360,7 @@ def s(v: int) -> str: header = 'Doctest summary' if self.total_failures or self.setup_failures or self.cleanup_failures: - self.app.statuscode = 1 + self._app.statuscode = 1 if self.config.doctest_fail_fast: header = f'{header} (exiting after first failed test)' underline = '=' * len(header) diff --git a/sphinx/theming.py b/sphinx/theming.py index a27dbfe0973..9e06faaeffc 100644 --- a/sphinx/theming.py +++ b/sphinx/theming.py @@ -28,6 +28,8 @@ from typing import Any, Required, TypedDict from sphinx.application import Sphinx + from sphinx.config import Config + from sphinx.registry import SphinxComponentRegistry class _ThemeToml(TypedDict, total=False): theme: Required[_ThemeTomlTheme] @@ -148,13 +150,21 @@ def _cleanup(self) -> None: class HTMLThemeFactory: """A factory class for HTML Themes.""" - def __init__(self, app: Sphinx) -> None: + def __init__( + self, + *, + confdir: Path, + app: Sphinx, + config: Config, + registry: SphinxComponentRegistry, + ) -> None: self._app = app - self._themes = app.registry.html_themes + self._confdir = confdir + self._themes = registry.html_themes self._entry_point_themes: dict[str, Callable[[], None]] = {} self._load_builtin_themes() - if getattr(app.config, 'html_theme_path', None): - self._load_additional_themes(app.config.html_theme_path) + if html_theme_path := getattr(config, 'html_theme_path', None): + self._load_additional_themes(html_theme_path) self._load_entry_point_themes() def _load_builtin_themes(self) -> None: @@ -166,7 +176,7 @@ def _load_builtin_themes(self) -> None: def _load_additional_themes(self, theme_paths: list[str]) -> None: """Load additional themes placed at specified directories.""" for theme_path in theme_paths: - abs_theme_path = (self._app.confdir / theme_path).resolve() + abs_theme_path = (self._confdir / theme_path).resolve() themes = self._find_themes(abs_theme_path) for name, theme in themes.items(): self._themes[name] = _StrPath(theme) diff --git a/sphinx/transforms/__init__.py b/sphinx/transforms/__init__.py index 6857e05fe58..7ba50aaa240 100644 --- a/sphinx/transforms/__init__.py +++ b/sphinx/transforms/__init__.py @@ -66,7 +66,7 @@ def app(self) -> Sphinx: cls_module = self.__class__.__module__ cls_name = self.__class__.__qualname__ _deprecation_warning(cls_module, f'{cls_name}.app', remove=(10, 0)) - return self.env.app + return self.env._app @property def env(self) -> BuildEnvironment: From 0e579cd9738c2624e7bdc984ff878b52c363c42d Mon Sep 17 00:00:00 2001 From: Adam Turner <9087854+aa-turner@users.noreply.github.com> Date: Sat, 7 Jun 2025 18:42:01 +0100 Subject: [PATCH 2/2] fixup! Avoid self.app in builder --- sphinx/application.py | 2 +- sphinx/builders/html/__init__.py | 12 +++++++----- sphinx/config.py | 6 ++++++ tests/test_extensions/test_ext_coverage.py | 2 +- tests/test_theming/test_theming.py | 8 ++++---- 5 files changed, 19 insertions(+), 11 deletions(-) diff --git a/sphinx/application.py b/sphinx/application.py index 25c2b193b0c..3874a6afa52 100644 --- a/sphinx/application.py +++ b/sphinx/application.py @@ -264,7 +264,7 @@ def __init__( else: self.confdir = _StrPath(confdir).resolve() self.config = Config.read(self.confdir, confoverrides or {}, self.tags) - self.config.verbosity = -1 if self.quiet else self.verbosity + self.config._verbosity = -1 if self.quiet else self.verbosity # set up translation infrastructure self._init_i18n() diff --git a/sphinx/builders/html/__init__.py b/sphinx/builders/html/__init__.py index 11039099e3f..1195d08beb6 100644 --- a/sphinx/builders/html/__init__.py +++ b/sphinx/builders/html/__init__.py @@ -260,11 +260,6 @@ def init_highlighter(self) -> None: self.dark_highlighter: PygmentsBridge | None if dark_style is not None: self.dark_highlighter = PygmentsBridge('html', dark_style) - self.add_css_file( - 'pygments_dark.css', - media='(prefers-color-scheme: dark)', - id='pygments_dark_css', - ) else: self.dark_highlighter = None @@ -278,6 +273,13 @@ def css_files(self) -> list[_CascadingStyleSheet]: def init_css_files(self) -> None: self._css_files = [] self.add_css_file('pygments.css', priority=200) + if self.dark_highlighter is not None: + self.add_css_file( + 'pygments_dark.css', + priority=200, + media='(prefers-color-scheme: dark)', + id='pygments_dark_css', + ) for filename in self._get_style_filenames(): self.add_css_file(filename, priority=200) diff --git a/sphinx/config.py b/sphinx/config.py index 2498ada6c56..3e16c151ebd 100644 --- a/sphinx/config.py +++ b/sphinx/config.py @@ -333,6 +333,8 @@ def __init__( raw_config['extensions'] = extensions self.extensions: list[str] = raw_config.get('extensions', []) + self._verbosity: int = 0 # updated in Sphinx.__init__() + @property def values(self) -> dict[str, _Opt]: return self._options @@ -341,6 +343,10 @@ def values(self) -> dict[str, _Opt]: def overrides(self) -> dict[str, Any]: return self._overrides + @property + def verbosity(self) -> int: + return self._verbosity + @classmethod def read( cls: type[Config], diff --git a/tests/test_extensions/test_ext_coverage.py b/tests/test_extensions/test_ext_coverage.py index 7422cd3560f..32fc2dba2d7 100644 --- a/tests/test_extensions/test_ext_coverage.py +++ b/tests/test_extensions/test_ext_coverage.py @@ -117,7 +117,7 @@ def test_show_missing_items(app: SphinxTestApp) -> None: 'coverage', testroot='root', confoverrides={'coverage_show_missing_items': True} ) def test_show_missing_items_quiet(app: SphinxTestApp) -> None: - app.quiet = True + app.config._verbosity = -1 # mimics status=None / app.quiet = True app.build(force_all=True) assert ( diff --git a/tests/test_theming/test_theming.py b/tests/test_theming/test_theming.py index 173e0c9c64b..8ff3919c967 100644 --- a/tests/test_theming/test_theming.py +++ b/tests/test_theming/test_theming.py @@ -159,10 +159,10 @@ def test_dark_style(app, monkeypatch): app.build() assert (app.outdir / '_static' / 'pygments_dark.css').exists() - css_file, properties = app.registry.css_files[0] - assert css_file == 'pygments_dark.css' - assert 'media' in properties - assert properties['media'] == '(prefers-color-scheme: dark)' + css_file = app.builder._css_files[1] + assert css_file.filename == '_static/pygments_dark.css' + assert 'media' in css_file.attributes + assert css_file.attributes['media'] == '(prefers-color-scheme: dark)' assert sorted(f.filename for f in app.builder._css_files) == [ '_static/classic.css',