Pydantic models both ways
MeTTa projects structured Python values into constructor expressions, keeps the declarations that describe their fields, and rebuilds Python objects from those atoms. Pydantic models take the same path as dataclasses and plain annotated classes.
Start with a model whose fields are a string and a number:
def test_pydantic_models_project_like_dataclasses():
pydantic = pytest.importorskip("pydantic")
class Reading(pydantic.BaseModel):
sensor: str
value: float
projected = project(Reading(sensor="t1", value=21.5))
assert projected.atom == Expression(S.Reading, "t1", 21.5)
assert "(: Reading (-> String Number Reading))" in set(
map(str, projected.declarations)
)
rebuilt = build(projected.atom, Reading)
assert isinstance(rebuilt, Reading) and rebuilt.value == 21.5
# The rebuild runs through the model itself, so validation runs where
# pydantic runs it: a field refusing its type is pydantic's own error.
with pytest.raises(pydantic.ValidationError):
build(Expression(S.Reading, "t1", S.tall), Reading)project produces (Reading "t1" 21.5) and a constructor declaration. build sends the decoded fields back through Pydantic, so Pydantic owns validation on the return path.
Aliases do not replace Python attribute names during rebuilding:
def test_pydantic_alias_fields_rebuild(metta):
pydantic = pytest.importorskip("pydantic")
class Wire(pydantic.BaseModel):
internal: int = pydantic.Field(alias="external")
model_config = pydantic.ConfigDict(populate_by_name=True)
projected = project(Wire(external=7))
rebuilt = build(projected.atom, Wire)
assert isinstance(rebuilt, Wire) and rebuilt.internal == 7The integration uses Pydantic 2's model_fields and model_validate(..., by_name=True) APIs. Pydantic is optional and has no MeTTa packaging extra, so install Pydantic separately before using this path.
Projection refuses extra fields because dropping them would make the round trip lossy:
def test_pydantic_extra_fields_are_refused_by_name():
pydantic = pytest.importorskip("pydantic")
class ExtraRejectModel(pydantic.BaseModel):
value: int
model_config = pydantic.ConfigDict(extra="allow")
value = ExtraRejectModel(value=1, retained=2, also_retained=3)
with pytest.raises(
TypeError, match=r"extra fields would be lost \(also_retained, retained\)"
):
project(value)Once projected, the constructor fields are ordinary MeTTa structure. The Python-objects example queries one field and rebuilds the original class:
projected = project(Robot("R2", Mood.calm))
check("projection", str(projected.atom), '(Robot "R2" calm)')
m.add(*projected.declarations, projected.atom)
m.add(project(Robot("HAL", Mood.stormy)).atom)
rows = m.match(S.Robot(V.name, S.stormy))
check("match on parts", str(rows[0].name), '"HAL"')
rebuilt = build(projected.atom)
check("rebuild", isinstance(rebuilt, Robot) and rebuilt.mood, Mood.calm)Continue with Python functions as MeTTa functions, project, build, and Rows.build.