Hide navigation sidebar
Hide table of contents sidebar
Skip to content
Back to top
Toggle table of contents sidebar

# Source code for betty.documentation

    """
    Provide the Documentation API.
    """

    from contextlib import AsyncExitStack
    from pathlib import Path
    from tempfile import TemporaryDirectory
    from typing import final

    from aiofiles.os import makedirs
    from typing_extensions import override

    from betty import serve, fs
    from betty.fs import ROOT_DIRECTORY_PATH
    from betty.locale.localizer import Localizer
    from betty.os import copy_tree
    from betty.serve import Server, NoPublicUrlBecauseServerNotStartedError
    from betty.subprocess import run_process

    async def _prebuild_documentation() -> None:
        await _build(fs.PREBUILT_ASSETS_DIRECTORY_PATH / "documentation")

    async def _ensure_documentation_directory(cache_directory_path: Path) -> Path:
        if (fs.PREBUILT_ASSETS_DIRECTORY_PATH / "documentation").exists():
            return fs.PREBUILT_ASSETS_DIRECTORY_PATH / "documentation"
        cache_directory_path /= "documentation"
        if not cache_directory_path.exists():
            await _build(cache_directory_path)
        return cache_directory_path

    async def _build(output_directory_path: Path) -> None:
        await makedirs(output_directory_path, exist_ok=True)
        with TemporaryDirectory() as working_directory_path_str:
            working_directory_path = Path(working_directory_path_str)
            # sphinx-apidoc must output to the documentation directory, but because we do not want
            # to 'pollute' that with generated files that must not be committed, do our work in a
            # temporary directory and copy the documentation source files there.
            source_directory_path = working_directory_path / "source"
            await copy_tree(ROOT_DIRECTORY_PATH / "documentation", source_directory_path)
            await run_process(
                [
                    "sphinx-apidoc",
                    "--force",
                    "--separate",
                    "-d",
                    "999",
                    "-o",
                    str(source_directory_path),
                    str(ROOT_DIRECTORY_PATH / "betty"),
                    str(ROOT_DIRECTORY_PATH / "betty" / "tests"),
                ],
                cwd=working_directory_path,
            )
            await run_process(
                [
                    "sphinx-build",
                    "-b",
                    "dirhtml",
                    "-j",
                    "auto",
                    str(source_directory_path),
                    str(output_directory_path),
                ],
                cwd=working_directory_path,
            )

    @final
    class DocumentationServer(Server):
        """
        Serve the documentation site.
        """

        def __init__(
            self,
            cache_directory_path: Path,
            *,
            localizer: Localizer,
        ):
            super().__init__(localizer)
            self._cache_directory_path = cache_directory_path
            self._server: Server | None = None
            self._exit_stack = AsyncExitStack()

        @override
        @property
        def public_url(self) -> str:
            if self._server is not None:
                return self._server.public_url
            raise NoPublicUrlBecauseServerNotStartedError()

        @override
        async def start(self) -> None:
            www_directory_path = await _ensure_documentation_directory(
                self._cache_directory_path
            )
            self._server = serve.BuiltinServer(
                www_directory_path, localizer=self._localizer
            )
            await self._exit_stack.enter_async_context(self._server)

        @override
        async def stop(self) -> None:
            await self._exit_stack.aclose()