Skip to content
Open
Binary file added .DS_Store
Binary file not shown.
9 changes: 8 additions & 1 deletion docs/en/dev/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,13 @@ cut.

### Changed

- Docs: split `scripts/generate_reference.py` into focused internal modules
under `scripts/_reference/` (page definitions, manual page loading, signature
formatting, type and default rendering, table and reference validation, and
output/check-mode). `npm run docs:reference` and
`npm run docs:reference:check` behavior, generated files, and the docs
generator's public surface are unchanged.

- CLI: split parser setup, command dispatch, local action handling, and remote
action handling into focused internal modules. Command syntax and behavior are
unchanged. ([#167](https://github.com/DevilsAutumn/quater/issues/167))
Expand Down Expand Up @@ -405,4 +412,4 @@ No public API is deprecated in this alpha.

- [Known Limitations](/en/dev/known-limitations): current gaps.
- [Stability](/en/dev/stability): version pinning and upgrade expectations.
- [Quickstart](/en/dev/quickstart): first working app.
- [Quickstart](/en/dev/quickstart): first working app.
1 change: 1 addition & 0 deletions scripts/_reference/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Internal helpers for scripts/generate_reference.py."""
40 changes: 40 additions & 0 deletions scripts/_reference/manual.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
"""Loader for manually-authored reference pages.

If every reference file exists and none carries the generated header, the
generator hands the manual content back unchanged (after checking each
public symbol has an anchor on its assigned page).
"""

from __future__ import annotations

from collections.abc import Mapping
from pathlib import Path

from _reference.pages import PAGES, ReferencePage
from _reference.paths import GENERATED_HEADER, REFERENCE_DIR
from _reference.types import symbol_anchor


def read_manual_reference(
public_api: tuple[str, ...],
pages_by_symbol: Mapping[str, ReferencePage],
) -> dict[Path, str] | None:
paths = {REFERENCE_DIR / "index.md", *(page.path for page in PAGES)}
outputs: dict[Path, str] = {}
for path in paths:
if not path.exists():
return None
content = path.read_text(encoding="utf-8")
if content.startswith(GENERATED_HEADER):
return None
outputs[path] = content

for name in public_api:
page = pages_by_symbol[name]
content = outputs[page.path]
anchor = symbol_anchor(name)
if anchor not in content:
raise SystemExit(
f"Manual reference page {page.path} does not document {name!r}"
)
return outputs
38 changes: 38 additions & 0 deletions scripts/_reference/output.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
"""Write generated reference files or diff them in --check mode."""

from __future__ import annotations

import difflib
import sys
from collections.abc import Mapping
from pathlib import Path

from _reference.paths import REFERENCE_DIR


def write_outputs(outputs: Mapping[Path, str]) -> None:
REFERENCE_DIR.mkdir(parents=True, exist_ok=True)
for path, content in sorted(outputs.items()):
path.write_text(content, encoding="utf-8")


def check_outputs(outputs: Mapping[Path, str]) -> int:
stale = False
for path, expected in sorted(outputs.items()):
if not path.exists():
print(f"Missing generated reference file: {path}", file=sys.stderr)
stale = True
continue
actual = path.read_text(encoding="utf-8")
if actual == expected:
continue
stale = True
diff = difflib.unified_diff(
actual.splitlines(),
expected.splitlines(),
fromfile=str(path),
tofile=f"{path} (generated)",
lineterm="",
)
print("\n".join(diff), file=sys.stderr)
return 1 if stale else 0
164 changes: 164 additions & 0 deletions scripts/_reference/pages.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
"""Reference page definitions and public-API validation."""

from __future__ import annotations

import ast
import re
from collections.abc import Iterable
from dataclasses import dataclass
from pathlib import Path
from typing import Any

from _reference.paths import PACKAGE_INIT, REFERENCE_DIR
from _reference.signatures import object_for

MIN_PUBLIC_DOCSTRING_WORDS = 8
PLACEHOLDER_DOCSTRING_WORDS = frozenset({"todo", "tbd", "fixme", "placeholder"})


@dataclass(frozen=True, slots=True)
class ReferencePage:
slug: str
title: str
description: str
symbols: tuple[str, ...]

@property
def path(self) -> Path:
return REFERENCE_DIR / f"{self.slug}.md"


PAGES: tuple[ReferencePage, ...] = (
ReferencePage(
slug="application",
title="Application",
description="App objects, route groups, and configuration.",
symbols=("Quater", "RouteGroup", "AppConfig", "CORSConfig", "__version__"),
),
ReferencePage(
slug="resources",
title="Resources",
description="Request-scoped resources injected into handlers.",
symbols=("Resource",),
),
ReferencePage(
slug="request",
title="Request",
description="Request data and state passed through handlers.",
symbols=("Request", "State", "FormData", "UploadFile"),
),
ReferencePage(
slug="parameters",
title="Parameters",
description="Handler parameter markers for request data binding.",
symbols=("Path", "Query", "Body", "Form", "File", "Header", "Cookie"),
),
ReferencePage(
slug="responses",
title="Responses",
description="Return values and explicit response classes.",
symbols=(
"Response",
"JSONResponse",
"TextResponse",
"HTMLResponse",
"BytesResponse",
"StreamResponse",
"RedirectResponse",
"EmptyResponse",
),
),
ReferencePage(
slug="auth",
title="Auth and Security",
description=(
"Auth hooks, approval hooks, framework errors, and signed cookies."
),
symbols=(
"AuthConfig",
"AuthContext",
"ApprovalRequest",
"ActionApproval",
"HTTPError",
"ImproperlyConfigured",
"SignedCookieSigner",
),
),
ReferencePage(
slug="observability",
title="Observability",
description="Access-log and MCP audit event types.",
symbols=("AccessLogEvent", "AccessLogHook", "ToolAuditEvent"),
),
ReferencePage(
slug="testing",
title="Testing",
description="In-process HTTP, MCP, and CLI test clients.",
symbols=("TestClient", "TestResponse", "MCPTestClient", "CliTestClient"),
),
)


def page_symbols(slug: str) -> tuple[str, ...]:
for page in PAGES:
if page.slug == slug:
return page.symbols
raise KeyError(slug)


def page_map(pages: Iterable[ReferencePage]) -> dict[str, ReferencePage]:
mapped: dict[str, ReferencePage] = {}
for page in pages:
for symbol in page.symbols:
if symbol in mapped:
raise SystemExit(f"Duplicate reference symbol: {symbol}")
mapped[symbol] = page
return mapped


def read_public_api() -> tuple[str, ...]:
module = ast.parse(PACKAGE_INIT.read_text(encoding="utf-8"))
for node in module.body:
if not isinstance(node, ast.Assign):
continue
if not any(
isinstance(target, ast.Name) and target.id == "__all__"
for target in node.targets
):
continue
value = ast.literal_eval(node.value)
if not isinstance(value, list) or not all(
isinstance(item, str) for item in value
):
raise SystemExit("__all__ must be a list of strings")
return tuple(value)
raise SystemExit("Could not find quater.__all__")


def validate_public_docstrings(package: Any, public_api: tuple[str, ...]) -> None:
missing: list[str] = []
for name in public_api:
obj = object_for(package, name)
kind_name = str(getattr(obj, "kind", ""))
if not kind_name.endswith(("CLASS", "FUNCTION")):
continue
docstring = getattr(obj, "docstring", None)
value = getattr(docstring, "value", None)
if not meaningful_docstring(value):
missing.append(name)

if missing:
raise SystemExit(
"Public classes/functions need meaningful docstrings: "
+ ", ".join(sorted(missing))
)


def meaningful_docstring(value: object) -> bool:
if not isinstance(value, str):
return False
words = re.findall(r"[A-Za-z0-9_]+", value)
if len(words) < MIN_PUBLIC_DOCSTRING_WORDS:
return False
lowered = {word.lower() for word in words}
return not bool(lowered & PLACEHOLDER_DOCSTRING_WORDS)
13 changes: 13 additions & 0 deletions scripts/_reference/paths.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
"""Filesystem paths and the shared generated-file header."""

from __future__ import annotations

from pathlib import Path

REPO_ROOT = Path(__file__).resolve().parents[2]
SOURCE_ROOT = REPO_ROOT / "src"
PACKAGE_INIT = SOURCE_ROOT / "quater" / "__init__.py"
REFERENCE_DIR = REPO_ROOT / "docs" / "en" / "dev" / "reference"
GENERATED_HEADER = (
"<!-- Generated by scripts/generate_reference.py. Do not edit by hand. -->"
)
Loading