Description
audit_coverage.py reports a slight drop in API documentation coverage, currently sitting at 99.45% (363/365 symbols). Two key symbols are missing from the generated documentation:
mellea.plugins.base: MelleaBasePayload
mellea.plugins.base: MelleaPlugin
Currently, these classes are defined conditionally in mellea/plugins/base.py:
if _HAS_PLUGIN_FRAMEWORK:
class MelleaBasePayload(PluginPayload):
"""Frozen base — all payloads are immutable by design. ..."""
...
class MelleaPlugin(_CpexPlugin):
"""Base class for Mellea plugins with lifecycle hooks ..."""
...
else:
class MelleaBasePayload: # stub
"""Stub — install "mellea[hooks]" for full plugin support."""
...
class MelleaPlugin: # stub
"""Stub — install "mellea[hooks]" for full plugin support."""
...
Root Cause
The documentation generators (audit_coverage.py and mdxify) rely on Griffe for code inspection. Griffe performs static AST (Abstract Syntax Tree) parsing rather than dynamic runtime introspection.
When Griffe encounters an if/else block, it does not evaluate the conditional. Instead, it sequentially parses both branches. Because Griffe stores parsed symbols in a namespace dictionary, a later definition of a class overwrites an earlier one.
In the current structure, the fallback stubs (with the minimal """Stub...""" docstrings) are in the else branch, meaning they are parsed last. They win the AST namespace collision. mdxify then correctly identifies these minimal docstrings as trivial and filters the classes out of the API coverage entirely.
Installing cpex at build time does not resolve this because Griffe's static parser never executes the conditional to know which branch is active.
Proposed Solutions
To avoid the manual maintenance burden and sync-drift of duplicating real docstrings across both branches, an automated structural fix is required.
Option 1: Branch Inversion (The AST Traversal Hack)
By inverting the conditional logic, Griffe is forced to parse the fallback stubs first, and the fully documented implementations last.
if not _HAS_PLUGIN_FRAMEWORK:
class MelleaBasePayload: # stub
"""Stub — install "mellea[hooks]" for full plugin support."""
...
class MelleaPlugin: # stub
...
else:
class MelleaBasePayload(PluginPayload):
"""Frozen base — all payloads are immutable by design. ..."""
...
class MelleaPlugin(_CpexPlugin):
"""Base class for Mellea plugins with lifecycle hooks ..."""
...
- Pros: Minimal code change; immediately fixes the documentation coverage without duplicating text.
- Cons: Fragile. It relies on Griffe's undocumented AST traversal behavior. If Griffe changes how it parses
if/else blocks, the coverage issue will silently regress.
Option 2: Dynamic Base Classes (Recommended)
Instead of conditionally defining the classes themselves, conditionally define their base classes. This ensures there is only ever a single ClassDef node in the AST for Griffe to parse, guaranteeing it picks up the authoritative docstring. Pair this with typing.TYPE_CHECKING to maintain support for static type checkers and IDEs.
from typing import TYPE_CHECKING
if _HAS_PLUGIN_FRAMEWORK or TYPE_CHECKING:
_PayloadBase = PluginPayload
_PluginBase = _CpexPlugin
else:
_PayloadBase = object
_PluginBase = object
class MelleaBasePayload(_PayloadBase):
"""Frozen base — all payloads are immutable by design. ..."""
...
class MelleaPlugin(_PluginBase):
"""Base class for Mellea plugins with lifecycle hooks ..."""
...
- Pros: Architecturally robust. It completely eliminates AST collisions, avoids docstring duplication, and explicitly manages typing behavior for static analyzers.
- Cons: Slightly abstracts the inheritance chain visually, though IDE autocompletion remains fully functional.
Description
audit_coverage.pyreports a slight drop in API documentation coverage, currently sitting at 99.45% (363/365 symbols). Two key symbols are missing from the generated documentation:mellea.plugins.base: MelleaBasePayloadmellea.plugins.base: MelleaPluginCurrently, these classes are defined conditionally in
mellea/plugins/base.py:Root Cause
The documentation generators (
audit_coverage.pyandmdxify) rely on Griffe for code inspection. Griffe performs static AST (Abstract Syntax Tree) parsing rather than dynamic runtime introspection.When Griffe encounters an
if/elseblock, it does not evaluate the conditional. Instead, it sequentially parses both branches. Because Griffe stores parsed symbols in a namespace dictionary, a later definition of a class overwrites an earlier one.In the current structure, the fallback stubs (with the minimal
"""Stub..."""docstrings) are in theelsebranch, meaning they are parsed last. They win the AST namespace collision.mdxifythen correctly identifies these minimal docstrings as trivial and filters the classes out of the API coverage entirely.Installing
cpexat build time does not resolve this because Griffe's static parser never executes the conditional to know which branch is active.Proposed Solutions
To avoid the manual maintenance burden and sync-drift of duplicating real docstrings across both branches, an automated structural fix is required.
Option 1: Branch Inversion (The AST Traversal Hack)
By inverting the conditional logic, Griffe is forced to parse the fallback stubs first, and the fully documented implementations last.
if/elseblocks, the coverage issue will silently regress.Option 2: Dynamic Base Classes (Recommended)
Instead of conditionally defining the classes themselves, conditionally define their base classes. This ensures there is only ever a single
ClassDefnode in the AST for Griffe to parse, guaranteeing it picks up the authoritative docstring. Pair this withtyping.TYPE_CHECKINGto maintain support for static type checkers and IDEs.