Skip to content

Commit 17d48d7

Browse files
authored
docs: fix MelleaPlugin/MelleaBasePayload missing from API coverage (#… (#670)
* docs: fix MelleaPlugin/MelleaBasePayload missing from API coverage (#667) Replace dual if/else class definitions with dynamic base classes so Griffe's static AST parser sees a single ClassDef node per class and always picks up the authoritative docstring. * docs: convert intrinsic core docstrings to Google style The Sphinx :param:/:return: style is not recognised by Griffe's quality audit. Convert to Google Args/Returns sections.
1 parent a0e2a46 commit 17d48d7

2 files changed

Lines changed: 134 additions & 96 deletions

File tree

mellea/plugins/base.py

Lines changed: 123 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,13 @@ async def __aexit__(
120120
_HAS_PLUGIN_FRAMEWORK = False
121121

122122
if TYPE_CHECKING:
123+
from cpex.framework.base import Plugin as _CpexPlugin
124+
from cpex.framework.models import (
125+
PluginContext,
126+
PluginPayload,
127+
PluginResult as _CFPluginResult,
128+
)
129+
123130
from mellea.core.backend import Backend
124131
from mellea.core.base import Context
125132
from mellea.stdlib.session import MelleaSession
@@ -146,119 +153,146 @@ def __init__( # noqa: D107
146153
super().__init__(f"Plugin blocked {hook_type}: {detail}{reason}")
147154

148155

149-
if _HAS_PLUGIN_FRAMEWORK:
156+
# ---------------------------------------------------------------------------
157+
# Dynamic base classes — a single ClassDef node per class ensures Griffe's
158+
# static AST parser always picks up the authoritative docstring. See #667.
159+
# ---------------------------------------------------------------------------
150160

151-
class MelleaBasePayload(PluginPayload):
152-
"""Frozen base — all payloads are immutable by design.
161+
if _HAS_PLUGIN_FRAMEWORK or TYPE_CHECKING:
162+
_PayloadBase = PluginPayload
163+
_PluginBase = _CpexPlugin
164+
else:
165+
_PayloadBase = object
166+
_PluginBase = object
153167

154-
Plugins must use ``model_copy(update={...})`` to propose modifications
155-
and return the copy via ``PluginResult.modified_payload``. The plugin
156-
manager applies the hook's ``HookPayloadPolicy`` to filter changes to
157-
writable fields only.
158-
"""
159168

160-
session_id: str | None = None
161-
request_id: str = ""
162-
timestamp: datetime = Field(default_factory=lambda: datetime.now(UTC))
163-
hook: str = ""
164-
user_metadata: dict[str, Any] = Field(default_factory=dict)
169+
class MelleaBasePayload(_PayloadBase): # type: ignore[misc,valid-type]
170+
"""Frozen base — all payloads are immutable by design.
165171
166-
class MelleaPlugin(_CpexPlugin):
167-
"""Base class for Mellea plugins with lifecycle hooks and typed accessors.
172+
Plugins must use ``model_copy(update={...})`` to propose modifications
173+
and return the copy via ``PluginResult.modified_payload``. The plugin
174+
manager applies the hook's ``HookPayloadPolicy`` to filter changes to
175+
writable fields only.
176+
"""
168177

169-
Use this when you need lifecycle hooks (``initialize``/``shutdown``)
170-
or typed context accessors. For simpler plugins, prefer ``@hook``
171-
on standalone functions or ``@plugin`` on plain classes.
178+
session_id: str | None = None
179+
request_id: str = ""
180+
timestamp: datetime = Field(default_factory=lambda: datetime.now(UTC))
181+
hook: str = ""
182+
user_metadata: dict[str, Any] = Field(default_factory=dict)
172183

173-
Instances support the context manager protocol for temporary activation::
174184

175-
class MyPlugin(MelleaPlugin):
176-
def __init__(self):
177-
super().__init__(PluginConfig(name="my-plugin", hooks=[...]))
185+
class MelleaPlugin(_PluginBase): # type: ignore[misc,valid-type]
186+
"""Base class for Mellea plugins with lifecycle hooks and typed accessors.
178187
179-
async def some_hook(self, payload, ctx):
180-
...
188+
Use this when you need lifecycle hooks (``initialize``/``shutdown``)
189+
or typed context accessors. For simpler plugins, prefer ``@hook``
190+
on standalone functions or ``@plugin`` on plain classes.
181191
182-
with MyPlugin() as p:
183-
result, ctx = instruct("...", ctx, backend)
192+
Instances support the context manager protocol for temporary activation::
184193
185-
# or async
186-
async with MyPlugin() as p:
187-
result, ctx = await ainstruct("...", ctx, backend)
188-
"""
194+
class MyPlugin(MelleaPlugin):
195+
def __init__(self):
196+
super().__init__(PluginConfig(name="my-plugin", hooks=[...]))
189197
190-
def get_backend(self, context: PluginContext) -> Backend | None:
191-
"""Get the Backend from the plugin context."""
192-
return context.global_context.state.get("backend")
198+
async def some_hook(self, payload, ctx):
199+
...
193200
194-
def get_mellea_context(self, context: PluginContext) -> Context | None:
195-
"""Get the Mellea Context from the plugin context."""
196-
return context.global_context.state.get("context")
201+
with MyPlugin() as p:
202+
result, ctx = instruct("...", ctx, backend)
197203
198-
def get_session(self, context: PluginContext) -> MelleaSession | None:
199-
"""Get the MelleaSession from the plugin context."""
200-
return context.global_context.state.get("session")
204+
# or async
205+
async with MyPlugin() as p:
206+
result, ctx = await ainstruct("...", ctx, backend)
207+
"""
201208

202-
@property
203-
def plugin_config(self) -> dict[str, Any]:
204-
"""Plugin-specific configuration from PluginConfig.config."""
205-
return self._config.config or {}
209+
def get_backend(self, context: PluginContext) -> Backend | None:
210+
"""Get the Backend from the plugin context.
206211
207-
def __enter__(self) -> MelleaPlugin:
208-
"""Register this plugin for the duration of a ``with`` block."""
209-
if getattr(self, "_scope_id", None) is not None:
210-
raise RuntimeError(
211-
f"MelleaPlugin {self.name!r} is already active as a context manager. "
212-
"Concurrent or nested reuse of the same instance is not supported; "
213-
"create a new instance instead."
214-
)
215-
import uuid
212+
Args:
213+
context: The plugin context provided by the hook framework.
216214
217-
from mellea.plugins.registry import register
215+
Returns:
216+
The active Backend, or ``None`` if unavailable.
217+
"""
218+
return context.global_context.state.get("backend")
218219

219-
self._scope_id = str(uuid.uuid4())
220-
register(self, session_id=self._scope_id)
221-
return self
220+
def get_mellea_context(self, context: PluginContext) -> Context | None:
221+
"""Get the Mellea Context from the plugin context.
222222
223-
def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
224-
"""Deregister this plugin on exit."""
225-
scope_id = getattr(self, "_scope_id", None)
226-
if scope_id is not None:
227-
from mellea.plugins.manager import deregister_session_plugins
223+
Args:
224+
context: The plugin context provided by the hook framework.
228225
229-
deregister_session_plugins(scope_id)
230-
self._scope_id = None # type: ignore[assignment]
226+
Returns:
227+
The active Mellea Context, or ``None`` if unavailable.
228+
"""
229+
return context.global_context.state.get("context")
231230

232-
async def __aenter__(self) -> MelleaPlugin:
233-
"""Async variant — delegates to the synchronous ``__enter__``."""
234-
return self.__enter__()
231+
def get_session(self, context: PluginContext) -> MelleaSession | None:
232+
"""Get the MelleaSession from the plugin context.
235233
236-
async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
237-
"""Async variant — delegates to the synchronous ``__exit__``."""
238-
self.__exit__(exc_type, exc_val, exc_tb)
234+
Args:
235+
context: The plugin context provided by the hook framework.
239236
240-
PluginResult: TypeAlias = _CFPluginResult # type: ignore[misc]
237+
Returns:
238+
The active MelleaSession, or ``None`` if unavailable.
239+
"""
240+
return context.global_context.state.get("session")
241241

242-
else:
243-
# Provide a stub when the plugin framework is not installed.
244-
class MelleaBasePayload: # type: ignore[no-redef]
245-
"""Stub — install ``"mellea[hooks]"`` for full plugin support."""
246-
247-
def __init__(self, *args: Any, **kwargs: Any) -> None: # noqa: D107
248-
raise ImportError(
249-
"MelleaPlugin requires the ContextForge plugin framework. "
250-
"Install it with: pip install 'mellea[hooks]'"
242+
@property
243+
def plugin_config(self) -> dict[str, Any]:
244+
"""Plugin-specific configuration from PluginConfig.config."""
245+
return self._config.config or {}
246+
247+
def __enter__(self) -> MelleaPlugin:
248+
"""Register this plugin for the duration of a ``with`` block."""
249+
if getattr(self, "_scope_id", None) is not None:
250+
raise RuntimeError(
251+
f"MelleaPlugin {self.name!r} is already active as a context manager. "
252+
"Concurrent or nested reuse of the same instance is not supported; "
253+
"create a new instance instead."
251254
)
255+
import uuid
252256

253-
# Provide a stub when the plugin framework is not installed.
254-
class MelleaPlugin: # type: ignore[no-redef]
255-
"""Stub — install ``"mellea[hooks]"`` for full plugin support."""
257+
from mellea.plugins.registry import register
256258

257-
def __init__(self, *args: Any, **kwargs: Any) -> None: # noqa: D107
258-
raise ImportError(
259-
"MelleaPlugin requires the ContextForge plugin framework. "
260-
"Install it with: pip install 'mellea[hooks]'"
261-
)
259+
self._scope_id = str(uuid.uuid4())
260+
register(self, session_id=self._scope_id)
261+
return self
262+
263+
def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
264+
"""Deregister this plugin on exit."""
265+
scope_id = getattr(self, "_scope_id", None)
266+
if scope_id is not None:
267+
from mellea.plugins.manager import deregister_session_plugins
268+
269+
deregister_session_plugins(scope_id)
270+
self._scope_id = None # type: ignore[assignment]
262271

263-
# Provide an alias when the plugin framework is not installed.
264-
PluginResult: TypeAlias = Any # type: ignore[no-redef, misc]
272+
async def __aenter__(self) -> MelleaPlugin:
273+
"""Async variant — delegates to the synchronous ``__enter__``."""
274+
return self.__enter__()
275+
276+
async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
277+
"""Async variant — delegates to the synchronous ``__exit__``."""
278+
self.__exit__(exc_type, exc_val, exc_tb)
279+
280+
281+
# When cpex is not installed, override __init__ to provide a clear error
282+
# message. This is done post-hoc so there is only one ClassDef node in the
283+
# AST for each class (the docstring-carrying definition above).
284+
if not _HAS_PLUGIN_FRAMEWORK:
285+
286+
def _stub_init(_self: Any, *_args: Any, **_kwargs: Any) -> None:
287+
raise ImportError(
288+
"This class requires the ContextForge plugin framework. "
289+
"Install it with: pip install 'mellea[hooks]'"
290+
)
291+
292+
MelleaBasePayload.__init__ = _stub_init # type: ignore[assignment,misc]
293+
MelleaPlugin.__init__ = _stub_init # type: ignore[assignment,misc]
294+
295+
if _HAS_PLUGIN_FRAMEWORK:
296+
PluginResult: TypeAlias = _CFPluginResult # type: ignore[misc]
297+
else:
298+
PluginResult: TypeAlias = Any # type: ignore[no-redef,misc]

mellea/stdlib/components/intrinsic/core.py

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,12 @@ def check_certainty(context: ChatContext, backend: AdapterMixin) -> float:
1313
assistant's response to a user's question. The context should end with
1414
a user question followed by an assistant answer.
1515
16-
:param context: Chat context containing user question and assistant answer.
17-
:param backend: Backend instance that supports LoRA/aLoRA adapters.
16+
Args:
17+
context: Chat context containing user question and assistant answer.
18+
backend: Backend instance that supports LoRA/aLoRA adapters.
1819
19-
:return: Certainty score as a float (higher = more certain).
20+
Returns:
21+
Certainty score as a float (higher = more certain).
2022
"""
2123
result_json = call_intrinsic("uncertainty", context, backend)
2224
return result_json["certainty"]
@@ -40,11 +42,13 @@ def requirement_check(
4042
requirements. Appends an evaluation prompt to the context following
4143
the format specified by the Granite Guardian requirement checker model card.
4244
43-
:param context: Chat context containing user question and assistant answer.
44-
:param backend: Backend instance that supports LoRA/aLoRA adapters.
45-
:param requirement: set of requirements to satisfy
45+
Args:
46+
context: Chat context containing user question and assistant answer.
47+
backend: Backend instance that supports LoRA/aLoRA adapters.
48+
requirement: Set of requirements to satisfy.
4649
47-
:return: Score as a float between 0.0 and 1.0 (higher = more likely satisfied).
50+
Returns:
51+
Score as a float between 0.0 and 1.0 (higher = more likely satisfied).
4852
"""
4953
eval_message = f"<requirements>: {requirement}\n{_EVALUATION_PROMPT}"
5054
context = context.add(Message("user", eval_message))

0 commit comments

Comments
 (0)