From e175070243ec3a531ea5688550f50c6e07f6e237 Mon Sep 17 00:00:00 2001 From: Oleh Prypin Date: Sun, 4 Jun 2023 23:22:56 +0200 Subject: [PATCH] Let plugins declare their own optional dependencies This is inferred from particular config keys The current known plugin in need of this is mkdocstrings (handlers) --- mkdocs/commands/get_deps.py | 57 +++++++++++++++++++++++++------------ 1 file changed, 39 insertions(+), 18 deletions(-) diff --git a/mkdocs/commands/get_deps.py b/mkdocs/commands/get_deps.py index b84ae519..6cc07875 100644 --- a/mkdocs/commands/get_deps.py +++ b/mkdocs/commands/get_deps.py @@ -4,7 +4,7 @@ import dataclasses import datetime import functools import logging -from typing import Iterator, Mapping +from typing import Mapping, Sequence import yaml @@ -15,20 +15,39 @@ from mkdocs.utils.cache import download_and_cache_url log = logging.getLogger(__name__) +# Note: do not rely on functions in this module, it is not public API. -def _extract_names(cfg, key: str) -> Iterator[str]: - """Get names of plugins/extensions from the config - in either a list of dicts or a dict.""" +NotFound = () + + +def dig(cfg, keys: str): + """Receives a string such as 'foo.bar' and returns `cfg['foo']['bar']`, or `NotFound`. + + A list of single-item dicts gets converted to a flat dict. This is intended for `plugins` config. + """ + key, _, rest = keys.partition('.') try: - items = iter(cfg.get(key, ())) - except TypeError: - log.error(f"Invalid config entry '{key}'") - for item in items: - try: - if not isinstance(item, str): - [item] = item - yield item - except (ValueError, TypeError): - log.error(f"Invalid config entry '{key}': {item}") + cfg = cfg[key] + except (KeyError, TypeError): + return NotFound + if isinstance(cfg, list): + orig_cfg = cfg + cfg = {} + for item in reversed(orig_cfg): + if isinstance(item, dict) and len(item) == 1: + cfg.update(item) + elif isinstance(item, str): + cfg[item] = {} + if not rest: + return cfg + return dig(cfg, rest) + + +def strings(obj) -> Sequence[str]: + if isinstance(obj, str): + return (obj,) + else: + return tuple(obj) @functools.lru_cache() @@ -68,8 +87,8 @@ def get_deps(projects_file_url: str, config_file_path: str | None = None) -> Non theme = cfg.get('theme') themes = {theme} if theme else set() - plugins = set(_extract_names(cfg, 'plugins')) - extensions = set(_extract_names(cfg, 'markdown_extensions')) + plugins = set(strings(dig(cfg, 'plugins'))) + extensions = set(strings(dig(cfg, 'markdown_extensions'))) wanted_plugins = ( (PluginKind('mkdocs_theme', 'mkdocs.themes'), themes - {'mkdocs', 'readthedocs'}), @@ -85,9 +104,7 @@ def get_deps(projects_file_url: str, config_file_path: str | None = None) -> Non packages_to_install = set() for project in projects: for kind, wanted in wanted_plugins: - available = project.get(kind.projects_key, ()) - if isinstance(available, str): - available = (available,) + available = strings(project.get(kind.projects_key, ())) for entry_name in available: if entry_name in wanted or ( # Also check theme-namespaced plugin names against the current theme. @@ -107,6 +124,10 @@ def get_deps(projects_file_url: str, config_file_path: str | None = None) -> Non ) continue packages_to_install.add(install_name) + for extra_key, extra_pkgs in project.get('extra_dependencies', {}).items(): + if dig(cfg, extra_key) is not NotFound: + packages_to_install.update(strings(extra_pkgs)) + wanted.remove(entry_name) for kind, wanted in wanted_plugins: