test bkp
This commit is contained in:
1 parent
6ccab0347b
commit
954a2fd6d6
18684 files changed
+601012
-190
No files matched your search
@@ -0,0 +1,211 @@
|
||||
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)
|
||||
@@ -0,0 +1,61 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import logging
|
||||
import sys
|
||||
|
||||
from . import get_deps, get_projects_file
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Show required PyPI packages inferred from plugins in mkdocs.yml."
|
||||
)
|
||||
parser.add_argument("-v", "--verbose", action="store_true", help="Enable verbose output")
|
||||
parser.add_argument(
|
||||
"-f",
|
||||
"--config-file",
|
||||
type=argparse.FileType("r"),
|
||||
help="Provide a specific MkDocs config. This can be a file name, or '-' to read from stdin.",
|
||||
default=None,
|
||||
)
|
||||
parser.add_argument(
|
||||
"-p",
|
||||
"--projects-file",
|
||||
help="URL or local path of the registry file that declares all known MkDocs-related projects.",
|
||||
default="https://raw.githubusercontent.com/mkdocs/catalog/main/projects.yaml",
|
||||
)
|
||||
|
||||
|
||||
class CountHandler(logging.NullHandler):
|
||||
warning_count = 0
|
||||
|
||||
def handle(self, record):
|
||||
rv = self.filter(record)
|
||||
if rv:
|
||||
self.warning_count += 1
|
||||
return rv
|
||||
|
||||
|
||||
def cli():
|
||||
args = parser.parse_args()
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.DEBUG if args.verbose else logging.INFO,
|
||||
format="%(levelname)s - mkdocs-get-deps: %(message)s",
|
||||
)
|
||||
|
||||
warning_counter = CountHandler()
|
||||
warning_counter.setLevel(logging.WARNING)
|
||||
logging.getLogger("mkdocs").addHandler(warning_counter)
|
||||
|
||||
with get_projects_file(args.projects_file) as projects_file:
|
||||
deps = get_deps(config_file=args.config_file, projects_file=projects_file)
|
||||
|
||||
for dep in deps:
|
||||
print(dep) # noqa: T201
|
||||
|
||||
if warning_counter.warning_count:
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
cli()
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -0,0 +1,70 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import datetime
|
||||
import hashlib
|
||||
import logging
|
||||
import os
|
||||
import random
|
||||
import urllib.request
|
||||
from typing import Callable
|
||||
|
||||
import platformdirs
|
||||
|
||||
from . import __version__
|
||||
|
||||
log = logging.getLogger(f"mkdocs.{__name__}")
|
||||
|
||||
|
||||
def _download_url(url: str) -> bytes:
|
||||
req = urllib.request.Request(url, headers={"User-Agent": f"mkdocs-get-deps/{__version__}"})
|
||||
with urllib.request.urlopen(req) as resp:
|
||||
return resp.read()
|
||||
|
||||
|
||||
def download_and_cache_url(
|
||||
url: str,
|
||||
cache_duration: datetime.timedelta,
|
||||
*,
|
||||
download: Callable[[str], bytes] = _download_url,
|
||||
comment: bytes = b"# ",
|
||||
) -> bytes:
|
||||
"""Downloads a file from the URL, stores it under ~/.cache/, and returns its content.
|
||||
|
||||
For tracking the age of the content, a prefix is inserted into the stored file, rather than relying on mtime.
|
||||
|
||||
Args:
|
||||
url: URL to use.
|
||||
download: Callback that will accept the URL and actually perform the download.
|
||||
cache_duration: How long to consider the URL content cached.
|
||||
comment: The appropriate comment prefix for this file format.
|
||||
"""
|
||||
directory = os.path.join(platformdirs.user_cache_dir("mkdocs"), "mkdocs_url_cache")
|
||||
name_hash = hashlib.sha256(url.encode()).hexdigest()[:32]
|
||||
path = os.path.join(directory, name_hash + os.path.splitext(url)[1])
|
||||
|
||||
now = int(datetime.datetime.now(datetime.timezone.utc).timestamp())
|
||||
prefix = b"%s%s downloaded at timestamp " % (comment, url.encode())
|
||||
# Check for cached file and try to return it
|
||||
if os.path.isfile(path):
|
||||
try:
|
||||
with open(path, "rb") as f:
|
||||
line = f.readline()
|
||||
if line.startswith(prefix):
|
||||
line = line[len(prefix) :]
|
||||
timestamp = int(line)
|
||||
if datetime.timedelta(seconds=(now - timestamp)) <= cache_duration:
|
||||
log.debug(f"Using cached '{path}' for '{url}'")
|
||||
return f.read()
|
||||
except (OSError, ValueError) as e:
|
||||
log.debug(f"{type(e).__name__}: {e}")
|
||||
|
||||
# Download and cache the file
|
||||
log.debug(f"Downloading '{url}' to '{path}'")
|
||||
content = download(url)
|
||||
os.makedirs(directory, exist_ok=True)
|
||||
temp_filename = f"{path}.{random.randrange(1 << 32):08x}.part"
|
||||
with open(temp_filename, "wb") as f:
|
||||
f.write(b"%s%d\n" % (prefix, now))
|
||||
f.write(content)
|
||||
os.replace(temp_filename, path)
|
||||
return content
|
||||
Whitespace-only changes.
@@ -0,0 +1,50 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import os
|
||||
import os.path
|
||||
from typing import IO, Any
|
||||
|
||||
import mergedeep # type: ignore
|
||||
import yaml
|
||||
|
||||
try:
|
||||
from yaml import CSafeLoader as SafeLoader
|
||||
except ImportError:
|
||||
from yaml import SafeLoader # type: ignore
|
||||
|
||||
log = logging.getLogger(f"mkdocs.{__name__}")
|
||||
|
||||
|
||||
class YamlLoader(SafeLoader):
|
||||
pass
|
||||
|
||||
|
||||
# Prevent errors from trying to access external modules which may not be installed yet.
|
||||
YamlLoader.add_constructor("!ENV", lambda loader, node: None) # type: ignore
|
||||
YamlLoader.add_constructor("!relative", lambda loader, node: None) # type: ignore
|
||||
YamlLoader.add_multi_constructor(
|
||||
"tag:yaml.org,2002:python/name:", lambda loader, suffix, node: None
|
||||
)
|
||||
YamlLoader.add_multi_constructor(
|
||||
"tag:yaml.org,2002:python/object/apply:", lambda loader, suffix, node: None
|
||||
)
|
||||
|
||||
|
||||
def yaml_load(source: IO | str) -> dict[str, Any]:
|
||||
"""Return dict of source YAML file using loader, recursively deep merging inherited parent."""
|
||||
result = yaml.load(source, Loader=YamlLoader)
|
||||
if result is None:
|
||||
return {}
|
||||
if (
|
||||
"INHERIT" in result
|
||||
and not isinstance(source, str)
|
||||
and getattr(source, "name", None) is not None
|
||||
):
|
||||
relpath = result.pop("INHERIT")
|
||||
abspath = os.path.normpath(os.path.join(os.path.dirname(source.name), relpath))
|
||||
log.debug(f"Loading inherited configuration file: {abspath}")
|
||||
with open(abspath, "rb") as f:
|
||||
parent = yaml_load(f)
|
||||
result = mergedeep.merge(parent, result)
|
||||
return result
|
||||
Reference in new issue
Block a user