mirror of
https://github.com/zensical/zensical.git
synced 2026-07-08 17:31:06 +00:00
refactor: reorganize config module
Signed-off-by: Timothée Mazzucotelli <dev@pawamoy.fr>
This commit is contained in:
committed by
GitHub
parent
84adc6f290
commit
a77087a455
+293
-262
@@ -163,6 +163,101 @@ def get_config() -> dict:
|
||||
return _CONFIG # ty:ignore[invalid-return-type]
|
||||
|
||||
|
||||
def _yaml_load(source: IO) -> dict[str, Any]:
|
||||
"""Load configuration file, resolve environment variables and parent files.
|
||||
|
||||
Note that INHERIT is only a bandaid that was introduced to allow for some
|
||||
degree of modularity, but with serious shortcomings. Zensical will use a
|
||||
different approach in the future, which will allow for composable and
|
||||
environment-specific configuration.
|
||||
"""
|
||||
Loader.add_constructor("!ENV", _construct_env_tag)
|
||||
try:
|
||||
config = yaml.load(
|
||||
# Compatibility shim: we remap Material's extension namespace to
|
||||
# Zensical's, and the now deprecated materialx namespace as well
|
||||
source.read()
|
||||
.replace("material.extensions", "zensical.extensions")
|
||||
.replace("materialx", "zensical.extensions"),
|
||||
Loader=Loader, # noqa: S506
|
||||
)
|
||||
except YAMLError as e:
|
||||
raise ConfigurationError(
|
||||
f"Encountered an error parsing the configuration file: {e}"
|
||||
) from e
|
||||
if config is None:
|
||||
return {}
|
||||
|
||||
# Try to resolve inherited configuration file
|
||||
if "INHERIT" in config and not isinstance(source, str):
|
||||
relpath = config.pop("INHERIT")
|
||||
abspath = os.path.normpath(
|
||||
os.path.join(os.path.dirname(source.name), relpath)
|
||||
)
|
||||
if not os.path.exists(abspath):
|
||||
raise ConfigurationError(
|
||||
f"Inherited config file '{relpath}' "
|
||||
f"doesn't exist at '{abspath}'."
|
||||
)
|
||||
with open(abspath, encoding="utf-8") as fd:
|
||||
parent = _yaml_load(fd)
|
||||
config = always_merger.merge(parent, config)
|
||||
|
||||
# Return resulting configuration
|
||||
return config
|
||||
|
||||
|
||||
def _construct_env_tag(
|
||||
loader: yaml.Loader,
|
||||
node: yaml.ScalarNode | yaml.SequenceNode | yaml.MappingNode,
|
||||
) -> Any:
|
||||
"""Assign value of ENV variable referenced at node.
|
||||
|
||||
MkDocs supports the use of !ENV to reference environment variables in YAML
|
||||
configuration files. We won't likely support this in Zensical, but for now
|
||||
we need it to build MkDocs projects. Zensical will use a different approach
|
||||
to create environment-specific configuration in the future.
|
||||
|
||||
Licensed under MIT
|
||||
Copyright (c) 2020 Waylan Limberg
|
||||
Taken and adapted from
|
||||
https://github.com/waylan/pyyaml-env-tag/blob/master/yaml_env_tag.py
|
||||
"""
|
||||
default = None
|
||||
|
||||
# Handle !ENV <name>
|
||||
if isinstance(node, yaml.nodes.ScalarNode):
|
||||
vars = [loader.construct_scalar(node)]
|
||||
|
||||
# Handle !ENV [<name>, <fallback>]
|
||||
elif isinstance(node, yaml.nodes.SequenceNode):
|
||||
child_nodes = node.value
|
||||
if len(child_nodes) > 1:
|
||||
default = loader.construct_object(child_nodes[-1])
|
||||
child_nodes = child_nodes[:-1]
|
||||
# Env Vars are resolved as string values, ignoring (implicit) types.
|
||||
vars = [loader.construct_scalar(child) for child in child_nodes]
|
||||
else:
|
||||
raise ConstructorError(
|
||||
context=f"expected a scalar or sequence node, but found {node.id}",
|
||||
context_mark=node.start_mark,
|
||||
)
|
||||
|
||||
# Resolve environment variable
|
||||
for var in vars:
|
||||
if var in os.environ:
|
||||
value = os.environ[var]
|
||||
# Resolve value to Python type using YAML's implicit resolvers
|
||||
tag = loader.resolve(yaml.nodes.ScalarNode, value, (True, False))
|
||||
return loader.construct_object(yaml.nodes.ScalarNode(tag, value))
|
||||
|
||||
# Otherwise return default
|
||||
return default
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
|
||||
|
||||
@functools.cache
|
||||
def get_themes() -> dict[str, EntryPoint]:
|
||||
"""Return available themes.
|
||||
@@ -218,6 +313,49 @@ def get_custom_theme_dir(path: str, config_path: str) -> str:
|
||||
return theme_dir
|
||||
|
||||
|
||||
def _yaml_load_theme_config(source: IO) -> dict[str, Any]:
|
||||
Loader.add_constructor("!ENV", _construct_env_tag)
|
||||
try:
|
||||
config = yaml.load(source, Loader=Loader) # noqa: S506
|
||||
except YAMLError as e:
|
||||
raise ConfigurationError(
|
||||
f"Encountered an error parsing the theme configuration file: {e}"
|
||||
) from e
|
||||
if config is None:
|
||||
return {}
|
||||
return config
|
||||
|
||||
|
||||
def _load_theme_config(theme_dir: str) -> tuple[dict[str, Any], list[str]]:
|
||||
# Keep track of the theme directories,
|
||||
# so that we can pass them up to Minijinja.
|
||||
theme_dirs = [theme_dir]
|
||||
|
||||
if (theme_config_file := Path(theme_dir, "mkdocs_theme.yml")).is_file():
|
||||
# We found a theme configuration file (mkdocs_theme.yml).
|
||||
with theme_config_file.open(encoding="utf-8") as file:
|
||||
theme_config = _yaml_load_theme_config(file)
|
||||
if extends := theme_config.pop("extends", None):
|
||||
# This theme extends another theme,
|
||||
# so we load this parent theme's configuration (recursively)
|
||||
# and merge the current theme's configuration into it.
|
||||
parent_theme_dir = get_theme_dir(extends)
|
||||
parent_config, parent_dirs = _load_theme_config(
|
||||
parent_theme_dir
|
||||
)
|
||||
parent_config.update(theme_config)
|
||||
theme_dirs.extend(parent_dirs)
|
||||
return parent_config, theme_dirs
|
||||
# Otherwise we just return the current theme configuration.
|
||||
return theme_config, theme_dirs
|
||||
|
||||
# No theme configuration.
|
||||
return {}, theme_dirs
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _apply_defaults(config: dict, path: str) -> dict:
|
||||
"""Apply default settings in configuration.
|
||||
|
||||
@@ -506,56 +644,11 @@ def _apply_defaults(config: dict, path: str) -> dict:
|
||||
# to support the same for when we load TOML. This is a bandaid, and we will
|
||||
# find a better solution, once we work on configuration management, but for
|
||||
# now this should be sufficient.
|
||||
emoji = config["mdx_configs"].get("pymdownx.emoji", {})
|
||||
if isinstance(emoji.get("emoji_generator"), str):
|
||||
emoji["emoji_generator"] = _resolve(emoji.get("emoji_generator"))
|
||||
if isinstance(emoji.get("emoji_index"), str):
|
||||
emoji["emoji_index"] = _resolve(emoji.get("emoji_index"))
|
||||
|
||||
# Tabbed extension configuration - resolve slugification function
|
||||
tabbed = config["mdx_configs"].get("pymdownx.tabbed", {})
|
||||
if isinstance(tabbed.get("slugify"), dict):
|
||||
object = tabbed["slugify"].get("object", "pymdownx.slugs.slugify")
|
||||
tabbed["slugify"] = _resolve(object)(
|
||||
**tabbed["slugify"].get("kwds", {})
|
||||
)
|
||||
|
||||
# Blocks-tab extension configuration - resolve slugification function
|
||||
blocks_tab = config["mdx_configs"].get("pymdownx.blocks.tab", {})
|
||||
if isinstance(blocks_tab.get("slugify"), dict):
|
||||
object = blocks_tab["slugify"].get("object", "pymdownx.slugs.slugify")
|
||||
blocks_tab["slugify"] = _resolve(object)(
|
||||
**blocks_tab["slugify"].get("kwds", {})
|
||||
)
|
||||
|
||||
# Table of contents extension configuration - resolve slugification function
|
||||
toc = config["mdx_configs"]["toc"]
|
||||
if isinstance(toc.get("slugify"), dict):
|
||||
object = toc["slugify"].get("object", "pymdownx.slugs.slugify")
|
||||
toc["slugify"] = _resolve(object)(**toc["slugify"].get("kwds", {}))
|
||||
|
||||
# Superfences extension configuration - resolve format function
|
||||
superfences = config["mdx_configs"].get("pymdownx.superfences", {})
|
||||
for fence in superfences.get("custom_fences", []):
|
||||
if isinstance(fence.get("format"), str):
|
||||
fence["format"] = _resolve(fence.get("format"))
|
||||
elif isinstance(fence.get("format"), dict):
|
||||
object = fence["format"].get(
|
||||
"object", "pymdownx.superfences.fence_code_format"
|
||||
)
|
||||
fence["format"] = _resolve(object)(
|
||||
**fence["format"].get("kwds", {})
|
||||
)
|
||||
if isinstance(fence.get("validator"), str):
|
||||
fence["validator"] = _resolve(fence.get("validator"))
|
||||
elif isinstance(fence.get("validator"), dict):
|
||||
object = fence["validator"].get("object")
|
||||
callable_object = (
|
||||
_resolve(object) if object else lambda *args, **kwargs: True
|
||||
)
|
||||
fence["validator"] = callable_object(
|
||||
**fence["validator"].get("kwds", {})
|
||||
)
|
||||
_resolve_pymdownx_emoji(config)
|
||||
_resolve_pymdownx_superfences(config)
|
||||
_resolve_pymdownx_tabbed(config)
|
||||
_resolve_pymdownx_blocks_tab(config)
|
||||
_resolve_toc(config)
|
||||
|
||||
# Ensure the table of contents title is initialized, as it's used inside
|
||||
# the template, and the table of contents extension is always defined
|
||||
@@ -565,43 +658,10 @@ def _apply_defaults(config: dict, path: str) -> dict:
|
||||
# Convert plugins configuration
|
||||
config["plugins"] = _convert_plugins(config.get("plugins", []), config)
|
||||
|
||||
# Set up mkdocstrings, which touches plugins and Markdown extensions
|
||||
if "mkdocstrings" in config["plugins"]:
|
||||
mkdocstrings_config = config["plugins"]["mkdocstrings"]["config"]
|
||||
if mkdocstrings_config.get("enabled", True):
|
||||
if not find_spec("mkdocstrings"):
|
||||
raise ConfigurationError(
|
||||
"mkdocstrings plugin is enabled, but mkdocstrings is not "
|
||||
"installed. Please install mkdocstrings or disable the "
|
||||
"plugin."
|
||||
)
|
||||
if autorefs := get_autorefs_extension():
|
||||
config["markdown_extensions"].append(autorefs)
|
||||
mkdocstrings = get_mkdocstrings_extension(config, path)
|
||||
config["markdown_extensions"].append(mkdocstrings)
|
||||
|
||||
# Support autorefs in standalone mode
|
||||
elif "autorefs" in config["plugins"]:
|
||||
if autorefs := get_autorefs_extension():
|
||||
config["markdown_extensions"].append(autorefs)
|
||||
else:
|
||||
raise ConfigurationError(
|
||||
"autorefs plugin is enabled, but autorefs is not "
|
||||
"installed. Please install autorefs or disable the "
|
||||
"plugin."
|
||||
)
|
||||
|
||||
# Map glightbox plugin configuration to the extension configuration
|
||||
if "glightbox" in config["plugins"]:
|
||||
plugin = config["plugins"]["glightbox"]["config"]
|
||||
config["markdown_extensions"].append(GlightboxExtension.name)
|
||||
config["mdx_configs"][GlightboxExtension.name] = plugin
|
||||
|
||||
# Map macros plugin configuration to the extension configuration
|
||||
if "macros" in config["plugins"]:
|
||||
plugin = config["plugins"]["macros"]["config"]
|
||||
config["markdown_extensions"].append(MacrosExtension.name)
|
||||
config["mdx_configs"][MacrosExtension.name] = plugin
|
||||
# Map plugins configuration to Markdown extensions
|
||||
_shim_mkdocstrings_autorefs(config, path)
|
||||
_shim_glightbox(config)
|
||||
_shim_macros(config)
|
||||
|
||||
# List files along with their hashes, so we can rebuild when they change
|
||||
config["watched_files"] = sorted(
|
||||
@@ -656,6 +716,128 @@ def _hash(data: Any) -> int:
|
||||
return int(hash.hexdigest(), 16) % (2**64)
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _resolve(symbol: str) -> Any:
|
||||
"""Resolve a symbol to its corresponding Python object."""
|
||||
module_path, func_name = symbol.rsplit(".", 1)
|
||||
module = importlib.import_module(module_path)
|
||||
return getattr(module, func_name)
|
||||
|
||||
|
||||
def _resolve_pymdownx_emoji(config: dict[str, Any]) -> None:
|
||||
# Emoji extension: resolve emoji generator and index functions
|
||||
emoji = config["mdx_configs"].get("pymdownx.emoji", {})
|
||||
if isinstance(emoji.get("emoji_generator"), str):
|
||||
emoji["emoji_generator"] = _resolve(emoji.get("emoji_generator"))
|
||||
if isinstance(emoji.get("emoji_index"), str):
|
||||
emoji["emoji_index"] = _resolve(emoji.get("emoji_index"))
|
||||
|
||||
|
||||
def _resolve_pymdownx_tabbed(config: dict[str, Any]) -> None:
|
||||
# Tabbed extension: resolve slugification function
|
||||
tabbed = config["mdx_configs"].get("pymdownx.tabbed", {})
|
||||
if isinstance(tabbed.get("slugify"), dict):
|
||||
object = tabbed["slugify"].get("object", "pymdownx.slugs.slugify")
|
||||
tabbed["slugify"] = _resolve(object)(
|
||||
**tabbed["slugify"].get("kwds", {})
|
||||
)
|
||||
|
||||
|
||||
def _resolve_pymdownx_blocks_tab(config: dict[str, Any]) -> None:
|
||||
# Blocks-tab extension: resolve slugification function
|
||||
blocks_tab = config["mdx_configs"].get("pymdownx.blocks.tab", {})
|
||||
if isinstance(blocks_tab.get("slugify"), dict):
|
||||
object = blocks_tab["slugify"].get("object", "pymdownx.slugs.slugify")
|
||||
blocks_tab["slugify"] = _resolve(object)(
|
||||
**blocks_tab["slugify"].get("kwds", {})
|
||||
)
|
||||
|
||||
|
||||
def _resolve_pymdownx_superfences(config: dict[str, Any]) -> None:
|
||||
# Superfences extension: resolve format and validator functions
|
||||
superfences = config["mdx_configs"].get("pymdownx.superfences", {})
|
||||
for fence in superfences.get("custom_fences", []):
|
||||
if isinstance(fence.get("format"), str):
|
||||
fence["format"] = _resolve(fence.get("format"))
|
||||
elif isinstance(fence.get("format"), dict):
|
||||
object = fence["format"].get(
|
||||
"object", "pymdownx.superfences.fence_code_format"
|
||||
)
|
||||
fence["format"] = _resolve(object)(
|
||||
**fence["format"].get("kwds", {})
|
||||
)
|
||||
if isinstance(fence.get("validator"), str):
|
||||
fence["validator"] = _resolve(fence.get("validator"))
|
||||
elif isinstance(fence.get("validator"), dict):
|
||||
object = fence["validator"].get("object")
|
||||
callable_object = (
|
||||
_resolve(object) if object else lambda *args, **kwargs: True
|
||||
)
|
||||
fence["validator"] = callable_object(
|
||||
**fence["validator"].get("kwds", {})
|
||||
)
|
||||
|
||||
|
||||
def _resolve_toc(config: dict[str, Any]) -> None:
|
||||
# Table of contents extension: resolve slugification function
|
||||
toc = config["mdx_configs"]["toc"]
|
||||
if isinstance(toc.get("slugify"), dict):
|
||||
object = toc["slugify"].get("object", "pymdownx.slugs.slugify")
|
||||
toc["slugify"] = _resolve(object)(**toc["slugify"].get("kwds", {}))
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _shim_mkdocstrings_autorefs(config: dict[str, Any], path: str) -> None:
|
||||
# Set up mkdocstrings, which touches plugins and Markdown extensions
|
||||
if "mkdocstrings" in config["plugins"]:
|
||||
mkdocstrings_config = config["plugins"]["mkdocstrings"]["config"]
|
||||
if mkdocstrings_config.get("enabled", True):
|
||||
if not find_spec("mkdocstrings"):
|
||||
raise ConfigurationError(
|
||||
"mkdocstrings plugin is enabled, but mkdocstrings is not "
|
||||
"installed. Please install mkdocstrings or disable the "
|
||||
"plugin."
|
||||
)
|
||||
if autorefs := get_autorefs_extension():
|
||||
config["markdown_extensions"].append(autorefs)
|
||||
mkdocstrings = get_mkdocstrings_extension(config, path)
|
||||
config["markdown_extensions"].append(mkdocstrings)
|
||||
|
||||
# Support autorefs in standalone mode
|
||||
elif "autorefs" in config["plugins"]:
|
||||
if autorefs := get_autorefs_extension():
|
||||
config["markdown_extensions"].append(autorefs)
|
||||
else:
|
||||
raise ConfigurationError(
|
||||
"autorefs plugin is enabled, but autorefs is not "
|
||||
"installed. Please install autorefs or disable the "
|
||||
"plugin."
|
||||
)
|
||||
|
||||
|
||||
def _shim_glightbox(config: dict[str, Any]) -> None:
|
||||
# Map glightbox plugin configuration to the extension configuration
|
||||
if "glightbox" in config["plugins"]:
|
||||
plugin = config["plugins"]["glightbox"]["config"]
|
||||
config["markdown_extensions"].append(GlightboxExtension.name)
|
||||
config["mdx_configs"][GlightboxExtension.name] = plugin
|
||||
|
||||
|
||||
def _shim_macros(config: dict[str, Any]) -> None:
|
||||
# Map macros plugin configuration to the extension configuration
|
||||
if "macros" in config["plugins"]:
|
||||
plugin = config["plugins"]["macros"]["config"]
|
||||
config["markdown_extensions"].append(MacrosExtension.name)
|
||||
config["mdx_configs"][MacrosExtension.name] = plugin
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _ignore_directory(dirpath: str) -> bool:
|
||||
"""Determine whether to ignore a folder based on its name."""
|
||||
return dirpath == "__pycache__" or dirpath.startswith(".venv")
|
||||
@@ -800,37 +982,14 @@ def _list_templates(config: dict) -> list[tuple[str, int]]:
|
||||
return sorted(files_with_mtime)
|
||||
|
||||
|
||||
def _convert_extra(data: dict | list) -> dict | list:
|
||||
"""Recursively convert None values in a dictionary/list to empty strings."""
|
||||
if isinstance(data, dict):
|
||||
# Process each key-value pair in the dictionary
|
||||
return {
|
||||
key: _convert_extra(value)
|
||||
if isinstance(value, (dict, list))
|
||||
else ("" if value is None else value)
|
||||
for key, value in data.items()
|
||||
}
|
||||
if isinstance(data, list):
|
||||
# Process each item in the list
|
||||
return [
|
||||
_convert_extra(item)
|
||||
if isinstance(item, (dict, list))
|
||||
else ("" if item is None else item)
|
||||
for item in data
|
||||
]
|
||||
return data
|
||||
|
||||
|
||||
def _resolve(symbol: str) -> Any:
|
||||
"""Resolve a symbol to its corresponding Python object."""
|
||||
module_path, func_name = symbol.rsplit(".", 1)
|
||||
module = importlib.import_module(module_path)
|
||||
return getattr(module, func_name)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _is_index(path: str) -> bool:
|
||||
"""Returns, whether the given path points to a section index."""
|
||||
return os.path.basename(path) in ("index.md", "README.md")
|
||||
|
||||
|
||||
def _convert_nav(nav: list) -> list:
|
||||
"""Convert MkDocs navigation."""
|
||||
return [_convert_nav_item(entry) for entry in nav]
|
||||
@@ -884,12 +1043,25 @@ def _convert_nav_item(item: str | dict | list) -> dict | list:
|
||||
raise TypeError(f"Unknown nav item type: {type(item)}")
|
||||
|
||||
|
||||
def _is_index(path: str) -> bool:
|
||||
"""Returns, whether the given path points to a section index."""
|
||||
return os.path.basename(path) in ("index.md", "README.md")
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def _convert_extra(data: dict | list) -> dict | list:
|
||||
"""Recursively convert None values in a dictionary/list to empty strings."""
|
||||
if isinstance(data, dict):
|
||||
# Process each key-value pair in the dictionary
|
||||
return {
|
||||
key: _convert_extra(value)
|
||||
if isinstance(value, (dict, list))
|
||||
else ("" if value is None else value)
|
||||
for key, value in data.items()
|
||||
}
|
||||
if isinstance(data, list):
|
||||
# Process each item in the list
|
||||
return [
|
||||
_convert_extra(item)
|
||||
if isinstance(item, (dict, list))
|
||||
else ("" if item is None else item)
|
||||
for item in data
|
||||
]
|
||||
return data
|
||||
|
||||
|
||||
def _convert_extra_javascript(value: list) -> list:
|
||||
@@ -914,9 +1086,6 @@ def _convert_extra_javascript(value: list) -> list:
|
||||
return value
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _convert_markdown_extensions(value: Any) -> tuple[list[str], dict]:
|
||||
"""Convert Markdown extensions to what Python Markdown expects."""
|
||||
markdown_extensions = ["toc", "tables"]
|
||||
@@ -967,9 +1136,6 @@ def _convert_markdown_extensions(value: Any) -> tuple[list[str], dict]:
|
||||
return list(set(markdown_extensions)), mdx_configs
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _convert_plugins(value: Any, config: dict) -> dict:
|
||||
"""Convert plugins configuration to something we can work with."""
|
||||
plugins = {}
|
||||
@@ -1048,138 +1214,3 @@ def _convert_plugins(value: Any, config: dict) -> dict:
|
||||
|
||||
# Return plugins
|
||||
return plugins
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _yaml_load(source: IO) -> dict[str, Any]:
|
||||
"""Load configuration file, resolve environment variables and parent files.
|
||||
|
||||
Note that INHERIT is only a bandaid that was introduced to allow for some
|
||||
degree of modularity, but with serious shortcomings. Zensical will use a
|
||||
different approach in the future, which will allow for composable and
|
||||
environment-specific configuration.
|
||||
"""
|
||||
Loader.add_constructor("!ENV", _construct_env_tag)
|
||||
try:
|
||||
config = yaml.load(
|
||||
# Compatibility shim: we remap Material's extension namespace to
|
||||
# Zensical's, and the now deprecated materialx namespace as well
|
||||
source.read()
|
||||
.replace("material.extensions", "zensical.extensions")
|
||||
.replace("materialx", "zensical.extensions"),
|
||||
Loader=Loader, # noqa: S506
|
||||
)
|
||||
except YAMLError as e:
|
||||
raise ConfigurationError(
|
||||
f"Encountered an error parsing the configuration file: {e}"
|
||||
) from e
|
||||
if config is None:
|
||||
return {}
|
||||
|
||||
# Try to resolve inherited configuration file
|
||||
if "INHERIT" in config and not isinstance(source, str):
|
||||
relpath = config.pop("INHERIT")
|
||||
abspath = os.path.normpath(
|
||||
os.path.join(os.path.dirname(source.name), relpath)
|
||||
)
|
||||
if not os.path.exists(abspath):
|
||||
raise ConfigurationError(
|
||||
f"Inherited config file '{relpath}' "
|
||||
f"doesn't exist at '{abspath}'."
|
||||
)
|
||||
with open(abspath, encoding="utf-8") as fd:
|
||||
parent = _yaml_load(fd)
|
||||
config = always_merger.merge(parent, config)
|
||||
|
||||
# Return resulting configuration
|
||||
return config
|
||||
|
||||
|
||||
def _yaml_load_theme_config(source: IO) -> dict[str, Any]:
|
||||
Loader.add_constructor("!ENV", _construct_env_tag)
|
||||
try:
|
||||
config = yaml.load(source, Loader=Loader) # noqa: S506
|
||||
except YAMLError as e:
|
||||
raise ConfigurationError(
|
||||
f"Encountered an error parsing the theme configuration file: {e}"
|
||||
) from e
|
||||
if config is None:
|
||||
return {}
|
||||
return config
|
||||
|
||||
|
||||
def _load_theme_config(theme_dir: str) -> tuple[dict[str, Any], list[str]]:
|
||||
# Keep track of the theme directories,
|
||||
# so that we can pass them up to Minijinja.
|
||||
theme_dirs = [theme_dir]
|
||||
|
||||
if (theme_config_file := Path(theme_dir, "mkdocs_theme.yml")).is_file():
|
||||
# We found a theme configuration file (mkdocs_theme.yml).
|
||||
with theme_config_file.open(encoding="utf-8") as file:
|
||||
theme_config = _yaml_load_theme_config(file)
|
||||
if extends := theme_config.pop("extends", None):
|
||||
# This theme extends another theme,
|
||||
# so we load this parent theme's configuration (recursively)
|
||||
# and merge the current theme's configuration into it.
|
||||
parent_theme_dir = get_theme_dir(extends)
|
||||
parent_config, parent_dirs = _load_theme_config(
|
||||
parent_theme_dir
|
||||
)
|
||||
parent_config.update(theme_config)
|
||||
theme_dirs.extend(parent_dirs)
|
||||
return parent_config, theme_dirs
|
||||
# Otherwise we just return the current theme configuration.
|
||||
return theme_config, theme_dirs
|
||||
|
||||
# No theme configuration.
|
||||
return {}, theme_dirs
|
||||
|
||||
|
||||
def _construct_env_tag(
|
||||
loader: yaml.Loader,
|
||||
node: yaml.ScalarNode | yaml.SequenceNode | yaml.MappingNode,
|
||||
) -> Any:
|
||||
"""Assign value of ENV variable referenced at node.
|
||||
|
||||
MkDocs supports the use of !ENV to reference environment variables in YAML
|
||||
configuration files. We won't likely support this in Zensical, but for now
|
||||
we need it to build MkDocs projects. Zensical will use a different approach
|
||||
to create environment-specific configuration in the future.
|
||||
|
||||
Licensed under MIT
|
||||
Copyright (c) 2020 Waylan Limberg
|
||||
Taken and adapted from
|
||||
https://github.com/waylan/pyyaml-env-tag/blob/master/yaml_env_tag.py
|
||||
"""
|
||||
default = None
|
||||
|
||||
# Handle !ENV <name>
|
||||
if isinstance(node, yaml.nodes.ScalarNode):
|
||||
vars = [loader.construct_scalar(node)]
|
||||
|
||||
# Handle !ENV [<name>, <fallback>]
|
||||
elif isinstance(node, yaml.nodes.SequenceNode):
|
||||
child_nodes = node.value
|
||||
if len(child_nodes) > 1:
|
||||
default = loader.construct_object(child_nodes[-1])
|
||||
child_nodes = child_nodes[:-1]
|
||||
# Env Vars are resolved as string values, ignoring (implicit) types.
|
||||
vars = [loader.construct_scalar(child) for child in child_nodes]
|
||||
else:
|
||||
raise ConstructorError(
|
||||
context=f"expected a scalar or sequence node, but found {node.id}",
|
||||
context_mark=node.start_mark,
|
||||
)
|
||||
|
||||
# Resolve environment variable
|
||||
for var in vars:
|
||||
if var in os.environ:
|
||||
value = os.environ[var]
|
||||
# Resolve value to Python type using YAML's implicit resolvers
|
||||
tag = loader.resolve(yaml.nodes.ScalarNode, value, (True, False))
|
||||
return loader.construct_object(yaml.nodes.ScalarNode(tag, value))
|
||||
|
||||
# Otherwise return default
|
||||
return default
|
||||
|
||||
Reference in New Issue
Block a user