Skip to content

docs: MelleaPlugin and MelleaBasePayload missing from API coverage #667

Description

@planetf1

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    documentationImprovements or additions to documentation

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions