Skip to content

docs: fix the griffe docstring warnings and guard the API reference - #42

Merged
murilo-cunha merged 2 commits into
mainfrom
docs/griffe-attributes
Jul 30, 2026
Merged

docs: fix the griffe docstring warnings and guard the API reference#42
murilo-cunha merged 2 commits into
mainfrom
docs/griffe-attributes

Conversation

@murilo-cunha

Copy link
Copy Markdown
Member

The bug

The docs build emitted 86 griffe warnings (43 fields × 2 checks) across models.py, config.py and groups.py — and that was not just log noise. It was a live defect on the published site: api-reference/models/ rendered tool_calls_include with an empty type cell and marked it required, when it defaults to []. Every pydantic field on the published reference was untyped and falsely required.

Root cause

griffe is a static analyser. A Google-style Args: section in a class docstring is validated against Class.parameters, which is literally all_members["__init__"].parameters. Classes whose __init__ is generated at runtime have no static __init__, so that list is empty — every documented name trips both the "no type or annotation" and "does not appear in the function signature" checks, and the rendered table loses its type column.

Kind Classes Static __init__?
@dataclass Message, TurnContext, TranscriptResult, evaluators ✅ griffe's dataclasses extension synthesises one
pydantic BaseModel JudgeConfig, ToolCallArgsConfig, Expect, Turn, Transcript, AgentEvalConfig, GroupConfig
NamedTuple AgentReply
str subclass with only __new__ ToolCall

It went unnoticed locally because a warm ./.cache/ replays the cached api-reference/* render, so the handler never re-collects and the warnings stop printing entirely.

The fix

Args: documents a static signature; Attributes: documents fields whose constructor is generated at runtime. griffe's attributes parser resolves each type from the real class-body annotation.

  • The 8 runtime-constructed classes move to Attributes:.
  • @dataclass docstrings stay on Args: — griffe proves a real signature there, so they keep correct types and a defaults column. The resulting mix in models.py is deliberate.
  • ToolCall.name/args move onto __new__, where they are genuinely constructor parameters with real annotations.
  • Two Annotated fields get an explicit name (type): override, so the table shows list[Evaluator] and str | Path | None rather than the raw annotation blob — which also leaked the private _PathLike alias.

Rejected: silencing via warn_unknown_params = false (leaves the wrong required/missing types published, and hides genuine typos), and griffe-pydantic (only labels fields and swaps a template — it does not synthesise __init__, so it would not fix this on its own).

The CI guard is not --strict

The obvious guard does not work. zensical build --strict prints every griffe warning and then reports No issues found and exits 0 — verified by injecting a regression. At the source (zensical 0.0.51) the flag is forwarded to the Rust runtime, whose issue collector never sees warnings routed through Python's logging; zensical serve --strict is blunter still and prints "Strict mode is currently unsupported."

So this adds scripts/check_docstrings.py: load the package with griffe, force every docstring to parse, exit non-zero on any warning. --strict is kept for what it does catch (broken links, missing anchors), with a comment so nobody mistakes it for a docstring guard.

Known gap, documented in the script: the Attributes: parser never validates names against the class body, so a typo'd attribute name renders a phantom row silently rather than warning.

Verification

  • griffe warnings 86 → 0. The new guard exits 0 on this tree and exits 1 listing all 10 warnings when a single class is reverted to Args:.
  • Cold strict build (rm -rf .cache site first — --clean alone does not clear .cache): exit 0, no griffe: lines.
  • Rendered HTML: 116 data rows across all 4 API pages, 0 with an empty type cell. tool_calls_includelist[str] and no longer required; __new__name: str/required, args: ToolArgs | None/None. The remaining required markers are all genuinely required.
  • uv run pytest: 457 passed, 7 skipped. ruff/ruff format clean.
  • docs/llms-full.txt needs no regeneration — _SKIP_PREFIXES already excludes api-reference/.

No behaviour change: the source diff is docstring text only.

griffe validates a Google-style `Args:` section in a class docstring against
`Class.parameters`, which is literally `all_members["__init__"].parameters`.
Classes whose `__init__` is generated at runtime have no static `__init__`, so
that list is empty and every documented name trips both the "no type or
annotation" and "does not appear in the function signature" checks -- 86
warnings across models.py, config.py and groups.py.

The damage was not just log noise: with `Class.parameters` empty the rendered
reference table had no type column and marked every pydantic field `required`,
including ones that default to `None` or `[]`. That was live on the published
site.

Switch the affected classes to `Attributes:`, which griffe resolves from the
real class-body annotation. The @DataClass docstrings stay on `Args:` --
griffe's built-in dataclasses extension synthesises an `__init__` there, so they
keep both correct types and a defaults column. The resulting mix is deliberate.

`ToolCall.name`/`args` move onto `__new__`, where they are genuinely constructor
parameters with real annotations.

Two `Annotated` fields get an explicit `name (type):` override so the table
shows `list[Evaluator]` and `str | Path | None` instead of the raw annotation
blob, which also leaked the private `_PathLike` alias.

This went unnoticed locally because a warm ./.cache/ replays the cached
api-reference render, so the handler never re-collects and the warnings stop
printing entirely.
The obvious guard does not work. `zensical build --strict` prints every griffe
warning and then reports "No issues found" and exits 0 -- verified against
zensical 0.0.51, where the flag is forwarded to the Rust runtime whose issue
collector never sees warnings routed through Python's logging. `zensical serve
--strict` is blunter still: it prints "Strict mode is currently unsupported."

A local build is no guard either, because a warm ./.cache/ replays the cached
api-reference render without re-collecting, so the warnings stop printing
entirely -- which is how 86 of them survived long enough to reach the published
site.

So add scripts/check_docstrings.py: load the package with griffe, force every
docstring to parse, and exit non-zero on any warning. Verified both ways --
exits 0 on the current tree, and exits 1 listing all 10 warnings when a single
class is reverted to `Args:`.

Keep --strict for what it does catch (zensical's own broken links and missing
anchors), with a comment so nobody mistakes it for a docstring guard.
@murilo-cunha
murilo-cunha merged commit 512734f into main Jul 30, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant