-
-
Notifications
You must be signed in to change notification settings - Fork 2.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Refactor
BuildInfo
and move to a new module (#12768)
- Loading branch information
Showing
3 changed files
with
118 additions
and
84 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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() |