Auto-Registry Product
AutoRegistryProduct is a mixin for AutoRegistry subclasses whose
discriminator key must be composed from the discriminator values of other
AutoRegistry types, rather than being a single, manually assigned string.
Why It Exists
AutoRegistry normally identifies a concrete class by a static field like
kind: str = "my_thing". That works perfectly when registration is keyed on
one dimension.
BaseInteractionRenderer needs to be keyed on two dimensions
simultaneously: which transport it handles and which interaction it
handles. The key must be a combination such as "cli.text_prompt".
AutoRegistryProduct extracts that pattern into a generic mixin so that any
AutoRegistry subclass can declare a composite discriminator without
duplicating the resolution logic.
How It Works
registry_key Format
Instead of a bare field name, the base class declares registry_key in the
form:
"<field_name>:<attr1>.<attr2>…"
| Part | Meaning |
|---|---|
<field_name> | The Pydantic field on the class that will hold the derived string key |
<attr1>, <attr2> | ClassVar attributes on concrete subclasses whose values are other AutoRegistry subclasses |
At subclass definition time AutoRegistryProduct.__init_subclass__ reads each
attribute, looks up its registry_key field default from Pydantic
model_fields, joins those defaults with ., writes the result to
<field_name>, and then delegates to AutoRegistry.__init_subclass__ so
normal registration proceeds.
MRO Ordering Requirement
AutoRegistryProduct must appear before AutoRegistry in the base list:
class BaseInteractionRenderer(
AutoRegistryProduct, # runs first, derives the key
AutoRegistry, # runs second, reads the key and registers
entry_point="interaction_renderer",
):
...
Python's MRO guarantees that AutoRegistryProduct.__init_subclass__ commits
the derived key to the class before AutoRegistry.__init_subclass__ reads
it for registration.
Step-by-Step Resolution
Given a concrete subclass:
class MyCLITextRenderer(
BaseInteractionRenderer[CLITransport, TextPromptInteraction]
):
handles_transport: ClassVar[type[CLITransport]] = CLITransport
handles_interaction: ClassVar[type[TextPromptInteraction]] = TextPromptInteraction
async def render(self, transport, interaction): ...
registry_keyon the base is"render_key:handles_transport.handles_interaction".AutoRegistryProductsplits on the first:→field_name = "render_key",raw_attrs = "handles_transport.handles_interaction".- For
handles_transport→ resolves toCLITransport→ readsCLITransport.registry_key(in this renderer example,"kind") → then readsCLITransport.model_fields[CLITransport.registry_key].default→ e.g."cli". - For
handles_interaction→ resolves toTextPromptInteraction→ readsTextPromptInteraction.registry_key(in this renderer example,"kind") → then readsTextPromptInteraction.model_fields[TextPromptInteraction.registry_key].default→ e.g."text_prompt". - Sets
cls.render_key = "cli.text_prompt"andcls.registry_key = "render_key". AutoRegistry.__init_subclass__registersMyCLITextRendererunder the key"cli.text_prompt".
Error Cases
| Situation | Exception raised |
|---|---|
Class uses AutoRegistryProduct without inheriting AutoRegistry | TypeError |
registry_key does not contain : | ValueError |
A referenced ClassVar attribute does not exist on the class | ValueError |
The referenced AutoRegistry type has no non-empty string default for its registry key field | ValueError |
Extending AutoRegistryProduct
Any base class that needs a composite key can adopt the same pattern:
class BaseMyThing(
AutoRegistryProduct,
AutoRegistry,
entry_point="my_thing",
):
registry_key: ClassVar[str] = "key:source_a.source_b"
key: str | None = None
source_a: ClassVar[type[SomeRegistry]] # concrete subclasses set this
source_b: ClassVar[type[OtherRegistry]] # concrete subclasses set this
Concrete subclasses assign source_a and source_b; they never touch key
directly.