Reference documentation
Blurzy maintains two complementary references. Architecture is the beginner-friendly, visual explanation of responsibilities, workflows, and design. Follow Writing architecture guides for shared authoring and asset conventions. API reference is generated from Python source on every build. This avoids maintaining a second copy of module inventories and signatures, but does not make behavioral explanations automatically correct.
From source to page
flowchart TD
Source["Python files under src/app"] --> Hook["Hook: AST inventory and member selection"]
Hook --> Virtual["Virtual Markdown pages and navigation"]
Source --> Griffe["Griffe: static API and docstring collection"]
Virtual --> Directive["mkdocstrings resolves ::: module"]
Directive --> Griffe
Griffe --> Sections["Google-style sections and Markdown rendering"]
Sections --> HTML["HTML presented by Material"]
Reading the hook
This file is a MkDocs build hook, not a standalone script that writes
Markdown files. mkdocs.yml registers it under hooks; MkDocs loads it as
part of the build and calls its hook functions at specific points. The hook
uses the directory containing mkdocs.yml as the project root.
The important lifecycle is:
- Collect files and shape the menus (
on_files): scansrc/app, parse Python declarations withast, add virtual Markdown pages to MkDocs' in-memory file collection, and replace the two reference navigation trees. It also validates package-noteSource:mappings, adds their generated API links to the build-time page copy, and reservesreference/apifor generated pages. - Render each API page: MkDocs processes the generated
app...directives with mkdocstrings. Its Python handler uses Griffe to statically read source and docstrings; the hook itself does not render the API HTML. - Finish and preview:
on_page_contextremoves edit links from virtual pages;on_post_pageturnsapi-asset:references into depth-correct URLs and checks the asset exists. Duringmkdocs serve,on_servewatchessrc/appso source changes trigger rebuilds.
The script's helpers separate those steps:
| Code | Responsibility |
|---|---|
discover_modules, children, api_navigation, note_navigation |
Map source files and editorial notes into stable page paths and menus |
local_members, is_api_assignment |
Select module-level API names using syntax and naming conventions, without evaluating code |
module_page, detection_model_catalog, resolve_export |
Compose virtual page Markdown, include the model catalog, and link package re-exports to their definitions |
on_files |
Connect discovery to MkDocs: validate notes, inject generated files, set navigation, and hide the app wrapper from the menu |
on_page_context, on_post_page, on_serve |
Apply generated-page edit-link, asset-URL, and live-reload behavior |
To change API descriptions, edit Python docstrings. To change package
responsibility notes, edit the authored Markdown under docs/architecture/. Edit
the hook only when generation, selection, links, assets, or navigation logic
needs to change; never add authored Markdown under docs/reference/api/.
1. Discover modules and choose members
The hook in scripts/docs_reference.py scans src/app deterministically.
discover_modules() maps a module such as
src/app/domain/video/metadata.py to reference/api/app/domain/video/metadata/index.md.
A package's __init__.py maps to its package index. A module named index.py
gets its own directory, so it cannot collide with that package index.
Python's ast.parse() reads declarations without executing imports or
initializers. local_members() selects defining-module API members; it does
not turn method bodies into prose. Functions, classes, constants, and type
aliases become candidates. Runtime objects such as logger = get_logger(...)
are implementation wiring, not API declarations.
2. Create virtual Markdown
module_page() creates a readable title, qualified-module context, a source
link, and a mkdocstrings directive. For example:
"""Tools for labeling frames."""
from app.shared.logging_cfg import get_logger
DEFAULT_LABEL = "person"
logger = get_logger("Example")
def label_frame(frame_index: int, label: str = DEFAULT_LABEL) -> str:
"""Build a display label for a frame.
Args:
frame_index: Zero-based frame index.
label: Text shown beside the frame.
Returns:
A display label containing the index and text.
"""
return f"{frame_index}: {label}"
For this hypothetical app.example module, the generated page contains:
::: app.example
options:
members: ['DEFAULT_LABEL', 'label_frame']
The rendered page shows the constant, label_frame signature with annotations
and defaults, its summary, and parameter/return sections. It does not show
the logger initializer or the implementation's return statement.
File.generated() stores these Markdown pages in memory for the build.
Generated Markdown is not written into docs/reference. HTML and search
artifacts go to site/docs; neither generated Markdown nor HTML is committed.
The generated DetectionEngine page also includes supported model names from
app.infrastructure.detection.model.names. The hook reads the literal
registries with AST parsing, so the catalog stays synchronized without
importing the model stack or duplicating names in documentation.
3. Collect API information statically
When Markdown processing encounters app.example, mkdocstrings delegates
to its Python handler. Griffe, that handler's source-analysis library,
collects module/class/function declarations, annotations, defaults, decorators,
properties, attributes, and docstrings.
This is distinct from the hook's AST pass: the hook chooses pages and
module-level members, while Griffe supplies structured information for rendering
those members and their public class members. paths: [src] locates the package.
allow_inspection: false disables fallback runtime imports. Documentation
builds therefore do not need Qt, model weights, or the application's ML stack.
Signals are documented as declared class attributes; no Qt object is created. Static collection cannot infer actual signal connections or prove lifecycle guarantees. Document those explicitly.
Keep API-example images in docs/assets/api/ rather than embedding large SVGs
in docstrings. Reference them from rendered HTML with the api-asset: prefix,
for example src="api-asset:tracking/nested-boxes.svg". The hook resolves that
stable asset path after mkdocstrings has rendered the docstring, calculating a
relative URL for the generated page's depth and failing the build if the asset
is missing or escapes the API asset directory.
4. Parse docstrings and render HTML
The configured Google-style parser recognizes sections such as Args:,
Returns:, Raises:, and Attributes:; docstring_section_style: spacy
renders those sections as readable tables. Descriptive text and examples then
pass through Markdown rendering. Material supplies maintained typography,
syntax highlighting, responsive navigation, color modes, and API symbol styling.
Small member-kind labels identify properties and attributes; they are metadata,
not extra API declarations.
Indentation and Markdown syntax matter. An unfenced Python comment beginning
with # can be interpreted as a heading after docstring parsing. Code examples
must be fenced; a successful build alone does not prove that prose looks right.
Keep all class-docstring prose, including Layer: and Context: footers,
indented to the class body, and align the closing triple quotes with the
opening quotes. An unindented footer prevents common-indent removal and can
turn the preceding prose and Google-style sections into Markdown code blocks.
Keep the additional indentation inside sections and examples intact.
Use $...$ for inline LaTeX and $$ on separate lines for display equations.
pymdownx.arithmatex preserves the math for MathJax, loaded from a pinned CDN;
rendering equations therefore requires a network connection. Use raw Python
docstrings (r"""...""") when formulas contain backslashes, so commands such as
\frac and \times reach the renderer unchanged. Keep display equations
indented with their containing docstring section or admonition. Restart
mkdocs serve after changing configuration or hook code.
Member-selection guidelines
| Declaration | Default treatment |
|---|---|
| Public classes and functions | Included, even without docstrings |
| Public methods, properties, instance attributes, signals | Included |
| Constructors | Included |
| Private names, external imported members, inherited Qt APIs | Omitted |
| Uppercase module constants, including factory-created constants | Included |
Python type aliases and annotated TypeAlias declarations |
Included |
Conventional assignment aliases such as Margins = tuple[int, ...] |
Included |
Module-level runtime objects, including logger and log |
Omitted |
Prefer an explicit type declaration for new aliases. Assignment-style aliases
are recognized syntactically from capitalized alias names and conventional
type names, subscripts, and unions; the hook does not evaluate their values. Naming conventions alone cannot
perfectly distinguish every constant, type, and runtime object.
For ambiguous cases, use MEMBER_EXCEPTIONS to explicitly include a
module-level name and MEMBER_EXCLUSIONS to omit one. Both are keyed by the
fully qualified defining module; exclusions take precedence. Keep overrides
small and deliberate, and add a regression test. Do not rename application
objects merely to make documentation filtering work.
Class-member visibility is controlled by mkdocstrings filters. An explicitly authored directive can override visibility for a selected private API when there is a real documentation need.
Navigation and authored architecture pages
The API reference menu starts at app's children, without an app wrapper.
Its page inventory is automatic, including new root-level modules. Material's
index-page navigation makes package titles clickable without separate Overview
entries. The app package index remains reachable from the API landing page.
Existing generated URLs retain their reference/api/app/ prefix.
Public package re-exports link through internal import chains to defining-module pages instead of rendering duplicate API objects. Defining modules own canonical symbol anchors. External and cyclic re-export chains are not followed into runtime imports.
An authored architecture page can include a Source: mapping when it should
link to the corresponding generated API package:
# Detection engine
Source: `../../../src/app/infrastructure/detection/engine/`
Explain the workflow here.
Source paths can be relative to the note's directory, as above for
docs/architecture/infrastructure/detection/engine.md, or repository-relative
(src/app/infrastructure/detection/engine/).
The hook validates that package and adds an API link to the build-time copy
of the page, leaving the committed file unchanged. A Source mapping that names a
nonexistent package fails explicitly. Pages without a Source line remain
narrative-only. Architecture pages are grouped in navigation by their titles
and directories; only add pages that teach a useful workflow or design concept.
Generated pages link to the actual Python source and do not offer an edit link to nonexistent Markdown. Handwritten notes retain their usual edit links. When moving source packages, update affected notes' Source lines too.
Docstring and narrative authoring
Follow the project-wide docstring standard
for contracts, examples, and Material markup. Griffe parses Google-style
sections; Markdown extensions and Material render their content. Types normally
come from signature annotations, so repeating them in Args: is optional.
Material's reference
documents available features, while mkdocs.yml determines which work here.
Put API-level explanations beside the implementation:
- Module docstrings summarize purpose and boundaries.
- Class docstrings explain responsibilities, ownership, and important invariants.
- Method/function docstrings explain behavior, parameters, return values, errors, side effects, and guarantees when they are not obvious.
- Use Google-style sections with correctly indented entries; avoid restating annotations when a short explanation is sufficient.
- Use single Markdown backticks for inline code in new text.
- Use fenced
pythonblocks inExamples:; keep the fence and body at the same indentation within the section. - Use correctly indented Material admonitions for important context, keeping
ownership and safety warnings visible. Optional detail can use
???/???+. Put constructor arguments on the constructor rather than duplicating them on the class. Article front matter does not belong in source docstrings. - Explain live references versus copies, locking boundaries, side effects, and partial failures when relevant; an attractive rendering does not prove a behavioral guarantee. State example prerequisites and use qualified API links.
For example:
def seek(frame_index: int) -> None:
"""Request a frame seek.
Args:
frame_index: Zero-based target frame.
Examples:
```python
# Seek before requesting the next display frame.
seek(120)
```
"""
Keep cross-component workflows, lifecycle diagrams, architecture decisions, and signal-flow explanations in authored architecture guides. Link to API pages rather than duplicating signatures or inventories there.
Existing docstrings can be improved incrementally. Missing docstrings do not fail builds; stale or misleading prose still needs human review. Generation does not validate examples by executing them or infer behavior from code.
Development and checks
Install only documentation tools and start the live preview:
python -m pip install -r requirements-docs.txt
python -m mkdocs serve
Open the printed local URL. The hook watches src/app, so edits, additions,
renames, and removals rebuild the reference. Check the Architecture and API reference menus and
use the palette toggle to inspect light and dark modes.
Before submitting changes to documentation tooling:
python -m unittest discover -s tests -p "test_docs_reference.py"
python -m mkdocs build --strict
The Pages workflow runs those checks with documentation-only dependencies. Regular application tests skip the reference tests when that tooling is absent. Do not install application dependencies solely to build documentation.
Troubleshooting
| Symptom | Check |
|---|---|
| Missing module member | Visibility, runtime-assignment policy, explicit overrides, and defining-module location |
| Logger or singleton shown | Module-level assignment classification and exclusion override |
| Python comment becomes a heading | Fence the entire example, including comments, inside Examples: |
| Args/Returns show as plain text | Google-style section spelling and indentation |
| Build fails on a package note | Its Source line must name an existing src/app package |
| New module absent in preview | Source tree location and live-server rebuild output; restart after hook/config changes |
| Styles look like the old theme | Install the pinned requirements, restart preview, and refresh browser assets |
| Claimed behavior is incorrect | Correct the source docstring or narrative; generation reproduces prose, not its truth |