From 79be92506b11ceec74d0d6e627b8298b3c6672e0 Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Tue, 6 Aug 2024 15:36:54 -0400 Subject: [PATCH 1/6] ENH: Use parseable buildinfo --- sphinx/builders/html/__init__.py | 70 ++++++++++++++++++++------------ 1 file changed, 44 insertions(+), 26 deletions(-) diff --git a/sphinx/builders/html/__init__.py b/sphinx/builders/html/__init__.py index 8f351b76a12..432e7d94faf 100644 --- a/sphinx/builders/html/__init__.py +++ b/sphinx/builders/html/__init__.py @@ -3,11 +3,12 @@ from __future__ import annotations import contextlib -import hashlib import html +import json import os import posixpath import re +import shutil import sys import types import warnings @@ -89,21 +90,30 @@ ] -def _stable_hash(obj: Any) -> str: - """Return a stable hash for a Python data structure. +def _stable_str(obj: Any) -> str: + """Return a stable string representation of a Python data structure. - We can't just use the md5 of str(obj) as the order of collections - may be random. + We can't just use str(obj) as the order of collections may be random. """ + return json.dumps(_json_prep(obj), separators=(',', ':')) + + +def _json_prep(obj: Any) -> dict[str, Any] | list[Any] | str: if isinstance(obj, dict): - obj = sorted(map(_stable_hash, obj.items())) + # convert to a sorted dict + obj = {_json_prep(k): _json_prep(v) for k, v in obj.items()} + obj = {k: obj[k] for k in sorted(obj, key=str)} if isinstance(obj, list | tuple | set | frozenset): - obj = sorted(map(_stable_hash, obj)) + # convert to a sorted list + obj = sorted(map(_json_prep, obj), key=str) elif isinstance(obj, type | types.FunctionType): # The default repr() of functions includes the ID, which is not ideal. # We use the fully qualified name instead. obj = f'{obj.__module__}.{obj.__qualname__}' - return hashlib.md5(str(obj).encode(), usedforsecurity=False).hexdigest() + else: + # we can't do any better, just use the string representation + obj = str(obj) + return obj def convert_locale_to_language_tag(locale: str | None) -> str | None: @@ -128,13 +138,13 @@ class BuildInfo: def load(cls: type[BuildInfo], f: IO[str]) -> BuildInfo: try: lines = f.readlines() - assert lines[0].rstrip() == '# Sphinx build info version 1' - assert lines[2].startswith('config: ') - assert lines[3].startswith('tags: ') + assert lines[0].rstrip() == '# Sphinx build info version 2', "Bad version" + assert lines[2].startswith('config: '), "File missing config entry" + assert lines[3].startswith('tags: '), "File missing tags entry" build_info = BuildInfo() - build_info.config_hash = lines[2].split()[1].strip() - build_info.tags_hash = lines[3].split()[1].strip() + build_info.config_hash = lines[2].split(maxsplit=1)[1].strip() + build_info.tags_hash = lines[3].split(maxsplit=1)[1].strip() return build_info except Exception as exc: raise ValueError(__('build info file is broken: %r') % exc) from exc @@ -150,22 +160,23 @@ def __init__( if config: values = {c.name: c.value for c in config.filter(config_categories)} - self.config_hash = _stable_hash(values) + self.config_hash = _stable_str(values) if tags: - self.tags_hash = _stable_hash(sorted(tags)) + self.tags_hash = _stable_str(sorted(tags)) def __eq__(self, other: BuildInfo) -> bool: # type: ignore[override] return (self.config_hash == other.config_hash and self.tags_hash == other.tags_hash) def dump(self, f: IO[str]) -> None: - f.write('# Sphinx build info version 1\n' - '# This file hashes the configuration used when building these files.' - ' When it is not found, a full rebuild will be done.\n' - 'config: %s\n' - 'tags: %s\n' % - (self.config_hash, self.tags_hash)) + f.write( + "# Sphinx build info version 2\n" + "# This file JSON-ifies the configuration used when building these " + "files. When it is not found, a full rebuild will be done.\n" + f"config: {self.config_hash}\n" + f"tags: {self.tags_hash}\n" + ) class StandaloneHTMLBuilder(Builder): @@ -396,7 +407,13 @@ def get_outdated_docs(self) -> Iterator[str]: buildinfo = BuildInfo.load(fp) if self.build_info != buildinfo: - logger.debug('[build target] did not match: build_info ') + logger.info( + bold(__("building [html]: ")) + + __("build_info mismatch, copying .buildinfo to .buildinfo.old") + ) + shutil.copy(build_info_fname, build_info_fname + ".old") + with open(build_info_fname, 'w', encoding="utf-8") as fp: + self.build_info.dump(fp) yield from self.env.found_docs return except ValueError as exc: @@ -426,7 +443,7 @@ def get_outdated_docs(self) -> Iterator[str]: template_mtime = 0 for docname in self.env.found_docs: if docname not in self.env.all_docs: - logger.debug('[build target] did not in env: %r', docname) + logger.debug('[build target] did not find in env: %r', docname) yield docname continue targetname = self.get_outfilename(docname) @@ -435,15 +452,16 @@ def get_outdated_docs(self) -> Iterator[str]: except Exception: targetmtime = 0 try: - srcmtime = max(_last_modified_time(self.env.doc2path(docname)), template_mtime) + docpath_mtime = _last_modified_time(self.env.doc2path(docname)) + srcmtime = max(docpath_mtime, template_mtime) if srcmtime > targetmtime: logger.debug( - '[build target] targetname %r(%s), template(%s), docname %r(%s)', + '[build target] targetname %r(%s) < max(template(%s), docname %r(%s))', targetname, _format_rfc3339_microseconds(targetmtime), _format_rfc3339_microseconds(template_mtime), docname, - _format_rfc3339_microseconds(_last_modified_time(self.env.doc2path(docname))), + _format_rfc3339_microseconds(docpath_mtime), ) yield docname except OSError: From 90bf0a7c790aaff0883287553cae16355e80eb79 Mon Sep 17 00:00:00 2001 From: Adam Turner <9087854+aa-turner@users.noreply.github.com> Date: Sun, 11 Aug 2024 21:11:53 +0100 Subject: [PATCH 2/6] style --- sphinx/builders/html/__init__.py | 97 ++++++++++++++++++-------------- sphinx/environment/__init__.py | 2 +- 2 files changed, 56 insertions(+), 43 deletions(-) diff --git a/sphinx/builders/html/__init__.py b/sphinx/builders/html/__init__.py index 851f7c5a121..d8e09c379e5 100644 --- a/sphinx/builders/html/__init__.py +++ b/sphinx/builders/html/__init__.py @@ -139,19 +139,33 @@ class BuildInfo: """ @classmethod - def load(cls: type[BuildInfo], f: IO[str]) -> BuildInfo: + def load(cls: type[BuildInfo], filename: Path, /) -> BuildInfo: try: - lines = f.readlines() - assert lines[0].rstrip() == '# Sphinx build info version 2', "Bad version" - assert lines[2].startswith('config: '), "File missing config entry" - assert lines[3].startswith('tags: '), "File missing tags entry" - - build_info = BuildInfo() - build_info.config_hash = lines[2].split(maxsplit=1)[1].strip() - build_info.tags_hash = lines[3].split(maxsplit=1)[1].strip() - return build_info - except Exception as exc: - raise ValueError(__('build info file is broken: %r') % exc) from exc + content = filename.read_text(encoding="utf-8") + except OSError as exc: + msg = __('could not read build info file: %r') % exc + raise ValueError(msg) from exc + lines = content.splitlines() + + version = lines[0].rstrip() + if version == '# Sphinx build info version 1': + logger.info(__('ignoring outdated build info file')) + return BuildInfo() + if version != '# Sphinx build info version 2': + msg = __('failed to read broken build info file (unknown version)') + raise ValueError(msg) + + if not lines[2].startswith('config: '): + msg = __('failed to read broken build info file (missing config entry)') + raise ValueError(msg) + if not lines[3].startswith('tags: '): + msg = __('failed to read broken build info file (missing tags entry)') + raise ValueError(msg) + + build_info = BuildInfo() + build_info._config_str = lines[2].removeprefix('config: ').strip() + build_info._tags_str = lines[3].removeprefix('tags: ').strip() + return build_info def __init__( self, @@ -159,28 +173,29 @@ def __init__( tags: Tags | None = None, config_categories: Set[_ConfigRebuild] = frozenset(), ) -> None: - self.config_hash = '' - self.tags_hash = '' + self._config_str = '' + self._tags_str = '' if config: values = {c.name: c.value for c in config.filter(config_categories)} - self.config_hash = _stable_str(values) + self._config_str = _stable_str(values) if tags: - self.tags_hash = _stable_str(sorted(tags)) + self._tags_str = _stable_str(sorted(tags)) def __eq__(self, other: BuildInfo) -> bool: # type: ignore[override] - return (self.config_hash == other.config_hash and - self.tags_hash == other.tags_hash) - - def dump(self, f: IO[str]) -> None: - f.write( - "# Sphinx build info version 2\n" - "# This file JSON-ifies the configuration used when building these " - "files. When it is not found, a full rebuild will be done.\n" - f"config: {self.config_hash}\n" - f"tags: {self.tags_hash}\n" + return (self._config_str == other._config_str and + self._tags_str == other._tags_str) + + def dump(self, filename: Path, /) -> None: + build_info = ( + '# Sphinx build info version 2\n' + '# This file records the configuration used when building these files. ' + 'When it is not found, a full rebuild will be done.\n' + f'config: {self._config_str}\n' + f'tags: {self._tags_str}\n' ) + filename.write_text(build_info, encoding="utf-8") class StandaloneHTMLBuilder(Builder): @@ -407,24 +422,23 @@ def math_renderer_name(self) -> str | None: def get_outdated_docs(self) -> Iterator[str]: build_info_fname = self.outdir / '.buildinfo' try: - with open(build_info_fname, encoding="utf-8") as fp: - buildinfo = BuildInfo.load(fp) - - if self.build_info != buildinfo: + build_info = BuildInfo.load(build_info_fname) + except ValueError as exc: + logger.warning(__('Failed to read build info file: %r'), exc) + else: + if self.build_info != build_info: logger.info( bold(__("building [html]: ")) + __("build_info mismatch, copying .buildinfo to .buildinfo.old") ) - shutil.copy(build_info_fname, build_info_fname + ".old") - with open(build_info_fname, 'w', encoding="utf-8") as fp: - self.build_info.dump(fp) - yield from self.env.found_docs - return - except ValueError as exc: - logger.warning(__('Failed to read build info file: %r'), exc) - except OSError: - # ignore errors on reading - pass + try: + shutil.move(build_info_fname, build_info_fname + ".old") + self.build_info.dump(build_info_fname) + except OSError: + pass # ignore errors on reading + else: + yield from self.env.found_docs + return if self.templates: template_mtime = int(self.templates.newest_template_mtime() * 10**6) @@ -961,8 +975,7 @@ def copy_extra_files(self) -> None: def write_buildinfo(self) -> None: try: - with open(path.join(self.outdir, '.buildinfo'), 'w', encoding="utf-8") as fp: - self.build_info.dump(fp) + self.build_info.dump(self.outdir / '.buildinfo') except OSError as exc: logger.warning(__('Failed to write build info file: %r'), exc) diff --git a/sphinx/environment/__init__.py b/sphinx/environment/__init__.py index 637fe185f7f..06362577cdf 100644 --- a/sphinx/environment/__init__.py +++ b/sphinx/environment/__init__.py @@ -66,7 +66,7 @@ # This is increased every time an environment attribute is added # or changed to properly invalidate pickle files. -ENV_VERSION = 63 +ENV_VERSION = 64 # config status CONFIG_UNSET = -1 From 20103116c9acb2e0465b361490411a31716c9868 Mon Sep 17 00:00:00 2001 From: Adam Turner <9087854+aa-turner@users.noreply.github.com> Date: Sun, 11 Aug 2024 22:15:53 +0100 Subject: [PATCH 3/6] Manual merge --- sphinx/builders/html/__init__.py | 97 ----------------------------- sphinx/builders/html/_build_info.py | 39 +++++++----- 2 files changed, 25 insertions(+), 111 deletions(-) diff --git a/sphinx/builders/html/__init__.py b/sphinx/builders/html/__init__.py index d953af6f248..891ff847261 100644 --- a/sphinx/builders/html/__init__.py +++ b/sphinx/builders/html/__init__.py @@ -4,13 +4,11 @@ import contextlib import html -import json import os import posixpath import re import shutil import sys -import types import warnings from os import path from pathlib import Path @@ -72,9 +70,7 @@ from docutils.readers import Reader from sphinx.application import Sphinx - from sphinx.config import _ConfigRebuild from sphinx.environment import BuildEnvironment - from sphinx.util.tags import Tags from sphinx.util.typing import ExtensionMetadata #: the filename for the inventory of objects @@ -95,32 +91,6 @@ ] -def _stable_str(obj: Any) -> str: - """Return a stable string representation of a Python data structure. - - We can't just use str(obj) as the order of collections may be random. - """ - return json.dumps(_json_prep(obj), separators=(',', ':')) - - -def _json_prep(obj: Any) -> dict[str, Any] | list[Any] | str: - if isinstance(obj, dict): - # convert to a sorted dict - obj = {_json_prep(k): _json_prep(v) for k, v in obj.items()} - obj = {k: obj[k] for k in sorted(obj, key=str)} - if isinstance(obj, list | tuple | set | frozenset): - # convert to a sorted list - obj = sorted(map(_json_prep, obj), key=str) - elif isinstance(obj, type | types.FunctionType): - # The default repr() of functions includes the ID, which is not ideal. - # We use the fully qualified name instead. - obj = f'{obj.__module__}.{obj.__qualname__}' - else: - # we can't do any better, just use the string representation - obj = str(obj) - return obj - - def convert_locale_to_language_tag(locale: str | None) -> str | None: """Convert a locale string to a language tag (ex. en_US -> en-US). @@ -132,73 +102,6 @@ def convert_locale_to_language_tag(locale: str | None) -> str | None: return None -class BuildInfo: - """buildinfo file manipulator. - - HTMLBuilder and its family are storing their own envdata to ``.buildinfo``. - This class is a manipulator for the file. - """ - - @classmethod - def load(cls: type[BuildInfo], filename: Path, /) -> BuildInfo: - try: - content = filename.read_text(encoding="utf-8") - except OSError as exc: - msg = __('could not read build info file: %r') % exc - raise ValueError(msg) from exc - lines = content.splitlines() - - version = lines[0].rstrip() - if version == '# Sphinx build info version 1': - logger.info(__('ignoring outdated build info file')) - return BuildInfo() - if version != '# Sphinx build info version 2': - msg = __('failed to read broken build info file (unknown version)') - raise ValueError(msg) - - if not lines[2].startswith('config: '): - msg = __('failed to read broken build info file (missing config entry)') - raise ValueError(msg) - if not lines[3].startswith('tags: '): - msg = __('failed to read broken build info file (missing tags entry)') - raise ValueError(msg) - - build_info = BuildInfo() - build_info._config_str = lines[2].removeprefix('config: ').strip() - build_info._tags_str = lines[3].removeprefix('tags: ').strip() - return build_info - - def __init__( - self, - config: Config | None = None, - tags: Tags | None = None, - config_categories: Set[_ConfigRebuild] = frozenset(), - ) -> None: - self._config_str = '' - self._tags_str = '' - - if config: - values = {c.name: c.value for c in config.filter(config_categories)} - self._config_str = _stable_str(values) - - if tags: - self._tags_str = _stable_str(sorted(tags)) - - def __eq__(self, other: BuildInfo) -> bool: # type: ignore[override] - return (self._config_str == other._config_str and - self._tags_str == other._tags_str) - - def dump(self, filename: Path, /) -> None: - build_info = ( - '# Sphinx build info version 2\n' - '# This file records the configuration used when building these files. ' - 'When it is not found, a full rebuild will be done.\n' - f'config: {self._config_str}\n' - f'tags: {self._tags_str}\n' - ) - filename.write_text(build_info, encoding="utf-8") - - class StandaloneHTMLBuilder(Builder): """ Builds standalone HTML docs. diff --git a/sphinx/builders/html/_build_info.py b/sphinx/builders/html/_build_info.py index 5b364c0d9fc..e853284bcd9 100644 --- a/sphinx/builders/html/_build_info.py +++ b/sphinx/builders/html/_build_info.py @@ -2,7 +2,7 @@ from __future__ import annotations -import hashlib +import json import types from typing import TYPE_CHECKING @@ -30,7 +30,9 @@ def load(cls: type[BuildInfo], filename: Path, /) -> BuildInfo: lines = content.splitlines() version = lines[0].rstrip() - if version != '# Sphinx build info version 1': + if version == '# Sphinx build info version 1': + return BuildInfo() # ignore outdated build info file + if version != '# Sphinx build info version 2': msg = __('failed to read broken build info file (unknown version)') raise ValueError(msg) @@ -57,18 +59,18 @@ def __init__( if config: values = {c.name: c.value for c in config.filter(config_categories)} - self.config_hash = _stable_hash(values) + self.config_hash = _stable_str(values) if tags: - self.tags_hash = _stable_hash(sorted(tags)) + self.tags_hash = _stable_str(sorted(tags)) def __eq__(self, other: BuildInfo) -> bool: # type: ignore[override] - return (self.config_hash == other.config_hash and - self.tags_hash == other.tags_hash) + return (self.config_hash == other.config_hash + and self.tags_hash == other.tags_hash) def dump(self, filename: Path, /) -> None: build_info = ( - '# Sphinx build info version 1\n' + '# Sphinx build info version 2\n' '# This file records the configuration used when building these files. ' 'When it is not found, a full rebuild will be done.\n' f'config: {self.config_hash}\n' @@ -77,18 +79,27 @@ def dump(self, filename: Path, /) -> None: filename.write_text(build_info, encoding="utf-8") -def _stable_hash(obj: Any) -> str: - """Return a stable hash for a Python data structure. +def _stable_str(obj: Any) -> str: + """Return a stable string representation of a Python data structure. - We can't just use the md5 of str(obj) as the order of collections - may be random. + We can't just use str(obj) as the order of collections may be random. """ + return json.dumps(_json_prep(obj), separators=(',', ':')) + + +def _json_prep(obj: Any) -> dict[str, Any] | list[Any] | str: if isinstance(obj, dict): - obj = sorted(map(_stable_hash, obj.items())) + # convert to a sorted dict + obj = {_json_prep(k): _json_prep(v) for k, v in obj.items()} + obj = {k: obj[k] for k in sorted(obj, key=str)} if isinstance(obj, list | tuple | set | frozenset): - obj = sorted(map(_stable_hash, obj)) + # convert to a sorted list + obj = sorted(map(_json_prep, obj), key=str) elif isinstance(obj, type | types.FunctionType): # The default repr() of functions includes the ID, which is not ideal. # We use the fully qualified name instead. obj = f'{obj.__module__}.{obj.__qualname__}' - return hashlib.md5(str(obj).encode(), usedforsecurity=False).hexdigest() + else: + # we can't do any better, just use the string representation + obj = str(obj) + return obj From 46df03b41e5cb56594a8df772a898b8c14cc42d4 Mon Sep 17 00:00:00 2001 From: Adam Turner <9087854+aa-turner@users.noreply.github.com> Date: Sun, 11 Aug 2024 22:21:01 +0100 Subject: [PATCH 4/6] bump env version in fixtures --- tests/js/fixtures/cpp/searchindex.js | 2 +- tests/js/fixtures/multiterm/searchindex.js | 2 +- tests/js/fixtures/partial/searchindex.js | 2 +- tests/js/fixtures/titles/searchindex.js | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/js/fixtures/cpp/searchindex.js b/tests/js/fixtures/cpp/searchindex.js index 78e5f761485..989c877a8cc 100644 --- a/tests/js/fixtures/cpp/searchindex.js +++ b/tests/js/fixtures/cpp/searchindex.js @@ -1 +1 @@ -Search.setIndex({"alltitles": {}, "docnames": ["index"], "envversion": {"sphinx": 63, "sphinx.domains.c": 3, "sphinx.domains.changeset": 1, "sphinx.domains.citation": 1, "sphinx.domains.cpp": 9, "sphinx.domains.index": 1, "sphinx.domains.javascript": 3, "sphinx.domains.math": 2, "sphinx.domains.python": 4, "sphinx.domains.rst": 2, "sphinx.domains.std": 2}, "filenames": ["index.rst"], "indexentries": {"sphinx (c++ class)": [[0, "_CPPv46Sphinx", false]]}, "objects": {"": [[0, 0, 1, "_CPPv46Sphinx", "Sphinx"]]}, "objnames": {"0": ["cpp", "class", "C++ class"]}, "objtypes": {"0": "cpp:class"}, "terms": {"The": 0, "becaus": 0, "c": 0, "can": 0, "cardin": 0, "challeng": 0, "charact": 0, "class": 0, "descript": 0, "drop": 0, "engin": 0, "fixtur": 0, "frequent": 0, "gener": 0, "i": 0, "index": 0, "inflat": 0, "mathemat": 0, "occur": 0, "often": 0, "project": 0, "punctuat": 0, "queri": 0, "relat": 0, "sampl": 0, "search": 0, "size": 0, "sphinx": 0, "term": 0, "thei": 0, "thi": 0, "token": 0, "us": 0, "web": 0, "would": 0}, "titles": ["<no title>"], "titleterms": {}}) \ No newline at end of file +Search.setIndex({"alltitles": {}, "docnames": ["index"], "envversion": {"sphinx": 64, "sphinx.domains.c": 3, "sphinx.domains.changeset": 1, "sphinx.domains.citation": 1, "sphinx.domains.cpp": 9, "sphinx.domains.index": 1, "sphinx.domains.javascript": 3, "sphinx.domains.math": 2, "sphinx.domains.python": 4, "sphinx.domains.rst": 2, "sphinx.domains.std": 2}, "filenames": ["index.rst"], "indexentries": {"sphinx (c++ class)": [[0, "_CPPv46Sphinx", false]]}, "objects": {"": [[0, 0, 1, "_CPPv46Sphinx", "Sphinx"]]}, "objnames": {"0": ["cpp", "class", "C++ class"]}, "objtypes": {"0": "cpp:class"}, "terms": {"The": 0, "becaus": 0, "c": 0, "can": 0, "cardin": 0, "challeng": 0, "charact": 0, "class": 0, "descript": 0, "drop": 0, "engin": 0, "fixtur": 0, "frequent": 0, "gener": 0, "i": 0, "index": 0, "inflat": 0, "mathemat": 0, "occur": 0, "often": 0, "project": 0, "punctuat": 0, "queri": 0, "relat": 0, "sampl": 0, "search": 0, "size": 0, "sphinx": 0, "term": 0, "thei": 0, "thi": 0, "token": 0, "us": 0, "web": 0, "would": 0}, "titles": ["<no title>"], "titleterms": {}}) \ No newline at end of file diff --git a/tests/js/fixtures/multiterm/searchindex.js b/tests/js/fixtures/multiterm/searchindex.js index 96b093c5fda..c4e4a32e30a 100644 --- a/tests/js/fixtures/multiterm/searchindex.js +++ b/tests/js/fixtures/multiterm/searchindex.js @@ -1 +1 @@ -Search.setIndex({"alltitles": {"Main Page": [[0, null]]}, "docnames": ["index"], "envversion": {"sphinx": 63, "sphinx.domains.c": 3, "sphinx.domains.changeset": 1, "sphinx.domains.citation": 1, "sphinx.domains.cpp": 9, "sphinx.domains.index": 1, "sphinx.domains.javascript": 3, "sphinx.domains.math": 2, "sphinx.domains.python": 4, "sphinx.domains.rst": 2, "sphinx.domains.std": 2}, "filenames": ["index.rst"], "indexentries": {}, "objects": {}, "objnames": {}, "objtypes": {}, "terms": {"At": 0, "adjac": 0, "all": 0, "an": 0, "appear": 0, "applic": 0, "ar": 0, "built": 0, "can": 0, "check": 0, "contain": 0, "do": 0, "document": 0, "doesn": 0, "each": 0, "fixtur": 0, "format": 0, "function": 0, "futur": 0, "html": 0, "i": 0, "includ": 0, "match": 0, "messag": 0, "multipl": 0, "multiterm": 0, "order": 0, "other": 0, "output": 0, "perform": 0, "perhap": 0, "phrase": 0, "project": 0, "queri": 0, "requir": 0, "same": 0, "search": 0, "successfulli": 0, "support": 0, "t": 0, "term": 0, "test": 0, "thi": 0, "time": 0, "us": 0, "when": 0, "write": 0}, "titles": ["Main Page"], "titleterms": {"main": 0, "page": 0}}) \ No newline at end of file +Search.setIndex({"alltitles": {"Main Page": [[0, null]]}, "docnames": ["index"], "envversion": {"sphinx": 64, "sphinx.domains.c": 3, "sphinx.domains.changeset": 1, "sphinx.domains.citation": 1, "sphinx.domains.cpp": 9, "sphinx.domains.index": 1, "sphinx.domains.javascript": 3, "sphinx.domains.math": 2, "sphinx.domains.python": 4, "sphinx.domains.rst": 2, "sphinx.domains.std": 2}, "filenames": ["index.rst"], "indexentries": {}, "objects": {}, "objnames": {}, "objtypes": {}, "terms": {"At": 0, "adjac": 0, "all": 0, "an": 0, "appear": 0, "applic": 0, "ar": 0, "built": 0, "can": 0, "check": 0, "contain": 0, "do": 0, "document": 0, "doesn": 0, "each": 0, "fixtur": 0, "format": 0, "function": 0, "futur": 0, "html": 0, "i": 0, "includ": 0, "match": 0, "messag": 0, "multipl": 0, "multiterm": 0, "order": 0, "other": 0, "output": 0, "perform": 0, "perhap": 0, "phrase": 0, "project": 0, "queri": 0, "requir": 0, "same": 0, "search": 0, "successfulli": 0, "support": 0, "t": 0, "term": 0, "test": 0, "thi": 0, "time": 0, "us": 0, "when": 0, "write": 0}, "titles": ["Main Page"], "titleterms": {"main": 0, "page": 0}}) \ No newline at end of file diff --git a/tests/js/fixtures/partial/searchindex.js b/tests/js/fixtures/partial/searchindex.js index b650c366e94..4925083169b 100644 --- a/tests/js/fixtures/partial/searchindex.js +++ b/tests/js/fixtures/partial/searchindex.js @@ -1 +1 @@ -Search.setIndex({"alltitles": {"sphinx_utils module": [[0, null]]}, "docnames": ["index"], "envversion": {"sphinx": 63, "sphinx.domains.c": 3, "sphinx.domains.changeset": 1, "sphinx.domains.citation": 1, "sphinx.domains.cpp": 9, "sphinx.domains.index": 1, "sphinx.domains.javascript": 3, "sphinx.domains.math": 2, "sphinx.domains.python": 4, "sphinx.domains.rst": 2, "sphinx.domains.std": 2}, "filenames": ["index.rst"], "indexentries": {}, "objects": {}, "objnames": {}, "objtypes": {}, "terms": {"ar": 0, "both": 0, "built": 0, "confirm": 0, "document": 0, "function": 0, "html": 0, "i": 0, "includ": 0, "input": 0, "javascript": 0, "match": 0, "partial": 0, "possibl": 0, "project": 0, "provid": 0, "restructuredtext": 0, "sampl": 0, "search": 0, "should": 0, "term": 0, "thi": 0, "titl": 0, "us": 0, "when": 0}, "titles": ["sphinx_utils module"], "titleterms": {"modul": 0, "sphinx_util": 0}}) \ No newline at end of file +Search.setIndex({"alltitles": {"sphinx_utils module": [[0, null]]}, "docnames": ["index"], "envversion": {"sphinx": 64, "sphinx.domains.c": 3, "sphinx.domains.changeset": 1, "sphinx.domains.citation": 1, "sphinx.domains.cpp": 9, "sphinx.domains.index": 1, "sphinx.domains.javascript": 3, "sphinx.domains.math": 2, "sphinx.domains.python": 4, "sphinx.domains.rst": 2, "sphinx.domains.std": 2}, "filenames": ["index.rst"], "indexentries": {}, "objects": {}, "objnames": {}, "objtypes": {}, "terms": {"ar": 0, "both": 0, "built": 0, "confirm": 0, "document": 0, "function": 0, "html": 0, "i": 0, "includ": 0, "input": 0, "javascript": 0, "match": 0, "partial": 0, "possibl": 0, "project": 0, "provid": 0, "restructuredtext": 0, "sampl": 0, "search": 0, "should": 0, "term": 0, "thi": 0, "titl": 0, "us": 0, "when": 0}, "titles": ["sphinx_utils module"], "titleterms": {"modul": 0, "sphinx_util": 0}}) \ No newline at end of file diff --git a/tests/js/fixtures/titles/searchindex.js b/tests/js/fixtures/titles/searchindex.js index a084952248a..293b087149b 100644 --- a/tests/js/fixtures/titles/searchindex.js +++ b/tests/js/fixtures/titles/searchindex.js @@ -1 +1 @@ -Search.setIndex({"alltitles": {"Main Page": [[0, null]], "Relevance": [[0, "relevance"], [1, null]], "Result Scoring": [[0, "result-scoring"]]}, "docnames": ["index", "relevance"], "envversion": {"sphinx": 63, "sphinx.domains.c": 3, "sphinx.domains.changeset": 1, "sphinx.domains.citation": 1, "sphinx.domains.cpp": 9, "sphinx.domains.index": 1, "sphinx.domains.javascript": 3, "sphinx.domains.math": 2, "sphinx.domains.python": 4, "sphinx.domains.rst": 2, "sphinx.domains.std": 2}, "filenames": ["index.rst", "relevance.rst"], "indexentries": {"example (class in relevance)": [[0, "relevance.Example", false]], "module": [[0, "module-relevance", false]], "relevance": [[0, "index-1", false], [0, "module-relevance", false]], "relevance (relevance.example attribute)": [[0, "relevance.Example.relevance", false]], "scoring": [[0, "index-0", true]]}, "objects": {"": [[0, 0, 0, "-", "relevance"]], "relevance": [[0, 1, 1, "", "Example"]], "relevance.Example": [[0, 2, 1, "", "relevance"]]}, "objnames": {"0": ["py", "module", "Python module"], "1": ["py", "class", "Python class"], "2": ["py", "attribute", "Python attribute"]}, "objtypes": {"0": "py:module", "1": "py:class", "2": "py:attribute"}, "terms": {"": [0, 1], "A": 1, "By": 0, "For": [0, 1], "In": [0, 1], "against": 0, "align": 0, "also": 1, "an": 0, "answer": 0, "appear": 1, "ar": 1, "area": 0, "ask": 0, "assign": 0, "attempt": 0, "attribut": 0, "both": 0, "built": 1, "can": [0, 1], "class": 0, "code": [0, 1], "collect": 0, "consid": 1, "contain": 0, "context": 0, "corpu": 1, "could": 1, "demonstr": 0, "describ": 1, "detail": 1, "determin": [0, 1], "docstr": 0, "document": [0, 1], "domain": 1, "dure": 0, "engin": 0, "evalu": 0, "exampl": [0, 1], "extract": 0, "feedback": 0, "find": 0, "found": 0, "from": 0, "function": 1, "ha": 1, "handl": 0, "happen": 1, "head": 0, "help": 0, "highli": [0, 1], "how": 0, "i": [0, 1], "improv": 0, "inform": 0, "intend": 0, "issu": [0, 1], "itself": 1, "knowledg": 0, "languag": 1, "less": 1, "like": [0, 1], "mani": 0, "match": 0, "mention": 1, "more": 0, "name": [0, 1], "numer": 0, "object": 0, "often": 0, "one": [0, 1], "onli": [0, 1], "order": 0, "other": 0, "over": 0, "page": 1, "part": 1, "particular": 0, "present": 0, "printf": 1, "program": 1, "project": 0, "queri": [0, 1], "question": 0, "re": 0, "rel": 0, "research": 0, "result": 1, "retriev": 0, "sai": 0, "same": 1, "search": [0, 1], "seem": 0, "softwar": 1, "some": 1, "sphinx": 0, "straightforward": 1, "subject": 0, "subsect": 0, "term": [0, 1], "test": 0, "text": 0, "than": [0, 1], "thei": 0, "them": 0, "thi": 0, "time": 0, "titl": 0, "two": 0, "typic": 0, "us": 0, "user": [0, 1], "we": [0, 1], "when": 0, "whether": 1, "which": 0, "within": 0, "word": 0, "would": [0, 1]}, "titles": ["Main Page", "Relevance"], "titleterms": {"main": 0, "page": 0, "relev": [0, 1], "result": 0, "score": 0}}) \ No newline at end of file +Search.setIndex({"alltitles": {"Main Page": [[0, null]], "Relevance": [[0, "relevance"], [1, null]], "Result Scoring": [[0, "result-scoring"]]}, "docnames": ["index", "relevance"], "envversion": {"sphinx": 64, "sphinx.domains.c": 3, "sphinx.domains.changeset": 1, "sphinx.domains.citation": 1, "sphinx.domains.cpp": 9, "sphinx.domains.index": 1, "sphinx.domains.javascript": 3, "sphinx.domains.math": 2, "sphinx.domains.python": 4, "sphinx.domains.rst": 2, "sphinx.domains.std": 2}, "filenames": ["index.rst", "relevance.rst"], "indexentries": {"example (class in relevance)": [[0, "relevance.Example", false]], "module": [[0, "module-relevance", false]], "relevance": [[0, "index-1", false], [0, "module-relevance", false]], "relevance (relevance.example attribute)": [[0, "relevance.Example.relevance", false]], "scoring": [[0, "index-0", true]]}, "objects": {"": [[0, 0, 0, "-", "relevance"]], "relevance": [[0, 1, 1, "", "Example"]], "relevance.Example": [[0, 2, 1, "", "relevance"]]}, "objnames": {"0": ["py", "module", "Python module"], "1": ["py", "class", "Python class"], "2": ["py", "attribute", "Python attribute"]}, "objtypes": {"0": "py:module", "1": "py:class", "2": "py:attribute"}, "terms": {"": [0, 1], "A": 1, "By": 0, "For": [0, 1], "In": [0, 1], "against": 0, "align": 0, "also": 1, "an": 0, "answer": 0, "appear": 1, "ar": 1, "area": 0, "ask": 0, "assign": 0, "attempt": 0, "attribut": 0, "both": 0, "built": 1, "can": [0, 1], "class": 0, "code": [0, 1], "collect": 0, "consid": 1, "contain": 0, "context": 0, "corpu": 1, "could": 1, "demonstr": 0, "describ": 1, "detail": 1, "determin": [0, 1], "docstr": 0, "document": [0, 1], "domain": 1, "dure": 0, "engin": 0, "evalu": 0, "exampl": [0, 1], "extract": 0, "feedback": 0, "find": 0, "found": 0, "from": 0, "function": 1, "ha": 1, "handl": 0, "happen": 1, "head": 0, "help": 0, "highli": [0, 1], "how": 0, "i": [0, 1], "improv": 0, "inform": 0, "intend": 0, "issu": [0, 1], "itself": 1, "knowledg": 0, "languag": 1, "less": 1, "like": [0, 1], "mani": 0, "match": 0, "mention": 1, "more": 0, "name": [0, 1], "numer": 0, "object": 0, "often": 0, "one": [0, 1], "onli": [0, 1], "order": 0, "other": 0, "over": 0, "page": 1, "part": 1, "particular": 0, "present": 0, "printf": 1, "program": 1, "project": 0, "queri": [0, 1], "question": 0, "re": 0, "rel": 0, "research": 0, "result": 1, "retriev": 0, "sai": 0, "same": 1, "search": [0, 1], "seem": 0, "softwar": 1, "some": 1, "sphinx": 0, "straightforward": 1, "subject": 0, "subsect": 0, "term": [0, 1], "test": 0, "text": 0, "than": [0, 1], "thei": 0, "them": 0, "thi": 0, "time": 0, "titl": 0, "two": 0, "typic": 0, "us": 0, "user": [0, 1], "we": [0, 1], "when": 0, "whether": 1, "which": 0, "within": 0, "word": 0, "would": [0, 1]}, "titles": ["Main Page", "Relevance"], "titleterms": {"main": 0, "page": 0, "relev": [0, 1], "result": 0, "score": 0}}) \ No newline at end of file From a0ccb84c1c8cdc90ef9b33087601049479ee8a76 Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Tue, 13 Aug 2024 13:59:07 -0400 Subject: [PATCH 5/6] FIX: Diff --- sphinx/builders/html/__init__.py | 11 ++++++++--- sphinx/builders/html/_build_info.py | 14 +++++++++++++- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/sphinx/builders/html/__init__.py b/sphinx/builders/html/__init__.py index 891ff847261..9cf6a145a23 100644 --- a/sphinx/builders/html/__init__.py +++ b/sphinx/builders/html/__init__.py @@ -336,15 +336,20 @@ def get_outdated_docs(self) -> Iterator[str]: if self.build_info != build_info: # log the mismatch and backup the old build info build_info_backup = build_info_fname.with_name('.buildinfo.bak') + bad_keys = self.build_info.differing_keys(build_info) + msg = __(f'all docs marked outdated due to build_info mismatch') + if bad_keys: + msg += __(f' in %s config key(s): ') % len(bad_keys) + bad_keys = bad_keys[:10] + ([] if len(bad_keys) <= 10 else ['...']) + msg += repr(bad_keys) try: shutil.move(build_info_fname, build_info_backup) self.build_info.dump(build_info_fname) except OSError: pass # ignore errors else: - # only log on success - msg = __('build_info mismatch, copying .buildinfo to .buildinfo.bak') - logger.info(bold(__('building [html]: ')) + msg) + msg += __(", copying .buildinfo to .buildinfo.bak") + logger.info(bold(__('building [html]: ')) + msg) yield from self.env.found_docs return diff --git a/sphinx/builders/html/_build_info.py b/sphinx/builders/html/_build_info.py index e853284bcd9..dccf7f44b59 100644 --- a/sphinx/builders/html/_build_info.py +++ b/sphinx/builders/html/_build_info.py @@ -78,6 +78,18 @@ def dump(self, filename: Path, /) -> None: ) filename.write_text(build_info, encoding="utf-8") + def differing_keys(self, other: BuildInfo, *, kind="config") -> list[str]: + """Compute the keys that differ between two configs.""" + self_config = json.loads(getattr(self, f"{kind}_hash")) + other_config = json.loads(getattr(other, f"{kind}_hash")) + return [ + key + for key in sorted(set(self_config) | set(other_config)) + if key not in self_config + or key not in other_config + or self_config[key] != other_config[key] + ] + def _stable_str(obj: Any) -> str: """Return a stable string representation of a Python data structure. @@ -92,7 +104,7 @@ def _json_prep(obj: Any) -> dict[str, Any] | list[Any] | str: # convert to a sorted dict obj = {_json_prep(k): _json_prep(v) for k, v in obj.items()} obj = {k: obj[k] for k in sorted(obj, key=str)} - if isinstance(obj, list | tuple | set | frozenset): + elif isinstance(obj, list | tuple | set | frozenset): # convert to a sorted list obj = sorted(map(_json_prep, obj), key=str) elif isinstance(obj, type | types.FunctionType): From aac15a0938e33602609146df7bcd91a088e198ee Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Tue, 13 Aug 2024 14:11:47 -0400 Subject: [PATCH 6/6] FIX: Dont bother --- sphinx/builders/html/__init__.py | 4 ++-- sphinx/builders/html/_build_info.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/sphinx/builders/html/__init__.py b/sphinx/builders/html/__init__.py index 9cf6a145a23..f0345978f2d 100644 --- a/sphinx/builders/html/__init__.py +++ b/sphinx/builders/html/__init__.py @@ -337,9 +337,9 @@ def get_outdated_docs(self) -> Iterator[str]: # log the mismatch and backup the old build info build_info_backup = build_info_fname.with_name('.buildinfo.bak') bad_keys = self.build_info.differing_keys(build_info) - msg = __(f'all docs marked outdated due to build_info mismatch') + msg = __('all docs marked outdated due to build_info mismatch') if bad_keys: - msg += __(f' in %s config key(s): ') % len(bad_keys) + msg += __(' in %s config key(s): ') % len(bad_keys) bad_keys = bad_keys[:10] + ([] if len(bad_keys) <= 10 else ['...']) msg += repr(bad_keys) try: diff --git a/sphinx/builders/html/_build_info.py b/sphinx/builders/html/_build_info.py index dccf7f44b59..da5ca29b82c 100644 --- a/sphinx/builders/html/_build_info.py +++ b/sphinx/builders/html/_build_info.py @@ -78,10 +78,10 @@ def dump(self, filename: Path, /) -> None: ) filename.write_text(build_info, encoding="utf-8") - def differing_keys(self, other: BuildInfo, *, kind="config") -> list[str]: + def differing_keys(self, other: BuildInfo) -> list[str]: """Compute the keys that differ between two configs.""" - self_config = json.loads(getattr(self, f"{kind}_hash")) - other_config = json.loads(getattr(other, f"{kind}_hash")) + self_config = json.loads(self.config_hash) + other_config = json.loads(other.config_hash) return [ key for key in sorted(set(self_config) | set(other_config))