diff --git a/check_projects.py b/check_projects.py index ca449f2..b789c4e 100644 --- a/check_projects.py +++ b/check_projects.py @@ -4,38 +4,58 @@ import sys import tempfile import textwrap +from collections.abc import Collection, Mapping, Sequence +from concurrent.futures import Future from pathlib import Path +from typing import Any, Literal, Required, TypeAlias, TypedDict, cast import yaml +EntrypointType: TypeAlias = Literal[ + "properdocs_theme", "mkdocs_theme", "properdocs_plugin", "mkdocs_plugin", "markdown_extension" +] -def _get_as_list(mapping, key): - names = mapping.get(key, ()) + +class Project(TypedDict, total=False): + name: Required[str] + category: Required[str] + labels: Collection[str] + properdocs_theme: str | Collection[str] + mkdocs_theme: str | Collection[str] + properdocs_plugin: str | Collection[str] + mkdocs_plugin: str | Collection[str] + markdown_extension: str | Collection[str] + github_id: str + pypi_id: str + + +def _get_as_list(mapping: Project, key: EntrypointType) -> Collection[str]: + names: str | Collection[str] = mapping.get(key, ()) if isinstance(names, str): names = (names,) return names -_kind_to_label = { - "mkdocs_plugin": "plugin", - "mkdocs_theme": "theme", - "markdown_extension": "markdown", +_kinds_to_label: Mapping[Collection[EntrypointType], str] = { + ("properdocs_plugin", "mkdocs_plugin"): "plugin", + ("properdocs_theme", "mkdocs_theme"): "theme", + ("markdown_extension",): "markdown", } -config = yaml.safe_load(Path("projects.yaml").read_text()) +config: Mapping[str, Any] = yaml.safe_load(Path("projects.yaml").read_text(encoding="utf-8")) -projects = config["projects"] -all_labels = dict.fromkeys(label["label"] for label in config["labels"]) -all_categories = dict.fromkeys(category["category"] for category in config["categories"]) +projects: Sequence[Project] = config["projects"] +all_labels: Collection[str] = dict.fromkeys(label["label"] for label in config["labels"]) +all_categories: Collection[str] = dict.fromkeys(category["category"] for category in config["categories"]) -def check_install_project(project, install_name, errors=None): +def check_install_project(project: Project, install_name: str, errors: list[str] | None = None) -> list[str]: if errors is None: errors = [] with tempfile.TemporaryDirectory(prefix="best-of-mkdocs-") as directory: try: - result = subprocess.run( + subprocess.run( ["pip", "install", "-U", "--ignore-requires-python", "--no-deps", "--target", directory, install_name], stdin=subprocess.DEVNULL, capture_output=True, @@ -45,15 +65,17 @@ def check_install_project(project, install_name, errors=None): ) except (subprocess.CalledProcessError, subprocess.TimeoutExpired) as e: errors.append(f"Failed {e.cmd}:\n{e.stderr}") - return + return errors - entry_points = configparser.ConfigParser() + entry_points_parser = configparser.ConfigParser() try: - [entry_points_file] = Path(directory).glob(f"*.dist-info/entry_points.txt") - entry_points.read_string(entry_points_file.read_text()) + [entry_points_file] = Path(directory).glob("*.dist-info/entry_points.txt") + entry_points_parser.read_string(entry_points_file.read_text()) except ValueError: pass - entry_points = {sect: list(entry_points[sect]) for sect in entry_points.sections()} + entry_points: dict[str, list[str]] = { + sect: list(entry_points_parser[sect]) for sect in entry_points_parser.sections() + } for item in _get_as_list(project, "mkdocs_plugin"): if item not in entry_points.get("mkdocs.plugins", ()): @@ -68,7 +90,7 @@ def check_install_project(project, install_name, errors=None): base_path = item.replace(".", "/") for pattern in base_path + ".py", base_path + "/__init__.py": path = Path(directory, pattern) - if path.is_file() and "makeExtension" in path.read_text(): + if path.is_file() and "makeExtension" in path.read_text(encoding="utf-8"): break else: errors.append( @@ -83,12 +105,12 @@ def check_install_project(project, install_name, errors=None): pool = concurrent.futures.ThreadPoolExecutor(4) # Tracks shadowing: projects earlier in the list take precedence. -available = {k: {} for k in _kind_to_label} +available: dict[EntrypointType, dict[str, str]] = {k: {} for keys in _kinds_to_label for k in keys} -futures = [] +futures: list[tuple[str, Future[list[str]]]] = [] for project in projects: - errors = [] + errors: list[str] = [] name = project.get("name") if not name: @@ -104,30 +126,34 @@ def check_install_project(project, install_name, errors=None): if label not in all_labels: errors.append(f"Unknown label: {label!r} - should be one of: {', '.join(all_labels)}") - for kind, label in _kind_to_label.items(): - items = _get_as_list(project, kind) - + for kinds, label in _kinds_to_label.items(): if label == "plugin" and "theme" in labels and "plugin" not in labels: pass - elif (label in labels) != bool(items): - errors.append(f"'{label}' label should be present if and only if '{kind}:' is present") - - for item in items: - already_available = available[kind].get(item) or ( - kind == "mkdocs_plugin" and available[kind].get(item.split("/")[-1]) - ) - if already_available: - if kind not in project.get("shadowed", ()): - errors.append( - f"{kind} '{item.split('/')[-1]}' is present in both project '{already_available}' and '{name}'.\n" - f"If that is expected, the later of the two projects will be ignored, " - f"and to indicate this, it should contain 'shadowed: [{kind}]'" - ) - else: - available[kind][item] = name - - install_name = None - if any(key in project for key in _kind_to_label): + elif (label in labels) != any(bool(project.get(kind)) for kind in kinds): + errors.append(f"'{label}' label should be present if and only if '{kinds}:' is present") + + for kind in kinds: + items = _get_as_list(project, kind) + + for item in items: + already_available: str | None = None + for subkind in (kind, cast("EntrypointType", kind.replace("mkdocs", "properdocs"))): + if already_available is None: + already_available = available[subkind].get(item) + if already_available is None and "plugin" in kind: + already_available = available[subkind].get(item.split("/")[-1]) + + if already_available: + if kind not in project.get("shadowed", ()): + errors.append( + f"{kind} '{item.split('/')[-1]}' is present in both project '{already_available}' and '{name}'.\n" + f"If that is expected, the later of the two projects will be ignored, " + f"and to indicate this, it should contain 'shadowed: [{kind}]'" + ) + available[kind].setdefault(item, name) + + install_name: str | None = None + if any(key in project for keys in _kinds_to_label for key in keys): if "pypi_id" in project: install_name = project["pypi_id"] if "_" in install_name: @@ -138,10 +164,11 @@ def check_install_project(project, install_name, errors=None): else: errors.append("Missing 'pypi_id:'") + fut: Future[list[str]] if install_name: fut = pool.submit(check_install_project, project, install_name, errors) else: - fut = concurrent.futures.Future() + fut = Future() fut.set_result(errors) futures.append((name, fut)) diff --git a/projects.yaml b/projects.yaml index 866394f..2faecc2 100644 --- a/projects.yaml +++ b/projects.yaml @@ -76,9 +76,10 @@ projects: category: theming - name: Material for MkDocs mkdocs_theme: material - mkdocs_plugin: [material/blog, material/group, material/offline, material/search, material/social, material/tags] + mkdocs_plugin: [material/blog, material/group, material/info, material/meta, material/offline, material/optimize, material/privacy, material/projects, material/search, material/social, material/tags, material/typeset] extra_dependencies: plugins.social: mkdocs-material[imaging] + plugins.optimize: mkdocs-material[imaging] github_id: squidfunk/mkdocs-material pypi_id: mkdocs-material labels: [theme, plugin] @@ -187,7 +188,7 @@ projects: labels: [theme] category: theming - name: Zettelkasten - mkdocs_theme: zettelkasten-solarized-light + mkdocs_theme: zettelkasten mkdocs_plugin: [zettelkasten] github_id: buvis/mkdocs-zettelkasten pypi_id: mkdocs-zettelkasten @@ -1511,11 +1512,6 @@ projects: pypi_id: mkdocs-categories-plugin labels: [plugin] category: nav-pages -- name: filename-title - mkdocs_plugin: filename_title - github_id: mipro98/mkdocs-filename-title-plugin - labels: [plugin] - category: nav-pages - name: mkdocs-title-casing-plugin mkdocs_plugin: title-casing github_id: mattchristopher314/mkdocs-title-casing-plugin @@ -1712,12 +1708,6 @@ projects: pypi_id: mkdocs-print-site-plugin labels: [plugin] category: site-conversion -- name: mk2pdf-export - mkdocs_plugin: mk2pdf-export - github_id: HaoLiuHust/mkdocs-mk2pdf-plugin - pypi_id: mkdocs-mk2pdf-plugin - labels: [plugin] - category: site-conversion - name: pdf-with-js mkdocs_plugin: pdf-with-js github_id: smaxtec/mkdocs-pdf-with-js-plugin diff --git a/ruff.toml b/ruff.toml new file mode 100644 index 0000000..f00091a --- /dev/null +++ b/ruff.toml @@ -0,0 +1,36 @@ +line-length = 120 + +[lint] +preview = true +select = [ + "YTT", "ASYNC", "FBT", "C4", "DTZ", "T10", "EXE", "FA", "ISC", "PIE", "RSE", "I", "E", "W", "F", "UP", + "S201", "S202", "S303", "S304", "S305", "S306", "S506", "S602", "S604", "S605", "S612", "S701", "S704", + "B002", "B003", "B004", "B005", "B007", "B008", "B009", "B010", "B011", "B012", "B013", "B014", "B015", "B016", "B017", "B018", "B019", "B020", "B021", "B022", "B023", "B025", "B026", "B029", "B030", "B031", "B032", "B033", "B034", "B035", "B039", "B043", "B901", "B905", "B909", "B911", "B912", + "COM818", + "LOG001", "LOG004", "LOG007", "LOG009", "LOG014", "LOG015", + "G001", "G002", "G003", "G010", "G101", "G201", "G202", + "PYI001", "PYI002", "PYI003", "PYI004", "PYI005", "PYI006", "PYI007", "PYI008", "PYI009", "PYI010", "PYI011", "PYI012", "PYI013", "PYI014", "PYI015", "PYI016", "PYI017", "PYI018", "PYI019", "PYI020", "PYI021", "PYI024", "PYI025", "PYI026", "PYI029", "PYI030", "PYI032", "PYI033", "PYI034", "PYI035", "PYI036", "PYI041", "PYI042", "PYI043", "PYI044", "PYI045", "PYI046", "PYI047", "PYI048", "PYI049", "PYI050", "PYI051", "PYI052", "PYI053", "PYI054", "PYI055", "PYI056", "PYI057", "PYI058", "PYI059", "PYI061", "PYI062", "PYI063", "PYI064", "PYI066", + "PT001", "PT002", "PT003", "PT006", "PT007", "PT008", "PT009", "PT010", "PT013", "PT014", "PT015", "PT016", "PT018", "PT019", "PT020", "PT021", "PT022", "PT023", "PT024", "PT025", "PT026", "PT027", "PT029", + "Q004", + "RET502", "RET503", "RET504", + "SIM101", "SIM103", "SIM105", "SIM107", "SIM109", "SIM110", "SIM113", "SIM114", "SIM118", "SIM201", "SIM202", "SIM208", "SIM210", "SIM211", "SIM212", "SIM220", "SIM221", "SIM222", "SIM223", "SIM300", "SIM401", "SIM905", "SIM910", "SIM911", + "TD004", "TD005", "TD006", "TD007", + "TC001", "TC002", "TC003", "TC004", "TC005", "TC006", "TC007", "TC008", "TC010", + "PTH124", "PTH201", "PTH210", + "FLY002", + "N803", "N804", "N805", "N806", "N807", "N815", "N816", "N999", + "PERF101", "PERF102", "PERF402", "PERF403", + "PGH003", "PGH004", "PGH005", + "PLC0105", "PLC0131", "PLC0132", "PLC0205", "PLC0206", "PLC0207", "PLC0208", "PLC0414", "PLC2401", "PLC2403", "PLC2701", "PLC2801", "PLC3002", + "PLE0100", "PLE0101", "PLE0115", "PLE0116", "PLE0117", "PLE0118", "PLE0237", "PLE0241", "PLE0302", "PLE0303", "PLE0304", "PLE0305", "PLE0307", "PLE0308", "PLE0309", "PLE0604", "PLE0605", "PLE0643", "PLE0704", "PLE1132", "PLE1141", "PLE1142", "PLE1205", "PLE1206", "PLE1300", "PLE1307", "PLE1310", "PLE1507", "PLE1519", "PLE1520", "PLE1700", "PLE2502", "PLE2510", "PLE2512", "PLE2513", "PLE2514", "PLE2515", "PLE4703", + "PLR0124", "PLR0133", "PLR0202", "PLR0203", "PLR0206", "PLR0402", "PLR1704", "PLR1708", "PLR1712", "PLR1716", "PLR1722", "PLR1733", "PLR1736", "PLR2044", "PLR6201", "PLR6301", + "PLW0108", "PLW0120", "PLW0127", "PLW0128", "PLW0129", "PLW0131", "PLW0133", "PLW0177", "PLW0211", "PLW0244", "PLW0245", "PLW0406", "PLW0602", "PLW0603", "PLW0604", "PLW0642", "PLW0711", "PLW1501", "PLW1507", "PLW1508", "PLW1514", "PLW1641", "PLW2101", "PLW2901", "PLW3201", + "FURB101", "FURB103", "FURB105", "FURB110", "FURB113", "FURB116", "FURB118", "FURB122", "FURB129", "FURB131", "FURB132", "FURB136", "FURB142", "FURB145", "FURB148", "FURB152", "FURB154", "FURB156", "FURB157", "FURB161", "FURB162", "FURB163", "FURB164", "FURB166", "FURB167", "FURB168", "FURB169", "FURB171", "FURB177", "FURB180", "FURB181", "FURB188", "FURB192", + "RUF001", "RUF002", "RUF003", "RUF005", "RUF006", "RUF007", "RUF008", "RUF009", "RUF010", "RUF012", "RUF013", "RUF015", "RUF016", "RUF017", "RUF018", "RUF019", "RUF020", "RUF021", "RUF022", "RUF023", "RUF024", "RUF026", "RUF028", "RUF029", "RUF030", "RUF031", "RUF032", "RUF033", "RUF034", "RUF036", "RUF037", "RUF038", "RUF039", "RUF040", "RUF041", "RUF043", "RUF045", "RUF046", "RUF047", "RUF048", "RUF049", "RUF051", "RUF052", "RUF053", "RUF054", "RUF055", "RUF056", "RUF057", "RUF058", "RUF059", "RUF060", "RUF061", "RUF063", "RUF064", "RUF065", "RUF066", "RUF068", "RUF069", "RUF070", "RUF071", "RUF100", "RUF101", "RUF102", "RUF103", "RUF104", "RUF200", + "TRY002", "TRY004", "TRY201", "TRY203", "TRY300", "TRY301", "TRY400", "TRY401", +] +ignore = ["E501", "E731"] +[lint.flake8-comprehensions] +allow-dict-calls-with-keyword-arguments = true +[lint.flake8-type-checking] +exempt-modules = ["typing", "collections.abc"]