Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
115 changes: 71 additions & 44 deletions check_projects.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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", ()):
Expand All @@ -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(
Expand All @@ -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:
Expand All @@ -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:
Expand All @@ -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))

Expand Down
16 changes: 3 additions & 13 deletions projects.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
36 changes: 36 additions & 0 deletions ruff.toml
Original file line number Diff line number Diff line change
@@ -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"]
Loading