mirror of http://git.sairate.top/sairate/doc.git
212 lines
7.3 KiB
Python
212 lines
7.3 KiB
Python
|
from __future__ import annotations
|
||
|
|
||
|
__version__ = "0.2.0"
|
||
|
|
||
|
import dataclasses
|
||
|
import datetime
|
||
|
import functools
|
||
|
import io
|
||
|
import logging
|
||
|
import os
|
||
|
import sys
|
||
|
import urllib.parse
|
||
|
from typing import IO, Any, BinaryIO, Collection, Mapping, Sequence
|
||
|
|
||
|
import yaml
|
||
|
|
||
|
from . import cache, yaml_util
|
||
|
|
||
|
log = logging.getLogger(f"mkdocs.{__name__}")
|
||
|
|
||
|
|
||
|
DEFAULT_PROJECTS_FILE = "https://raw.githubusercontent.com/mkdocs/catalog/main/projects.yaml"
|
||
|
|
||
|
BUILTIN_THEMES = {"mkdocs", "readthedocs"}
|
||
|
BUILTIN_PLUGINS = {"search"}
|
||
|
_BUILTIN_EXTENSIONS = "abbr admonition attr_list codehilite def_list extra fenced_code footnotes md_in_html meta nl2br sane_lists smarty tables toc wikilinks legacy_attrs legacy_em".split()
|
||
|
BUILTIN_EXTENSIONS = {
|
||
|
*_BUILTIN_EXTENSIONS,
|
||
|
*(f"markdown.extensions.{e}" for e in _BUILTIN_EXTENSIONS),
|
||
|
}
|
||
|
|
||
|
_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:
|
||
|
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(maxsize=None)
|
||
|
def _entry_points(group: str) -> Mapping[str, Any]:
|
||
|
if sys.version_info >= (3, 10):
|
||
|
from importlib.metadata import entry_points
|
||
|
else:
|
||
|
from importlib_metadata import entry_points
|
||
|
|
||
|
eps = {ep.name: ep for ep in entry_points(group=group)}
|
||
|
log.debug(f"Available '{group}' entry points: {sorted(eps)}")
|
||
|
return eps
|
||
|
|
||
|
|
||
|
@dataclasses.dataclass(frozen=True)
|
||
|
class _PluginKind:
|
||
|
projects_key: str
|
||
|
entry_points_key: str
|
||
|
|
||
|
def __str__(self) -> str:
|
||
|
return self.projects_key.rpartition("_")[-1]
|
||
|
|
||
|
|
||
|
def get_projects_file(path: str | None = None) -> BinaryIO:
|
||
|
if path is None:
|
||
|
path = DEFAULT_PROJECTS_FILE
|
||
|
if urllib.parse.urlsplit(path).scheme in ("http", "https"):
|
||
|
content = cache.download_and_cache_url(path, datetime.timedelta(days=1))
|
||
|
else:
|
||
|
with open(path, "rb") as f:
|
||
|
content = f.read()
|
||
|
return io.BytesIO(content)
|
||
|
|
||
|
|
||
|
def get_deps(
|
||
|
config_file: IO | os.PathLike | str | None = None,
|
||
|
projects_file: IO | None = None,
|
||
|
) -> Collection[str]:
|
||
|
"""
|
||
|
Print PyPI package dependencies inferred from a mkdocs.yml file based on a reverse mapping of known projects.
|
||
|
|
||
|
Args:
|
||
|
config_file: Non-default mkdocs.yml file - content as a buffer, or path.
|
||
|
projects_file: File/buffer that declares all known MkDocs-related projects.
|
||
|
The file is in YAML format and contains `projects: [{mkdocs_theme:, mkdocs_plugin:, markdown_extension:}]
|
||
|
"""
|
||
|
if config_file is None:
|
||
|
if os.path.isfile("mkdocs.yml"):
|
||
|
config_file = "mkdocs.yml"
|
||
|
elif os.path.isfile("mkdocs.yaml"):
|
||
|
config_file = "mkdocs.yaml"
|
||
|
else:
|
||
|
config_file = "mkdocs.yml"
|
||
|
opened_config_file: IO
|
||
|
if isinstance(config_file, (str, os.PathLike)):
|
||
|
config_file = os.path.abspath(config_file)
|
||
|
opened_config_file = open(config_file, "rb")
|
||
|
else:
|
||
|
opened_config_file = config_file
|
||
|
|
||
|
log.debug(f"Loading configuration file: {config_file}")
|
||
|
with opened_config_file:
|
||
|
cfg = yaml_util.yaml_load(opened_config_file)
|
||
|
if not isinstance(cfg, dict):
|
||
|
raise ValueError(
|
||
|
f"The configuration is invalid. Expected a key-value mapping but received {type(cfg)}"
|
||
|
)
|
||
|
|
||
|
packages_to_install = set()
|
||
|
|
||
|
if all(c not in cfg for c in ("site_name", "theme", "plugins", "markdown_extensions")):
|
||
|
log.warning(f"The file {config_file!r} doesn't seem to be a mkdocs.yml config file")
|
||
|
else:
|
||
|
if _dig(cfg, "theme.locale") not in (_NotFound, "en"):
|
||
|
packages_to_install.add("mkdocs[i18n]")
|
||
|
else:
|
||
|
packages_to_install.add("mkdocs")
|
||
|
|
||
|
try:
|
||
|
theme = cfg["theme"]["name"]
|
||
|
except (KeyError, TypeError):
|
||
|
theme = cfg.get("theme")
|
||
|
themes = {theme} if theme else set()
|
||
|
|
||
|
plugins = set(_strings(_dig(cfg, "plugins")))
|
||
|
extensions = set(_strings(_dig(cfg, "markdown_extensions")))
|
||
|
|
||
|
wanted_plugins = (
|
||
|
(_PluginKind("mkdocs_theme", "mkdocs.themes"), themes - BUILTIN_THEMES),
|
||
|
(_PluginKind("mkdocs_plugin", "mkdocs.plugins"), plugins - BUILTIN_PLUGINS),
|
||
|
(_PluginKind("markdown_extension", "markdown.extensions"), extensions - BUILTIN_EXTENSIONS),
|
||
|
)
|
||
|
for kind, wanted in wanted_plugins:
|
||
|
log.debug(f"Wanted {kind}s: {sorted(wanted)}")
|
||
|
|
||
|
if projects_file is None:
|
||
|
projects_file = get_projects_file()
|
||
|
with projects_file:
|
||
|
projects = yaml.load(projects_file, Loader=yaml_util.SafeLoader)["projects"]
|
||
|
|
||
|
for project in projects:
|
||
|
for kind, wanted in wanted_plugins:
|
||
|
available = _strings(project.get(kind.projects_key, ()))
|
||
|
for entry_name in available:
|
||
|
if ( # Also check theme-namespaced plugin names against the current theme.
|
||
|
"/" in entry_name
|
||
|
and theme is not None
|
||
|
and kind.projects_key == "mkdocs_plugin"
|
||
|
and entry_name.startswith(f"{theme}/")
|
||
|
and entry_name[len(theme) + 1 :] in wanted
|
||
|
and entry_name not in wanted
|
||
|
):
|
||
|
entry_name = entry_name[len(theme) + 1 :]
|
||
|
if entry_name in wanted:
|
||
|
if "pypi_id" in project:
|
||
|
install_name = project["pypi_id"]
|
||
|
elif "github_id" in project:
|
||
|
install_name = "git+https://github.com/{github_id}".format_map(project)
|
||
|
else:
|
||
|
log.error(
|
||
|
f"Can't find how to install {kind} '{entry_name}' although it was identified as {project}"
|
||
|
)
|
||
|
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:
|
||
|
for entry_name in sorted(wanted):
|
||
|
dist_name = None
|
||
|
ep = _entry_points(kind.entry_points_key).get(entry_name)
|
||
|
if ep is not None and ep.dist is not None:
|
||
|
dist_name = ep.dist.name
|
||
|
warning = (
|
||
|
f"{str(kind).capitalize()} '{entry_name}' is not provided by any registered project"
|
||
|
)
|
||
|
if ep is not None:
|
||
|
warning += " but is installed locally"
|
||
|
if dist_name:
|
||
|
warning += f" from '{dist_name}'"
|
||
|
log.info(warning)
|
||
|
else:
|
||
|
log.warning(warning)
|
||
|
|
||
|
return sorted(packages_to_install)
|