/
yaml.py
146 lines (110 loc) · 4.89 KB
/
yaml.py
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
from __future__ import annotations
import functools
import logging
import os
import os.path
from typing import IO, TYPE_CHECKING, Any
import mergedeep
import yaml
import yaml.constructor
import yaml_env_tag
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:
if self.config._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(self.config._current_page.file.abs_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