Forwarding registrations to another Modelable
The Modelable decorators (as_attribute, extends_union, extends_enum,
rebuilds_model) all act on the Modelable subclass they are called on. To
use them, the registering code must import that target model directly.
A ModelableForwarder lifts that requirement. It is a registration proxy:
every decorator invoked on it is delegated, unchanged, to a target Modelable,
so the target behaves exactly as if it had been decorated directly. This lets
a module register onto a core model it never imports, by going through a
forwarder that sits closer to it in the dependency graph.
Defining an extensible Model in your core project
Picking up the animal "shelter" project from the other How-Tos, we'll start
with a shelter module exposing a Shelter model to be extended:
from pydantic_modelable import Modelable
class Shelter(Modelable):
...
Exposing a forwarder from an extension
We'll now write a clinic extension, importing the shelter module to add a
veterinary Clinic facility to the Shelter. On top of that, the clinic
extension wants to let further extensions attach their own equipment to the
Shelter, without forcing them to depend on the core shelter module. To
that end, it exposes a forwarder bound to Shelter:
from pydantic import BaseModel
from pydantic_modelable import ModelableForwarder
from shelter import Shelter
@Shelter.as_attribute('clinic')
class Clinic(BaseModel):
veterinary: str
class ClinicForwarder(ModelableForwarder, forwards_to=Shelter):
...
Registering through the forwarder from a third party
A further xray extension can now equip the shelter with an imaging room. It
only depends on the clinic extension, and never imports the core shelter
module. Decorating through ClinicForwarder lands the field on Shelter just
as if Shelter.as_attribute had been used:
from pydantic import BaseModel
from clinic import ClinicForwarder
@ClinicForwarder.as_attribute('xray')
class XRayRoom(BaseModel):
model: str
| :memo: | The same holds for extends_union, extends_enum and rebuilds_model: the forwarder exposes the full Modelable registration surface. |
|---|---|
Chaining forwarders
A forwarder's forwards_to target may itself be another ModelableForwarder.
Registrations are then delegated down the chain until a concrete Modelable is
reached, so each intermediate layer can expose its own forwarder without
knowing the final target:
class ClinicForwarder(ModelableForwarder, forwards_to=Shelter):
...
class ImagingForwarder(ModelableForwarder, forwards_to=ClinicForwarder):
...
# Still lands on `Shelter`:
@ImagingForwarder.as_attribute('xray')
class XRayRoom(BaseModel):
model: str
Actual form of the modules (once extensions are loaded)
Once the shelter module is loaded along with both the clinic and xray
extensions, the Shelter model would be equivalent at runtime to the following
"hardcoded-style" definition:
from pydantic import BaseModel
from pydantic_modelable import Modelable
class Clinic(BaseModel):
veterinary: str
class XRayRoom(BaseModel):
model: str
class Shelter(Modelable):
clinic: Clinic
xray: XRayRoom