mirror of
https://github.com/sphinx-doc/sphinx.git
synced 2025-02-25 18:55:22 -06:00
Refactor `BuildInfo
` and move to a new module (#12768)
This commit is contained in:
parent
9d3087cc92
commit
a3f138329b
@ -15,7 +15,8 @@ from docutils import nodes
|
|||||||
from docutils.utils import smartquotes
|
from docutils.utils import smartquotes
|
||||||
|
|
||||||
from sphinx import addnodes
|
from sphinx import addnodes
|
||||||
from sphinx.builders.html import BuildInfo, StandaloneHTMLBuilder
|
from sphinx.builders.html import StandaloneHTMLBuilder
|
||||||
|
from sphinx.builders.html._build_info import BuildInfo
|
||||||
from sphinx.locale import __
|
from sphinx.locale import __
|
||||||
from sphinx.util import logging
|
from sphinx.util import logging
|
||||||
from sphinx.util.display import status_iterator
|
from sphinx.util.display import status_iterator
|
||||||
|
@ -3,17 +3,16 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import contextlib
|
import contextlib
|
||||||
import hashlib
|
|
||||||
import html
|
import html
|
||||||
import os
|
import os
|
||||||
import posixpath
|
import posixpath
|
||||||
import re
|
import re
|
||||||
|
import shutil
|
||||||
import sys
|
import sys
|
||||||
import types
|
|
||||||
import warnings
|
import warnings
|
||||||
from os import path
|
from os import path
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import IO, TYPE_CHECKING, Any
|
from typing import TYPE_CHECKING, Any
|
||||||
from urllib.parse import quote
|
from urllib.parse import quote
|
||||||
|
|
||||||
import docutils.readers.doctree
|
import docutils.readers.doctree
|
||||||
@ -31,6 +30,7 @@ from sphinx.builders.html._assets import (
|
|||||||
_file_checksum,
|
_file_checksum,
|
||||||
_JavaScript,
|
_JavaScript,
|
||||||
)
|
)
|
||||||
|
from sphinx.builders.html._build_info import BuildInfo
|
||||||
from sphinx.config import ENUM, Config
|
from sphinx.config import ENUM, Config
|
||||||
from sphinx.deprecation import _deprecation_warning
|
from sphinx.deprecation import _deprecation_warning
|
||||||
from sphinx.domains import Domain, Index, IndexEntry
|
from sphinx.domains import Domain, Index, IndexEntry
|
||||||
@ -63,16 +63,14 @@ from sphinx.writers.html import HTMLWriter
|
|||||||
from sphinx.writers.html5 import HTML5Translator
|
from sphinx.writers.html5 import HTML5Translator
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from collections.abc import Iterable, Iterator, Set
|
from collections.abc import Iterable, Iterator
|
||||||
from typing import TypeAlias
|
from typing import TypeAlias
|
||||||
|
|
||||||
from docutils.nodes import Node
|
from docutils.nodes import Node
|
||||||
from docutils.readers import Reader
|
from docutils.readers import Reader
|
||||||
|
|
||||||
from sphinx.application import Sphinx
|
from sphinx.application import Sphinx
|
||||||
from sphinx.config import _ConfigRebuild
|
|
||||||
from sphinx.environment import BuildEnvironment
|
from sphinx.environment import BuildEnvironment
|
||||||
from sphinx.util.tags import Tags
|
|
||||||
from sphinx.util.typing import ExtensionMetadata
|
from sphinx.util.typing import ExtensionMetadata
|
||||||
|
|
||||||
#: the filename for the inventory of objects
|
#: the filename for the inventory of objects
|
||||||
@ -93,23 +91,6 @@ DOMAIN_INDEX_TYPE: TypeAlias = tuple[
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def _stable_hash(obj: Any) -> str:
|
|
||||||
"""Return a stable hash for a Python data structure.
|
|
||||||
|
|
||||||
We can't just use the md5 of str(obj) as the order of collections
|
|
||||||
may be random.
|
|
||||||
"""
|
|
||||||
if isinstance(obj, dict):
|
|
||||||
obj = sorted(map(_stable_hash, obj.items()))
|
|
||||||
if isinstance(obj, list | tuple | set | frozenset):
|
|
||||||
obj = sorted(map(_stable_hash, obj))
|
|
||||||
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()
|
|
||||||
|
|
||||||
|
|
||||||
def convert_locale_to_language_tag(locale: str | None) -> str | None:
|
def convert_locale_to_language_tag(locale: str | None) -> str | None:
|
||||||
"""Convert a locale string to a language tag (ex. en_US -> en-US).
|
"""Convert a locale string to a language tag (ex. en_US -> en-US).
|
||||||
|
|
||||||
@ -121,57 +102,6 @@ def convert_locale_to_language_tag(locale: str | None) -> str | None:
|
|||||||
return 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], 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: ')
|
|
||||||
|
|
||||||
build_info = BuildInfo()
|
|
||||||
build_info.config_hash = lines[2].split()[1].strip()
|
|
||||||
build_info.tags_hash = lines[3].split()[1].strip()
|
|
||||||
return build_info
|
|
||||||
except Exception as exc:
|
|
||||||
raise ValueError(__('build info file is broken: %r') % exc) from exc
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
config: Config | None = None,
|
|
||||||
tags: Tags | None = None,
|
|
||||||
config_categories: Set[_ConfigRebuild] = frozenset(),
|
|
||||||
) -> None:
|
|
||||||
self.config_hash = ''
|
|
||||||
self.tags_hash = ''
|
|
||||||
|
|
||||||
if config:
|
|
||||||
values = {c.name: c.value for c in config.filter(config_categories)}
|
|
||||||
self.config_hash = _stable_hash(values)
|
|
||||||
|
|
||||||
if tags:
|
|
||||||
self.tags_hash = _stable_hash(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))
|
|
||||||
|
|
||||||
|
|
||||||
class StandaloneHTMLBuilder(Builder):
|
class StandaloneHTMLBuilder(Builder):
|
||||||
"""
|
"""
|
||||||
Builds standalone HTML docs.
|
Builds standalone HTML docs.
|
||||||
@ -396,18 +326,28 @@ class StandaloneHTMLBuilder(Builder):
|
|||||||
def get_outdated_docs(self) -> Iterator[str]:
|
def get_outdated_docs(self) -> Iterator[str]:
|
||||||
build_info_fname = self.outdir / '.buildinfo'
|
build_info_fname = self.outdir / '.buildinfo'
|
||||||
try:
|
try:
|
||||||
with open(build_info_fname, encoding="utf-8") as fp:
|
build_info = BuildInfo.load(build_info_fname)
|
||||||
buildinfo = BuildInfo.load(fp)
|
|
||||||
|
|
||||||
if self.build_info != buildinfo:
|
|
||||||
logger.debug('[build target] did not match: build_info ')
|
|
||||||
yield from self.env.found_docs
|
|
||||||
return
|
|
||||||
except ValueError as exc:
|
except ValueError as exc:
|
||||||
logger.warning(__('Failed to read build info file: %r'), exc)
|
logger.warning(__('Failed to read build info file: %r'), exc)
|
||||||
except OSError:
|
except OSError:
|
||||||
# ignore errors on reading
|
# ignore errors on reading
|
||||||
pass
|
pass
|
||||||
|
else:
|
||||||
|
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')
|
||||||
|
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)
|
||||||
|
|
||||||
|
yield from self.env.found_docs
|
||||||
|
return
|
||||||
|
|
||||||
if self.templates:
|
if self.templates:
|
||||||
template_mtime = int(self.templates.newest_template_mtime() * 10**6)
|
template_mtime = int(self.templates.newest_template_mtime() * 10**6)
|
||||||
@ -943,8 +883,7 @@ class StandaloneHTMLBuilder(Builder):
|
|||||||
|
|
||||||
def write_buildinfo(self) -> None:
|
def write_buildinfo(self) -> None:
|
||||||
try:
|
try:
|
||||||
with open(path.join(self.outdir, '.buildinfo'), 'w', encoding="utf-8") as fp:
|
self.build_info.dump(self.outdir / '.buildinfo')
|
||||||
self.build_info.dump(fp)
|
|
||||||
except OSError as exc:
|
except OSError as exc:
|
||||||
logger.warning(__('Failed to write build info file: %r'), exc)
|
logger.warning(__('Failed to write build info file: %r'), exc)
|
||||||
|
|
||||||
|
94
sphinx/builders/html/_build_info.py
Normal file
94
sphinx/builders/html/_build_info.py
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
"""Record metadata for the build process."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import hashlib
|
||||||
|
import types
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
from sphinx.locale import __
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from collections.abc import Set
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from sphinx.config import Config, _ConfigRebuild
|
||||||
|
from sphinx.util.tags import Tags
|
||||||
|
|
||||||
|
|
||||||
|
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:
|
||||||
|
content = filename.read_text(encoding="utf-8")
|
||||||
|
lines = content.splitlines()
|
||||||
|
|
||||||
|
version = lines[0].rstrip()
|
||||||
|
if version != '# Sphinx build info version 1':
|
||||||
|
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_hash = lines[2].removeprefix('config: ').strip()
|
||||||
|
build_info.tags_hash = 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_hash = ''
|
||||||
|
self.tags_hash = ''
|
||||||
|
|
||||||
|
if config:
|
||||||
|
values = {c.name: c.value for c in config.filter(config_categories)}
|
||||||
|
self.config_hash = _stable_hash(values)
|
||||||
|
|
||||||
|
if tags:
|
||||||
|
self.tags_hash = _stable_hash(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, filename: Path, /) -> None:
|
||||||
|
build_info = (
|
||||||
|
'# Sphinx build info version 1\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'
|
||||||
|
f'tags: {self.tags_hash}\n'
|
||||||
|
)
|
||||||
|
filename.write_text(build_info, encoding="utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
def _stable_hash(obj: Any) -> str:
|
||||||
|
"""Return a stable hash for a Python data structure.
|
||||||
|
|
||||||
|
We can't just use the md5 of str(obj) as the order of collections
|
||||||
|
may be random.
|
||||||
|
"""
|
||||||
|
if isinstance(obj, dict):
|
||||||
|
obj = sorted(map(_stable_hash, obj.items()))
|
||||||
|
if isinstance(obj, list | tuple | set | frozenset):
|
||||||
|
obj = sorted(map(_stable_hash, obj))
|
||||||
|
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()
|
Loading…
Reference in New Issue
Block a user