chore(release): 0.0.7

Squash-merge lib-standardisation: adopt repo standards across packaging,
tooling, docs, and CI.

- standardize pyproject.toml (canonical packaging, lint config, extras, GSDFC pydoclint)
- add py.typed marker, modernize type hints and Google-style docstrings
- nest generated docs under docs/lib and docs/mcp; add hand-written wiki
- adopt standard Type A PyPI CI pipeline (tag==version guard + quality gate)
- promote generated CHANGELOG.md; bump version to 0.0.7
This commit is contained in:
2026-10-17 02:16:44 +05:30
parent 8253c25928
commit 06ffd2febd
149 changed files with 7132 additions and 4594 deletions

View File

@@ -26,11 +26,13 @@ independent of module hierarchy.
from .spec import NavSpec, load_nav_spec
from .resolver import ResolvedNav, resolve_nav
from .mkdocs import MkDocsNavEmitter
from .wiki import build_wiki_nav
__all__ = [
"NavSpec",
"ResolvedNav",
"MkDocsNavEmitter",
"build_wiki_nav",
"resolve_nav",
"load_nav_spec",
]

View File

@@ -1,11 +1,13 @@
from .spec import NavSpec, load_nav_spec
from .resolver import ResolvedNav, resolve_nav
from .mkdocs import MkDocsNavEmitter
from .resolver import ResolvedNav, resolve_nav
from .spec import NavSpec, load_nav_spec
from .wiki import build_wiki_nav
__all__ = [
"NavSpec",
"ResolvedNav",
"MkDocsNavEmitter",
"build_wiki_nav",
"resolve_nav",
"load_nav_spec",
]

View File

@@ -4,10 +4,18 @@ MkDocs navigation emitter.
This module provides the ``MkDocsNavEmitter`` class, which converts a
``ResolvedNav`` instance into the navigation structure required by the
MkDocs ``nav`` configuration.
---
Notes:
- The emitted structure is a list of dictionaries, one per top-level nav
entry, matching the MkDocs ``nav`` YAML format.
---
"""
from pathlib import Path
from typing import List, Dict, Any
from typing import Any
from docforge.nav.resolver import ResolvedNav
@@ -20,20 +28,22 @@ class MkDocsNavEmitter:
list structure expected by the MkDocs ``nav`` configuration field.
"""
def emit(self, nav: ResolvedNav) -> List[Dict[str, Any]]:
def emit(self, nav: ResolvedNav) -> list[dict[str, Any]]:
"""
Generate a navigation structure for ``mkdocs.yml``.
Args:
nav: Resolved navigation data describing documentation groups
nav (ResolvedNav):
Resolved navigation data describing documentation groups
and their associated Markdown files.
Returns:
A list of dictionaries representing the MkDocs navigation layout.
Each dictionary maps a navigation label to a page or a list of
pages.
list[dict[str, Any]]:
A list of dictionaries representing the MkDocs navigation layout.
Each dictionary maps a navigation label to a page or a list of
pages.
"""
result: List[Dict[str, Any]] = []
result: list[dict[str, Any]] = []
# Home entry (semantic path)
if nav.home:
@@ -41,7 +51,7 @@ class MkDocsNavEmitter:
# Group entries
for group, paths in nav.groups.items():
entries: List[str] = []
entries: list[str] = []
for p in paths:
# Convert filesystem path back to docs-relative path
rel_path = self._to_relative(p, nav._docs_root)
@@ -59,11 +69,14 @@ class MkDocsNavEmitter:
resulting path is relative to the documentation root.
Args:
path: Filesystem path to convert.
docs_root: Root directory of the documentation sources.
path (Path):
Filesystem path to convert.
docs_root (Path | None):
Root directory of the documentation sources.
Returns:
POSIX-style path relative to the documentation root.
str:
POSIX-style path relative to the documentation root.
"""
if docs_root and path.is_absolute():
try:
@@ -75,7 +88,7 @@ class MkDocsNavEmitter:
path_str = path.as_posix()
docs_root_str = docs_root.as_posix()
if path_str.startswith(docs_root_str + "/"):
return path_str[len(docs_root_str) + 1:]
return path_str[len(docs_root_str) + 1 :]
# Fallback for other cases
return path.as_posix().split("/docs/", 1)[-1]

View File

@@ -1,15 +1,14 @@
from typing import Dict, List, Any
from pathlib import Path
from typing import Any
from docforge.nav.resolver import ResolvedNav
class MkDocsNavEmitter:
"""
Converts a ResolvedNav into MkDocs-compatible `nav` data.
"""
def emit(self, nav: ResolvedNav) -> List[Dict[str, Any]]:
def emit(self, nav: ResolvedNav) -> list[dict[str, Any]]:
"""
Emit a structure suitable for insertion into mkdocs.yml.

