From 3a93c18d99a7bb2a5dd9e4762262ccee4a0e8715 Mon Sep 17 00:00:00 2001 From: Pascal Tomecek Date: Wed, 27 May 2026 14:00:26 -0400 Subject: [PATCH] Restore NullContext as a distinct class with virtual subclass semantics Previously `NullContext` was a direct alias of `ContextBase`. This preserved substitutability (any context could be passed where a `NullContext` was expected) but degraded introspection: `.context_type.__name__` reported "ContextBase" with the abstract base's docstring, which was confusing for users of models that declare `context: NullContext`. Reintroduce `NullContext` as a distinct subclass of `ContextBase` whose metaclass lies in `__instancecheck__` / `__subclasscheck__` so that any `ContextBase` instance/subclass virtually satisfies `NullContext`. The lie is gated to `NullContext` itself, so user-defined subclasses follow normal class-hierarchy rules. Pydantic's v2 instance fast-path in `model_validate` respects the lie, so original context instances flow through unchanged. Net effect: zero behavior change at runtime; introspection now reports `NullContext` with its own docstring. Signed-off-by: Pascal Tomecek --- ccflow/context.py | 34 ++++++++++++++++++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/ccflow/context.py b/ccflow/context.py index 9a04fad..67b4b80 100644 --- a/ccflow/context.py +++ b/ccflow/context.py @@ -86,8 +86,38 @@ _SEPARATOR = "," -# Starting 0.8.0 Nullcontext is an alias to ContextBase -NullContext = ContextBase + +class _NullContextMeta(type(ContextBase)): + """Metaclass that makes ``NullContext`` (and only ``NullContext``) a virtual superclass + of every ``ContextBase`` subclass. + + A model annotated ``context: NullContext`` should accept any context. We achieve that + by lying in ``isinstance`` / ``issubclass`` while keeping ``NullContext`` a distinct + class so that introspection (``.context_type.__name__``, docstring, schema title) + reports ``NullContext`` rather than ``ContextBase``. + + The lie applies only when the comparison target is ``NullContext`` itself; user-defined + subclasses of ``NullContext`` fall through to normal class-hierarchy rules. + """ + + def __instancecheck__(cls, obj): + if cls is globals().get("NullContext"): + return isinstance(type(obj), type) and ContextBase in type(obj).__mro__ + return super().__instancecheck__(obj) + + def __subclasscheck__(cls, sub): + if cls is globals().get("NullContext"): + return isinstance(sub, type) and ContextBase in sub.__mro__ + return super().__subclasscheck__(sub) + + +class NullContext(ContextBase, metaclass=_NullContextMeta): + """A Null Context, used when no context is required. + + Any context can be passed where a NullContext is expected, but the model + should not rely on any of its fields. + """ + C = TypeVar("C", bound=Hashable)