from __future__ import annotations
import os
import zlib
from functools import cache
from typing import TYPE_CHECKING
from sphinx.errors import ThemeError
if TYPE_CHECKING:
from pathlib import Path
from typing import Any, NoReturn
class _CascadingStyleSheet:
filename: str | os.PathLike[str]
priority: int
attributes: dict[str, str]
def __init__(
self,
filename: str | os.PathLike[str],
/,
*,
priority: int = 500,
rel: str = 'stylesheet',
type: str = 'text/css',
**attributes: str,
) -> None:
object.__setattr__(self, 'filename', filename)
object.__setattr__(self, 'priority', priority)
object.__setattr__(self, 'attributes', {'rel': rel, 'type': type} | attributes)
def __str__(self) -> str:
attr = ', '.join(f'{k}={v!r}' for k, v in self.attributes.items())
return (
f'{self.__class__.__name__}({self.filename!r}, '
f'priority={self.priority}, '
f'{attr})'
)
def __eq__(self, other: object) -> bool:
if not isinstance(other, _CascadingStyleSheet):
return NotImplemented
return (
self.filename == other.filename
and self.priority == other.priority
and self.attributes == other.attributes
)
def __hash__(self) -> int:
return hash((self.filename, self.priority, *sorted(self.attributes.items())))
def __setattr__(self, key: str, value: Any) -> NoReturn:
msg = f'{self.__class__.__name__} is immutable'
raise AttributeError(msg)
def __delattr__(self, key: str) -> NoReturn:
msg = f'{self.__class__.__name__} is immutable'
raise AttributeError(msg)
class _JavaScript:
filename: str | os.PathLike[str]
priority: int
attributes: dict[str, str]
def __init__(
self,
filename: str | os.PathLike[str],
/,
*,
priority: int = 500,
**attributes: str,
) -> None:
object.__setattr__(self, 'filename', filename)
object.__setattr__(self, 'priority', priority)
object.__setattr__(self, 'attributes', attributes)
def __str__(self) -> str:
attr = ''
if self.attributes:
attr = ', ' + ', '.join(f'{k}={v!r}' for k, v in self.attributes.items())
return (
f'{self.__class__.__name__}({self.filename!r}, '
f'priority={self.priority}'
f'{attr})'
)
def __eq__(self, other: object) -> bool:
if not isinstance(other, _JavaScript):
return NotImplemented
return (
self.filename == other.filename
and self.priority == other.priority
and self.attributes == other.attributes
)
def __hash__(self) -> int:
return hash((self.filename, self.priority, *sorted(self.attributes.items())))
def __setattr__(self, key: str, value: Any) -> NoReturn:
msg = f'{self.__class__.__name__} is immutable'
raise AttributeError(msg)
def __delattr__(self, key: str) -> NoReturn:
msg = f'{self.__class__.__name__} is immutable'
raise AttributeError(msg)
def _file_checksum(outdir: Path, filename: str | os.PathLike[str]) -> str:
filename = os.fspath(filename)
# Don't generate checksums for HTTP URIs
if '://' in filename:
return ''
# Some themes and extensions have used query strings
# for a similar asset checksum feature.
# As we cannot safely strip the query string,
# raise an error to the user.
if '?' in filename:
msg = f'Local asset file paths must not contain query strings: {filename!r}'
raise ThemeError(msg)
return _file_checksum_inner(outdir.joinpath(filename).resolve())
@cache
def _file_checksum_inner(file: Path) -> str:
try:
# Remove all carriage returns to avoid checksum differences
content = file.read_bytes().translate(None, b'\r')
except FileNotFoundError:
return ''
if not content:
return ''
return f'{zlib.crc32(content):08x}'