Skip to content

mamut_routing_publish.docs_build

Build the MkDocs documentation site into the published static tree.

The documentation (docs/ + mkdocs.yml at the repository root) is a phase of site build: it renders into <site-output>/docs/ so the website serves it under /docs/ from the same tree as everything else, staging builds (--site-output-dir) never touch the live dist/docs, and the precompress phase covers the rendered HTML like any other asset.

MkDocs is an optional dependency group (uv sync --group docs; installed by default through [tool.uv] default-groups). The publisher imports it lazily so the rest of the CLI works without it and site build can fail fast, before the multi-hour cache phases, when the toolchain is missing.

DocsToolingUnavailable

Bases: RuntimeError

The MkDocs toolchain is not importable in this environment.

DocsBuildError

Bases: RuntimeError

The MkDocs build failed (configuration error or strict-mode warnings).

ensure_docs_tooling_available

ensure_docs_tooling_available() -> None

Raise :class:DocsToolingUnavailable unless every docs plugin imports.

Source code in src/mamut_routing_publish/docs_build.py
def ensure_docs_tooling_available() -> None:
    """Raise :class:`DocsToolingUnavailable` unless every docs plugin imports."""
    import importlib

    missing: list[str] = []
    for module_name in _REQUIRED_MODULES:
        try:
            importlib.import_module(module_name)
        except ImportError:
            missing.append(module_name)
    if missing:
        raise DocsToolingUnavailable(
            "The MkDocs documentation toolchain is not installed "
            f"(missing: {', '.join(missing)}). Run `uv sync --group docs`, "
            "or pass --skip-docs to `site build`."
        )

has_docs_config

has_docs_config(repo_root: Path) -> bool

True when the checkout carries a documentation site to build.

Source code in src/mamut_routing_publish/docs_build.py
def has_docs_config(repo_root: Path) -> bool:
    """True when the checkout carries a documentation site to build."""
    return (repo_root / DEFAULT_MKDOCS_CONFIG_NAME).is_file()

build_docs_site

build_docs_site(
    repo_root: Path,
    site_dir: Path,
    *,
    strict: bool = True,
    quiet: bool = False,
    config_file: Path | None = None,
) -> DocsBuildSummary

Render mkdocs.yml into site_dir (cleaned first by MkDocs itself).

quiet silences the console only: strict-mode warnings are still counted and still fail the build.

Runs with the repository root as the working directory: the gen-files script, the mkdocstrings source paths and the snippet base path in mkdocs.yml are all relative to it. Strict mode turns every MkDocs warning (missing nav target, broken link, unresolved reference) into a :class:DocsBuildError, which is what CI and the publisher want.

Source code in src/mamut_routing_publish/docs_build.py
def build_docs_site(
    repo_root: Path,
    site_dir: Path,
    *,
    strict: bool = True,
    quiet: bool = False,
    config_file: Path | None = None,
) -> DocsBuildSummary:
    """Render ``mkdocs.yml`` into ``site_dir`` (cleaned first by MkDocs itself).

    ``quiet`` silences the console only: strict-mode warnings are still
    counted and still fail the build.

    Runs with the repository root as the working directory: the gen-files
    script, the mkdocstrings source paths and the snippet base path in
    ``mkdocs.yml`` are all relative to it. Strict mode turns every MkDocs
    warning (missing nav target, broken link, unresolved reference) into a
    :class:`DocsBuildError`, which is what CI and the publisher want.
    """
    ensure_docs_tooling_available()
    from mkdocs.commands.build import build
    from mkdocs.config import load_config
    from mkdocs.exceptions import MkDocsException

    config_path = config_file or (repo_root / DEFAULT_MKDOCS_CONFIG_NAME)
    if not config_path.is_file():
        raise DocsBuildError(f"MkDocs configuration not found: {config_path}")
    site_dir = site_dir if site_dir.is_absolute() else repo_root / site_dir
    site_dir.parent.mkdir(parents=True, exist_ok=True)

    mkdocs_logger = logging.getLogger("mkdocs")
    previous_propagate = mkdocs_logger.propagate
    quiet_handler: logging.Handler | None = None
    if quiet:
        # MkDocs counts warnings for strict mode through a handler it attaches
        # to this logger inside build(); records must therefore keep flowing.
        # Quiet only stops them from reaching the console (root handlers or
        # logging's last-resort stderr handler).
        mkdocs_logger.propagate = False
        quiet_handler = logging.NullHandler()
        mkdocs_logger.addHandler(quiet_handler)
    # A plugin in the toolchain prints a long advisory about the MkDocs 2.0
    # transition on every build; it is noise for a pinned, tested toolchain.
    previous_advisory = os.environ.get(_MKDOCS_ADVISORY_ENV)
    os.environ[_MKDOCS_ADVISORY_ENV] = "true"
    started_at = time.perf_counter()
    try:
        with _working_directory(repo_root):
            config = load_config(config_file=str(config_path), site_dir=str(site_dir), strict=strict)
            build(config)
    except MkDocsException as error:
        raise DocsBuildError(f"mkdocs build failed: {error}") from error
    finally:
        mkdocs_logger.propagate = previous_propagate
        if quiet_handler is not None:
            mkdocs_logger.removeHandler(quiet_handler)
        if previous_advisory is None:
            os.environ.pop(_MKDOCS_ADVISORY_ENV, None)
        else:
            os.environ[_MKDOCS_ADVISORY_ENV] = previous_advisory

    html_files_written = sum(1 for path in site_dir.rglob("*.html") if path.is_file())
    return DocsBuildSummary(
        site_dir=site_dir,
        html_files_written=html_files_written,
        wall_time_seconds=time.perf_counter() - started_at,
        strict=strict,
    )