From a59e280d80830ddf2bef3967fd0fe8813b3872e6 Mon Sep 17 00:00:00 2001 From: Oleh Prypin Date: Fri, 23 Jun 2023 17:17:08 +0200 Subject: [PATCH 1/3] Make `config_file_path` default to empty string --- docs/hooks.py | 2 +- mkdocs/commands/gh_deploy.py | 2 +- mkdocs/commands/serve.py | 3 ++- mkdocs/config/base.py | 13 +++++++++---- mkdocs/config/config_options.py | 4 ++-- mkdocs/config/defaults.py | 2 +- 6 files changed, 16 insertions(+), 10 deletions(-) diff --git a/docs/hooks.py b/docs/hooks.py index 1ab58b88..34b556f1 100644 --- a/docs/hooks.py +++ b/docs/hooks.py @@ -16,7 +16,7 @@ def _get_language_of_translation_file(path: Path) -> str: def on_page_markdown(markdown: str, page: Page, config: MkDocsConfig, **kwargs): if page.file.src_uri == 'user-guide/choosing-your-theme.md': - here = Path(config.config_file_path or '').parent + here = Path(config.config_file_path).parent def replacement(m: re.Match) -> str: lines = [] diff --git a/mkdocs/commands/gh_deploy.py b/mkdocs/commands/gh_deploy.py index de5fb99b..f6e5fa05 100644 --- a/mkdocs/commands/gh_deploy.py +++ b/mkdocs/commands/gh_deploy.py @@ -116,7 +116,7 @@ def gh_deploy( if message is None: message = default_message - sha = _get_current_sha(os.path.dirname(config.config_file_path or '')) + sha = _get_current_sha(os.path.dirname(config.config_file_path)) message = message.format(version=mkdocs.__version__, sha=sha) log.info( diff --git a/mkdocs/commands/serve.py b/mkdocs/commands/serve.py index 48095f9b..1797e815 100644 --- a/mkdocs/commands/serve.py +++ b/mkdocs/commands/serve.py @@ -100,7 +100,8 @@ def serve( if livereload: # Watch the documentation files, the config file and the theme files. server.watch(config.docs_dir) - server.watch(config.config_file_path) + if config.config_file_path: + server.watch(config.config_file_path) if watch_theme: for d in config.theme.dirs: diff --git a/mkdocs/config/base.py b/mkdocs/config/base.py index dcbc6e23..14f1d953 100644 --- a/mkdocs/config/base.py +++ b/mkdocs/config/base.py @@ -133,7 +133,7 @@ class Config(UserDict): """ _schema: PlainConfigSchema - config_file_path: str | None + config_file_path: str def __init_subclass__(cls): schema = dict(getattr(cls, '_schema', ())) @@ -170,7 +170,7 @@ class Config(UserDict): config_file_path = config_file_path.decode(encoding=sys.getfilesystemencoding()) except UnicodeDecodeError: raise ValidationError("config_file_path is not a Unicode string.") - self.config_file_path = config_file_path + self.config_file_path = config_file_path or '' def set_defaults(self) -> None: """ @@ -344,7 +344,9 @@ def _open_config_file(config_file: str | IO | None) -> Iterator[IO]: result_config_file.close() -def load_config(config_file: str | IO | None = None, **kwargs) -> MkDocsConfig: +def load_config( + config_file: str | IO | None = None, *, config_file_path: str | None = None, **kwargs +) -> MkDocsConfig: """ Load the configuration for a given file object or name @@ -366,7 +368,10 @@ def load_config(config_file: str | IO | None = None, **kwargs) -> MkDocsConfig: # Initialize the config with the default schema. from mkdocs.config.defaults import MkDocsConfig - cfg = MkDocsConfig(config_file_path=getattr(fd, 'name', '')) + if config_file_path is None: + if fd is not sys.stdin.buffer: + config_file_path = getattr(fd, 'name', None) + cfg = MkDocsConfig(config_file_path=config_file_path) # load the config file cfg.load_file(fd) diff --git a/mkdocs/config/config_options.py b/mkdocs/config/config_options.py index 23a113c0..05994b03 100644 --- a/mkdocs/config/config_options.py +++ b/mkdocs/config/config_options.py @@ -708,7 +708,7 @@ class Dir(FilesystemObject): class DocsDir(Dir): def post_validation(self, config: Config, key_name: str): - if config.config_file_path is None: + if not config.config_file_path: return # Validate that the dir is not the parent dir of the config file. @@ -826,7 +826,7 @@ class Theme(BaseConfigOption[theme.Theme]): # Ensure custom_dir is an absolute path if 'custom_dir' in theme_config and not os.path.isabs(theme_config['custom_dir']): - config_dir = os.path.dirname(self.config_file_path or '') + config_dir = os.path.dirname(self.config_file_path) theme_config['custom_dir'] = os.path.join(config_dir, theme_config['custom_dir']) if 'custom_dir' in theme_config and not os.path.isdir(theme_config['custom_dir']): diff --git a/mkdocs/config/defaults.py b/mkdocs/config/defaults.py index 891ee710..71e01bcb 100644 --- a/mkdocs/config/defaults.py +++ b/mkdocs/config/defaults.py @@ -16,7 +16,7 @@ def get_schema() -> base.PlainConfigSchema: class MkDocsConfig(base.Config): """The configuration of MkDocs itself (the root object of mkdocs.yml).""" - config_file_path: str | None = c.Optional(c.Type(str)) # type: ignore[assignment] + config_file_path: str = c.Type(str) # type: ignore[assignment] """The path to the mkdocs.yml config file. Can't be populated from the config.""" site_name = c.Type(str) From 7d8483904a5fde632fd7d12f8b4c74fd0f36fa49 Mon Sep 17 00:00:00 2001 From: Oleh Prypin Date: Fri, 23 Jun 2023 18:30:34 +0200 Subject: [PATCH 2/3] Add YAML placeholder tags that resolve to current paths - !relative # Obtains the directory of the currently rendered Markdown file - !relative $docs_dir # Obtains the docs_dir - !relative $config_dir # Obtains the directory of mkdocs.yml --- mkdocs/commands/build.py | 10 ++- mkdocs/config/base.py | 15 ++--- mkdocs/config/defaults.py | 13 +++- mkdocs/tests/build_tests.py | 87 +++++++++++++++++++++++++ mkdocs/utils/__init__.py | 43 +------------ mkdocs/utils/yaml.py | 122 ++++++++++++++++++++++++++++++++++++ 6 files changed, 237 insertions(+), 53 deletions(-) create mode 100644 mkdocs/utils/yaml.py diff --git a/mkdocs/commands/build.py b/mkdocs/commands/build.py index c118737a..4c2a3dab 100644 --- a/mkdocs/commands/build.py +++ b/mkdocs/commands/build.py @@ -154,6 +154,7 @@ def _build_extra_template(template_name: str, files: Files, config: MkDocsConfig def _populate_page(page: Page, config: MkDocsConfig, files: Files, dirty: bool = False) -> None: """Read page content from docs_dir and render Markdown.""" + config._current_page = page try: # When --dirty is used, only read the page if the file has been modified since the # previous build of the output. @@ -185,6 +186,8 @@ def _populate_page(page: Page, config: MkDocsConfig, files: Files, dirty: bool = message += f" {e}" log.error(message) raise + finally: + config._current_page = None def _build_page( @@ -198,6 +201,7 @@ def _build_page( ) -> None: """Pass a Page to theme template and write output to site_dir.""" + config._current_page = page try: # When --dirty is used, only build the page if the file has been modified since the # previous build of the output. @@ -238,8 +242,6 @@ def _build_page( else: log.info(f"Page skipped: '{page.file.src_uri}'. Generated empty output.") - # Deactivate page - page.active = False except Exception as e: message = f"Error building page '{page.file.src_uri}':" # Prevent duplicated the error message because it will be printed immediately afterwards. @@ -247,6 +249,10 @@ def _build_page( message += f" {e}" log.error(message) raise + finally: + # Deactivate page + page.active = False + config._current_page = None def build( diff --git a/mkdocs/config/base.py b/mkdocs/config/base.py index 14f1d953..cee32e31 100644 --- a/mkdocs/config/base.py +++ b/mkdocs/config/base.py @@ -21,8 +21,6 @@ from typing import ( overload, ) -from yaml import YAMLError - from mkdocs import exceptions, utils from mkdocs.utils import weak_property @@ -257,13 +255,12 @@ class Config(UserDict): def load_file(self, config_file: IO) -> None: """Load config options from the open file descriptor of a YAML file.""" - try: - return self.load_dict(utils.yaml_load(config_file)) - except YAMLError as e: - # MkDocs knows and understands ConfigurationErrors - raise exceptions.ConfigurationError( - f"MkDocs encountered an error parsing the configuration file: {e}" - ) + warnings.warn( + "Config.load_file is not used since MkDocs 1.5 and will be removed soon. " + "Use MkDocsConfig.load_file instead", + DeprecationWarning, + ) + return self.load_dict(utils.yaml_load(config_file)) @weak_property def user_configs(self) -> Sequence[Mapping[str, Any]]: diff --git a/mkdocs/config/defaults.py b/mkdocs/config/defaults.py index 71e01bcb..1a76f1fa 100644 --- a/mkdocs/config/defaults.py +++ b/mkdocs/config/defaults.py @@ -1,9 +1,11 @@ from __future__ import annotations -from typing import Dict +from typing import IO, Dict +import mkdocs.structure.pages from mkdocs.config import base from mkdocs.config import config_options as c +from mkdocs.utils.yaml import get_yaml_loader, yaml_load def get_schema() -> base.PlainConfigSchema: @@ -137,7 +139,16 @@ class MkDocsConfig(base.Config): watch = c.ListOfPaths(default=[]) """A list of extra paths to watch while running `mkdocs serve`.""" + _current_page: mkdocs.structure.pages.Page | None = None + """The currently rendered page. Please do not access this and instead + rely on the `page` argument to event handlers.""" + def load_dict(self, patch: dict) -> None: super().load_dict(patch) if 'config_file_path' in patch: raise base.ValidationError("Can't set config_file_path in config") + + def load_file(self, config_file: IO) -> None: + """Load config options from the open file descriptor of a YAML file.""" + loader = get_yaml_loader(config=self) + self.load_dict(yaml_load(config_file, loader)) diff --git a/mkdocs/tests/build_tests.py b/mkdocs/tests/build_tests.py index bc161fce..24b2fa6c 100644 --- a/mkdocs/tests/build_tests.py +++ b/mkdocs/tests/build_tests.py @@ -2,13 +2,19 @@ from __future__ import annotations import contextlib +import io +import os.path +import re import textwrap import unittest from pathlib import Path from typing import TYPE_CHECKING from unittest import mock +import markdown.preprocessors + from mkdocs.commands import build +from mkdocs.config import base from mkdocs.exceptions import PluginError from mkdocs.livereload import LiveReloadServer from mkdocs.structure.files import File, Files @@ -743,6 +749,61 @@ class BuildTests(PathAssertionMixin, unittest.TestCase): else: self.assertPathExists(summary_path) + @tempdir( + files={ + 'README.md': 'CONFIG_README\n', + 'docs/foo.md': 'ROOT_FOO\n', + 'docs/test/bar.md': 'TEST_BAR\n', + 'docs/main/foo.md': 'MAIN_FOO\n', + 'docs/main/main.md': ( + '--8<-- "README.md"\n\n' + '--8<-- "foo.md"\n\n' + '--8<-- "test/bar.md"\n\n' + '--8<-- "../foo.md"\n\n' + ), + } + ) + def test_markdown_extension_with_relative(self, config_dir): + for base_path, expected in { + '!relative': ''' +

(Failed to read 'README.md')

+

MAIN_FOO

+

(Failed to read 'test/bar.md')

+

ROOT_FOO

''', + '!relative $docs_dir': ''' +

(Failed to read 'README.md')

+

ROOT_FOO

+

TEST_BAR

+

(Failed to read '../foo.md')

''', + '!relative $config_dir/docs': ''' +

(Failed to read 'README.md')

+

ROOT_FOO

+

TEST_BAR

+

(Failed to read '../foo.md')

''', + '!relative $config_dir': ''' +

CONFIG_README

+

(Failed to read 'foo.md')

+

(Failed to read 'test/bar.md')

+

(Failed to read '../foo.md')

''', + }.items(): + with self.subTest(base_path=base_path): + cfg = f''' + site_name: test + use_directory_urls: false + markdown_extensions: + - mkdocs.tests.build_tests: + base_path: {base_path} + ''' + config = base.load_config( + io.StringIO(cfg), config_file_path=os.path.join(config_dir, 'mkdocs.yml') + ) + + with self._assert_build_logs(''): + build.build(config) + main_path = Path(config_dir, 'site', 'main', 'main.html') + self.assertTrue(main_path.is_file()) + self.assertIn(textwrap.dedent(expected), main_path.read_text()) + # Test build.site_directory_contains_stale_files @tempdir(files=['index.html']) @@ -752,3 +813,29 @@ class BuildTests(PathAssertionMixin, unittest.TestCase): @tempdir() def test_not_site_dir_contains_stale_files(self, site_dir): self.assertFalse(build.site_directory_contains_stale_files(site_dir)) + + +class _TestPreprocessor(markdown.preprocessors.Preprocessor): + def __init__(self, base_path: str) -> None: + self.base_path = base_path + + def run(self, lines: list[str]) -> list[str]: + for i, line in enumerate(lines): + m = re.search(r'^--8<-- "(.+)"$', line) + if m: + try: + lines[i] = Path(self.base_path, m[1]).read_text() + except OSError: + lines[i] = f"(Failed to read {m[1]!r})\n" + return lines + + +class _TestExtension(markdown.extensions.Extension): + def __init__(self, base_path: str) -> None: + self.base_path = base_path + + def extendMarkdown(self, md: markdown.Markdown) -> None: + md.preprocessors.register(_TestPreprocessor(self.base_path), "mkdocs_test", priority=32) + + +makeExtension = _TestExtension diff --git a/mkdocs/utils/__init__.py b/mkdocs/utils/__init__.py index 23ec3a0b..4787d8ca 100644 --- a/mkdocs/utils/__init__.py +++ b/mkdocs/utils/__init__.py @@ -17,7 +17,7 @@ import warnings from collections import defaultdict from datetime import datetime, timezone from pathlib import PurePath -from typing import IO, TYPE_CHECKING, Any, Collection, Iterable, MutableSequence, TypeVar +from typing import TYPE_CHECKING, Collection, Iterable, MutableSequence, TypeVar from urllib.parse import urlsplit if sys.version_info >= (3, 10): @@ -25,11 +25,8 @@ if sys.version_info >= (3, 10): else: from importlib_metadata import EntryPoint, entry_points -import yaml -from mergedeep import merge -from yaml_env_tag import construct_env_tag - from mkdocs import exceptions +from mkdocs.utils.yaml import get_yaml_loader, yaml_load # noqa - legacy re-export if TYPE_CHECKING: from mkdocs.structure.pages import Page @@ -47,42 +44,6 @@ markdown_extensions = ( ) -def get_yaml_loader(loader=yaml.Loader): - """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', construct_env_tag) - - 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() - result = yaml.load(source, Loader=Loader) - 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 = merge(parent, result) - return result - - def modified_time(file_path): warnings.warn( "modified_time is never used in MkDocs and will be removed soon.", DeprecationWarning diff --git a/mkdocs/utils/yaml.py b/mkdocs/utils/yaml.py new file mode 100644 index 00000000..d7a152ab --- /dev/null +++ b/mkdocs/utils/yaml.py @@ -0,0 +1,122 @@ +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 = (node and node.value) or '' + prefix, _, subvalue = value.partition('/') + if prefix.startswith('$'): + if prefix == '$config_dir': + return _ConfigDirPlaceholder(config, subvalue) + elif prefix == '$docs_dir': + return _DocsDirPlaceholder(config, subvalue) + else: + raise exceptions.ConfigurationError( + f"Unknown prefix {prefix!r} in {node.tag} {node.value!r}" + ) + else: + if value: + raise exceptions.ConfigurationError( + f"{node.tag!r} tag does not expect any value; received {node.value!r}" + ) + return _RelativeDirPlaceholder(config) + + +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: + return os.path.join(self.value(), self.suffix) + + def __str__(self) -> str: + return self.__fspath__() + + +class _ConfigDirPlaceholder(_DirPlaceholder): + def value(self) -> str: + return os.path.dirname(self.config.config_file_path) + + +class _DocsDirPlaceholder(_DirPlaceholder): + def value(self) -> str: + return self.config.docs_dir + + +class _RelativeDirPlaceholder(_DirPlaceholder): + 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 From 99a9a905531158fa9ab1c2f0af72c71a64f46f39 Mon Sep 17 00:00:00 2001 From: Oleh Prypin Date: Sun, 25 Jun 2023 23:08:42 +0200 Subject: [PATCH 3/3] Document the `!relative` tag --- docs/dev-guide/plugins.md | 4 +-- docs/user-guide/configuration.md | 54 ++++++++++++++++++++++++++++---- mkdocs/utils/yaml.py | 48 +++++++++++++++++++++------- 3 files changed, 86 insertions(+), 20 deletions(-) diff --git a/docs/dev-guide/plugins.md b/docs/dev-guide/plugins.md index 9fdc410f..705faf57 100644 --- a/docs/dev-guide/plugins.md +++ b/docs/dev-guide/plugins.md @@ -110,7 +110,7 @@ class MyPlugin(mkdocs.plugins.BasePlugin): > from mkdocs.config import base, config_options as c > > class _ValidationOptions(base.Config): -> enable = c.Type(bool, default=True) +> enabled = c.Type(bool, default=True) > verbose = c.Type(bool, default=False) > skip_checks = c.ListOfItems(c.Choice(('foo', 'bar', 'baz')), default=[]) > @@ -130,7 +130,7 @@ class MyPlugin(mkdocs.plugins.BasePlugin): > my_plugin: > definition_file: configs/test.ini # relative to mkdocs.yml > validation: -> enable: !ENV [CI, false] +> enabled: !ENV [CI, false] > verbose: true > skip_checks: > - foo diff --git a/docs/user-guide/configuration.md b/docs/user-guide/configuration.md index 73661dea..bfb025ea 100644 --- a/docs/user-guide/configuration.md +++ b/docs/user-guide/configuration.md @@ -618,7 +618,7 @@ For example, to enable permalinks in the (included) `toc` extension, use: ```yaml markdown_extensions: - toc: - permalink: True + permalink: true ``` Note that a colon (`:`) must follow the extension name (`toc`) and then on a new @@ -629,7 +629,7 @@ defined on a separate line: ```yaml markdown_extensions: - toc: - permalink: True + permalink: true separator: "_" ``` @@ -641,10 +641,14 @@ for that extension: markdown_extensions: - smarty - toc: - permalink: True + permalink: true - sane_lists ``` +> NOTE: **Dynamic config values.** +> +> To dynamically configure the extensions, you can get the config values from [environment variables](#environment-variables) or [obtain paths](#paths-relative-to-the-current-file-or-site) of the currently rendered Markdown file or the overall MkDocs site. + In the above examples, each extension is a list item (starts with a `-`). As an alternative, key/value pairs can be used instead. However, in that case an empty value must be provided for extensions for which no options are defined. @@ -654,14 +658,14 @@ Therefore, the last example above could also be defined as follows: markdown_extensions: smarty: {} toc: - permalink: True + permalink: true sane_lists: {} ``` This alternative syntax is required if you intend to override some options via [inheritance]. -> NOTE: **See Also:** +> NOTE: **More extensions.** > > The Python-Markdown documentation provides a [list of extensions][exts] > which are available out-of-the-box. For a list of configuration options @@ -896,7 +900,9 @@ plugins: **default**: `full` -## Environment Variables +## Special YAML tags + +### Environment variables In most cases, the value of a configuration option is set directly in the configuration file. However, as an option, the value of a configuration option @@ -933,6 +939,41 @@ cannot be defined within a single environment variable. For more details, see the [pyyaml_env_tag](https://github.com/waylan/pyyaml-env-tag) project. +### Paths relative to the current file or site + +NEW: **New in version 1.5.** + +Some Markdown extensions can benefit from knowing the path of the Markdown file that's currently being processed, or just the root path of the current site. For that, the special tag `!relative` can be used in most contexts within the config file, though the only known usecases are within [`markdown_extensions`](#markdown_extensions). + +Examples of the possible values are: + +```yaml +- !relative # Relative to the directory of the current Markdown file +- !relative $docs_dir # Path of the docs_dir +- !relative $config_dir # Path of the directory that contains the main mkdocs.yml +- !relative $config_dir/some/child/dir # Some subdirectory of the root config directory +``` + +(Here, `$docs_dir` and `$config_dir` are currently the *only* special prefixes that are recognized.) + +Example: + +```yaml +markdown_extensions: + - pymdownx.snippets: + base_path: !relative # Relative to the current Markdown file +``` + +This allows the [pymdownx.snippets] extension to include files relative to the current Markdown file, which without this tag it would have no way of knowing. + +> NOTE: Even for the default case, any extension's base path is technically the *current working directory* although the assumption is that it's the *directory of mkdocs.yml*. So even if you don't want the paths to be relative, to improve the default behavior, always prefer to use this idiom: +> +> ```yaml +> markdown_extensions: +> - pymdownx.snippets: +> base_path: !relative $config_dir # Relative to the root directory with mkdocs.yml +> ``` + ## Configuration Inheritance Generally, a single file would hold the entire configuration for a site. @@ -1077,3 +1118,4 @@ echo '{INHERIT: mkdocs.yml, site_name: "Renamed site"}' | mkdocs build -f - [markdown_extensions]: #markdown_extensions [nav]: #nav [inheritance]: #configuration-inheritance +[pymdownx.snippets]: https://facelessuser.github.io/pymdown-extensions/extensions/snippets/ diff --git a/mkdocs/utils/yaml.py b/mkdocs/utils/yaml.py index d7a152ab..40e1d301 100644 --- a/mkdocs/utils/yaml.py +++ b/mkdocs/utils/yaml.py @@ -24,23 +24,19 @@ def _construct_dir_placeholder( ) -> _DirPlaceholder: loader.construct_scalar(node) - value = (node and node.value) or '' - prefix, _, subvalue = value.partition('/') + value: str = (node and node.value) or '' + prefix, _, suffix = value.partition('/') if prefix.startswith('$'): if prefix == '$config_dir': - return _ConfigDirPlaceholder(config, subvalue) + return ConfigDirPlaceholder(config, suffix) elif prefix == '$docs_dir': - return _DocsDirPlaceholder(config, subvalue) + return DocsDirPlaceholder(config, suffix) else: raise exceptions.ConfigurationError( f"Unknown prefix {prefix!r} in {node.tag} {node.value!r}" ) else: - if value: - raise exceptions.ConfigurationError( - f"{node.tag!r} tag does not expect any value; received {node.value!r}" - ) - return _RelativeDirPlaceholder(config) + return RelativeDirPlaceholder(config, value) class _DirPlaceholder(os.PathLike): @@ -52,23 +48,51 @@ class _DirPlaceholder(os.PathLike): 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): +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): +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): +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(