Cours/venv/lib/python3.12/site-packages/mkdocs/utils/yaml.py

151 lines
5.0 KiB
Python

from __future__ import annotations
import functools
import logging
import os
import os.path
from typing import IO, TYPE_CHECKING, Any
import mergedeep # type: ignore
import yaml
import yaml.constructor
import yaml_env_tag # type: ignore
from mkdocs import exceptions
if TYPE_CHECKING:
from mkdocs.config.defaults import MkDocsConfig
log = logging.getLogger(__name__)
def _construct_dir_placeholder(
config: MkDocsConfig, loader: yaml.BaseLoader, node: yaml.ScalarNode
) -> _DirPlaceholder:
loader.construct_scalar(node)
value: str = (node and node.value) or ''
prefix, _, suffix = value.partition('/')
if prefix.startswith('$'):
if prefix == '$config_dir':
return ConfigDirPlaceholder(config, suffix)
elif prefix == '$docs_dir':
return DocsDirPlaceholder(config, suffix)
else:
raise exceptions.ConfigurationError(
f"Unknown prefix {prefix!r} in {node.tag} {node.value!r}"
)
else:
return RelativeDirPlaceholder(config, value)
class _DirPlaceholder(os.PathLike):
def __init__(self, config: MkDocsConfig, suffix: str = ''):
self.config = config
self.suffix = suffix
def value(self) -> str:
raise NotImplementedError
def __fspath__(self) -> str:
"""Can be used as a path."""
return os.path.join(self.value(), self.suffix)
def __str__(self) -> str:
"""Can be converted to a string to obtain the current class."""
return self.__fspath__()
class ConfigDirPlaceholder(_DirPlaceholder):
"""
A placeholder object that gets resolved to the directory of the config file when used as a path.
The suffix can be an additional sub-path that is always appended to this path.
This is the implementation of the `!relative $config_dir/suffix` tag, but can also be passed programmatically.
"""
def value(self) -> str:
return os.path.dirname(self.config.config_file_path)
class DocsDirPlaceholder(_DirPlaceholder):
"""
A placeholder object that gets resolved to the docs dir when used as a path.
The suffix can be an additional sub-path that is always appended to this path.
This is the implementation of the `!relative $docs_dir/suffix` tag, but can also be passed programmatically.
"""
def value(self) -> str:
return self.config.docs_dir
class RelativeDirPlaceholder(_DirPlaceholder):
"""
A placeholder object that gets resolved to the directory of the Markdown file currently being rendered.
This is the implementation of the `!relative` tag, but can also be passed programmatically.
"""
def __init__(self, config: MkDocsConfig, suffix: str = ''):
if suffix:
raise exceptions.ConfigurationError(
f"'!relative' tag does not expect any value; received {suffix!r}"
)
super().__init__(config, suffix)
def value(self) -> str:
current_page = self.config._current_page
if current_page is None:
raise exceptions.ConfigurationError(
"The current file is not set for the '!relative' tag. "
"It cannot be used in this context; the intended usage is within `markdown_extensions`."
)
return os.path.dirname(os.path.join(self.config.docs_dir, current_page.file.src_path))
def get_yaml_loader(loader=yaml.Loader, config: MkDocsConfig | None = None):
"""Wrap PyYaml's loader so we can extend it to suit our needs."""
class Loader(loader):
"""
Define a custom loader derived from the global loader to leave the
global loader unaltered.
"""
# Attach Environment Variable constructor.
# See https://github.com/waylan/pyyaml-env-tag
Loader.add_constructor('!ENV', yaml_env_tag.construct_env_tag)
if config is not None:
Loader.add_constructor('!relative', functools.partial(_construct_dir_placeholder, config))
return Loader
def yaml_load(source: IO | str, loader: type[yaml.BaseLoader] | None = None) -> dict[str, Any]:
"""Return dict of source YAML file using loader, recursively deep merging inherited parent."""
loader = loader or get_yaml_loader()
try:
result = yaml.load(source, Loader=loader)
except yaml.YAMLError as e:
raise exceptions.ConfigurationError(
f"MkDocs encountered an error parsing the configuration file: {e}"
)
if result is None:
return {}
if 'INHERIT' in result and not isinstance(source, str):
relpath = result.pop('INHERIT')
abspath = os.path.normpath(os.path.join(os.path.dirname(source.name), relpath))
if not os.path.exists(abspath):
raise exceptions.ConfigurationError(
f"Inherited config file '{relpath}' does not exist at '{abspath}'."
)
log.debug(f"Loading inherited configuration file: {abspath}")
with open(abspath, 'rb') as fd:
parent = yaml_load(fd, loader)
result = mergedeep.merge(parent, result)
return result