View File

@@ -3,12 +3,19 @@ Navigation resolution utilities.
This module resolves a ``NavSpec`` against the filesystem by expanding glob
patterns and validating that referenced documentation files exist.
---
Notes:
- Glob resolution is recursive and returns paths in sorted order.
- Unmatched patterns raise ``FileNotFoundError`` to fail fast on typos.
---
"""
from pathlib import Path
from typing import Dict, Iterable, List
import glob
from collections.abc import Iterable
from pathlib import Path
from docforge.nav.spec import NavSpec
@@ -29,16 +36,19 @@ class ResolvedNav:
def __init__(
self,
home: str | None,
groups: Dict[str, List[Path]],
groups: dict[str, list[Path]],
docs_root: Path | None = None,
) -> None:
"""
Initialize a ResolvedNav instance.
Args:
home: Relative path to the home page within the documentation root.
groups: Mapping of group titles to resolved documentation file paths.
docs_root: Root directory of the documentation source files.
home (str | None):
Relative path to the home page within the documentation root.
groups (dict[str, list[Path]]):
Mapping of group titles to resolved documentation file paths.
docs_root (Path | None):
Root directory of the documentation source files.
"""
self.home = home
self.groups = groups
@@ -48,8 +58,10 @@ class ResolvedNav:
"""
Iterate over all files referenced by the navigation structure.
Returns:
An iterable of ``Path`` objects representing documentation files.
Yields:
Path:
A documentation file referenced by the navigation, including
the home page when defined.
Raises:
RuntimeError: If the home page is defined but the documentation
@@ -61,8 +73,7 @@ class ResolvedNav:
yield self._docs_root / self.home
for paths in self.groups.values():
for p in paths:
yield p
yield from paths
def resolve_nav(
@@ -76,11 +87,14 @@ def resolve_nav(
that referenced documentation files exist within the documentation root.
Args:
spec: Navigation specification describing documentation layout.
docs_root: Root directory containing documentation Markdown files.
spec (NavSpec):
Navigation specification describing documentation layout.
docs_root (Path):
Root directory containing documentation Markdown files.
Returns:
A ``ResolvedNav`` instance containing validated navigation paths.
ResolvedNav:
A `ResolvedNav` instance containing validated navigation paths.
Raises:
FileNotFoundError: If the documentation root does not exist or a
@@ -89,23 +103,23 @@ def resolve_nav(
if not docs_root.exists():
raise FileNotFoundError(docs_root)
def resolve_pattern(pattern: str) -> List[Path]:
def resolve_pattern(pattern: str) -> list[Path]:
"""
Resolve a glob pattern relative to the documentation root.
Args:
pattern: Glob pattern used to match documentation files.
pattern (str):
Glob pattern used to match documentation files.
Returns:
A sorted list of matching ``Path`` objects.
list[Path]:
A sorted list of matching `Path` objects.
Raises:
FileNotFoundError: If the pattern does not match any files.
"""
full = docs_root / pattern
matches = sorted(
Path(p) for p in glob.glob(str(full), recursive=True)
)
matches = sorted(Path(p) for p in glob.glob(str(full), recursive=True))
if not matches:
raise FileNotFoundError(pattern)
@@ -121,10 +135,10 @@ def resolve_nav(
home = spec.home
# Resolve navigation groups
resolved_groups: Dict[str, List[Path]] = {}
resolved_groups: dict[str, list[Path]] = {}
for group, patterns in spec.groups.items():
files: List[Path] = []
files: list[Path] = []
for pattern in patterns:
files.extend(resolve_pattern(pattern))
resolved_groups[group] = files

View File

@@ -1,9 +1,8 @@
from collections.abc import Iterable
from pathlib import Path
from typing import Dict, List, Iterable, Optional
from docforge.nav.spec import NavSpec
class ResolvedNav:
"""
Fully-resolved navigation tree.
@@ -13,17 +12,16 @@ class ResolvedNav:
- Order is preserved
"""
home: Optional[str]
groups: Dict[str, List[Path]]
_docs_root: Optional[Path]
home: str | None
groups: dict[str, list[Path]]
_docs_root: Path | None
def __init__(
self,
home: str | None,
groups: Dict[str, List[Path]],
groups: dict[str, list[Path]],
docs_root: Path | None = ...,
) -> None: ...
def all_files(self) -> Iterable[Path]:
"""
Return all resolved documentation files in nav order.
@@ -33,7 +31,6 @@ class ResolvedNav:
"""
...
def resolve_nav(
spec: NavSpec,
docs_root: Path,

View File

@@ -4,10 +4,20 @@ Navigation specification model.
This module defines the ``NavSpec`` class, which represents the navigation
structure defined by the user in the doc-forge navigation specification
(typically ``docforge.nav.yml``).
---
Notes:
- The spec file supports an optional ``icon`` mapping for MkDocs theme
customization.
- All file references in ``groups`` are relative to the documentation root.
---
"""
from __future__ import annotations
from pathlib import Path
from typing import Dict, List, Optional
import yaml
@@ -24,34 +34,46 @@ class NavSpec:
``index.md``).
groups: Mapping of navigation group titles to lists of file patterns
or glob expressions.
icon: Optional mapping of theme icon entries (for example
``{"logo": "material/code-tags"}``) injected into the MkDocs
theme as ``theme.icon``.
"""
def __init__(
self,
home: Optional[str],
groups: Dict[str, List[str]],
home: str | None,
groups: dict[str, list[str]],
icon: dict[str, str] | None = None,
) -> None:
"""
Initialize a NavSpec instance.
Args:
home: Relative path to the home document.
groups: Mapping of group names to lists of path patterns
home (str | None):
Relative path to the home document.
groups (dict[str, list[str]]):
Mapping of group names to lists of path patterns
(glob expressions).
icon (dict[str, str] | None):
Optional mapping of theme icon entries applied to the
generated MkDocs configuration.
"""
self.home = home
self.groups = groups
self.icon = icon
@classmethod
def load(cls, path: Path) -> "NavSpec":
def load(cls, path: Path) -> NavSpec:
"""
Load a navigation specification from a YAML file.
Args:
path: Filesystem path to the navigation specification file.
path (Path):
Filesystem path to the navigation specification file.
Returns:
A ``NavSpec`` instance representing the parsed configuration.
NavSpec:
A ``NavSpec`` instance representing the parsed configuration.
Raises:
FileNotFoundError: If the specified file does not exist.
@@ -68,6 +90,7 @@ class NavSpec:
home = data.get("home")
groups = data.get("groups", {})
icon = data.get("icon")
if home is not None and not isinstance(home, str):
raise ValueError("home must be a string")
@@ -83,17 +106,26 @@ class NavSpec:
):
raise ValueError(f"group '{key}' must be a list of strings")
return cls(home=home, groups=groups)
if icon is not None and (
not isinstance(icon, dict)
or not all(
isinstance(k, str) and isinstance(v, str) for k, v in icon.items()
)
):
raise ValueError("icon must be a mapping of strings")
def all_patterns(self) -> List[str]:
return cls(home=home, groups=groups, icon=icon)
def all_patterns(self) -> list[str]:
"""
Return all path patterns referenced by the specification.
Returns:
A list containing the home document (if defined) and all
group pattern entries.
list[str]:
A list containing the home document (if defined) and all
group pattern entries.
"""
patterns: List[str] = []
patterns: list[str] = []
if self.home:
patterns.append(self.home)
@@ -112,10 +144,12 @@ def load_nav_spec(path: Path) -> NavSpec:
corresponding ``NavSpec`` instance.
Args:
path: Path to the navigation specification file.
path (Path):
Path to the navigation specification file.
Returns:
A ``NavSpec`` instance representing the parsed specification.
NavSpec:
A ``NavSpec`` instance representing the parsed specification.
Raises:
FileNotFoundError: If the specification file does not exist.
@@ -132,4 +166,5 @@ def load_nav_spec(path: Path) -> NavSpec:
return NavSpec(
home=data.get("home"),
groups=data.get("groups", {}),
icon=data.get("icon"),
)

View File

@@ -1,6 +1,4 @@
from pathlib import Path
from typing import Dict, List, Optional
class NavSpec:
"""
@@ -10,18 +8,16 @@ class NavSpec:
of filesystem structure or MkDocs specifics.
"""
home: Optional[str]
groups: Dict[str, List[str]]
home: str | None
groups: dict[str, list[str]]
def __init__(
self,
home: Optional[str],
groups: Dict[str, List[str]],
) -> None:
...
home: str | None,
groups: dict[str, list[str]],
) -> None: ...
@classmethod
def load(cls, path: Path) -> "NavSpec":
def load(cls, path: Path) -> NavSpec:
"""
Load and validate a nav specification from YAML.
@@ -31,12 +27,11 @@ class NavSpec:
"""
...
def all_patterns(self) -> List[str]:
def all_patterns(self) -> list[str]:
"""
Return all path patterns referenced by the spec
(including home and group entries).
"""
...
def load_nav_spec(path: Path) -> NavSpec: ...

165
docforge/nav/wiki.py Normal file
View File

@@ -0,0 +1,165 @@
"""
# Summary
Wiki navigation derivation.
This module provides ``build_wiki_nav``, which derives an MkDocs-ready
navigation block from the file structure of a hand-written wiki directory
(typically ``docs/wiki``). wiki content is authored by hand and is never
modified by doc-forge; only the navigation layout is inferred.
# Notes
- ``index.md`` at the wiki root becomes the ``Home`` entry.
- Page labels are derived from filenames: numeric order prefixes such as
``01_`` or ``02-`` are stripped, separators are replaced with spaces, and
names are title-cased (``01_overview.md`` becomes ``Overview``).
- Subdirectories become nested navigation groups. A nested ``index.md`` is
rendered as the section root placed first inside the group.
- Only ``.md`` files are considered; hidden entries are ignored.
"""
import re
from collections.abc import Callable
from pathlib import Path
from typing import Any
def build_wiki_nav(wiki_dir: Path) -> list[dict[str, Any]]:
"""
Derive an MkDocs navigation block from a wiki directory.
Returned paths are relative to the parent of ``wiki_dir`` and carry the
wiki directory name as their leading component (for example
``wiki/01_overview.md`` when the wiki lives at ``docs/wiki``). This makes
the result directly usable in an MkDocs ``nav`` block with
- ``index.md`` at the wiki root becomes the ``Home`` entry.
- Page labels are derived from filenames: numeric order prefixes such as
``01_`` or ``02-`` are stripped, separators are replaced with spaces, and
names are title-cased (``01_overview.md`` becomes ``Overview``).
- Subdirectories become nested navigation groups. A nested ``index.md`` is
rendered as the section root placed first inside the group.
- Only ``.md`` files are considered; hidden entries are ignored.
Args:
wiki_dir (Path):
Path to the hand-written wiki directory, for example ``docs/wiki``.
Returns:
list[dict[str, Any]]:
Navigation entries compatible with the MkDocs ``nav`` configuration.
The list is empty if the wiki contains no Markdown files.
Raises:
FileNotFoundError:
If the wiki directory does not exist.
"""
if not wiki_dir.exists():
raise FileNotFoundError(wiki_dir)
root = wiki_dir.parent
def rel(path: Path) -> str:
try:
return path.relative_to(root).as_posix()
except ValueError:
return path.as_posix()
nav: list[dict[str, Any]] = []
if (wiki_dir / "index.md").exists():
nav.append({"Home": rel(wiki_dir / "index.md")})
nav.extend(_render_entries(wiki_dir, rel))
return nav
def _render_entries(
base_dir: Path,
rel: Callable[[Path], str],
) -> list[dict[str, Any]]:
"""
Render navigation entries for the children of a wiki directory.
Markdown pages become labeled entries, subdirectories containing
Markdown become nested groups, and a nested ``index.md`` is emitted
first as the section root.
Args:
base_dir (Path):
Directory whose children are rendered.
rel (Callable[[Path], str]):
Callable converting a wiki file path into a docs-relative path.
Returns:
list[dict[str, Any]]:
Navigation entries for ``base_dir`` in natural sort order.
"""
children = sorted(
(child for child in base_dir.iterdir() if not child.name.startswith(".")),
key=lambda child: _natural_key(child.name),
)
entries: list[dict[str, Any]] = []
for child in children:
if child.is_dir():
if not any(child.rglob("*.md")):
continue
group: list[dict[str, Any]] = []
nested_index = child / "index.md"
if nested_index.exists():
group.append({_prettify(child.name): rel(nested_index)})
group.extend(_render_entries(child, rel))
entries.append({_prettify(child.name): group})
elif child.suffix == ".md":
if child.name == "index.md":
continue
entries.append({_prettify(child.stem): rel(child)})
return entries
def _prettify(name: str) -> str:
"""
Convert a filename or directory name into a navigation label.
Numeric order prefixes (``01_``, ``02-``, ``03.``) are stripped,
separators are replaced with spaces, and the result is title-cased.
Args:
name (str):
Filename stem or directory name to prettify.
Returns:
str:
Title-cased navigation label.
"""
name = re.sub(r"^\d+[_\-. ]?", "", name)
name = re.sub(r"[_-]+", " ", name).strip()
return name.title()
def _natural_key(name: str) -> list[object]:
"""
Build a natural sort key from a filename.
The key splits ``name`` into alternating non-digit and digit parts so
that numeric prefixes sort numerically (``02`` before ``10``).
Args:
name (str):
Filename or directory name to key.
Returns:
list[object]:
Mixed list of lowercased strings and integers used for sorting.
"""
return [
int(part) if part.isdigit() else part.lower()
for part in re.split(r"(\d+)", name)
]

41
docforge/nav/wiki.pyi Normal file
View File

@@ -0,0 +1,41 @@
from collections.abc import Callable
from pathlib import Path
from typing import Any
def build_wiki_nav(wiki_dir: Path) -> list[dict[str, Any]]:
"""
Derive an MkDocs navigation block from a wiki directory.
Args:
wiki_dir (Path):
Path to the hand-written wiki directory.
Returns:
list[dict[str, Any]]:
Wiki navigation entries compatible with the MkDocs `nav`
configuration.
Raises:
FileNotFoundError:
If the wiki directory does not exist.
"""
...
def _render_entries(
base_dir: Path,
rel: Callable[[Path], str],
) -> list[dict[str, Any]]:
"""
Render navigation entries for the children of a wiki directory.
Args:
base_dir (Path):
Directory whose children are rendered.
rel (Callable[[Path], str]):
Callable converting a wiki file path into a docs-relative path.
Returns:
list[dict[str, Any]]:
Navigation entries for `base_dir` in natural sort order.
"""
...