File: yaml.py

package info (click to toggle)
python-mkdocs 1.6.1%2Bdfsg1-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 7,812 kB
  • sloc: python: 14,346; javascript: 10,535; perl: 143; sh: 57; makefile: 30; xml: 11
file content (150 lines) | stat: -rw-r--r-- 5,108 bytes parent folder | download
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