1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150
|
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
|