151 lines
5.0 KiB
Python
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
|