Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion ops/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -1873,6 +1873,10 @@ def _add_transfer(self):
# data.destination will be stored under the Juju relation key 'to'
relation.save(data, self.unit)

If a Pydantic model's ``model_dump`` method omits any field (e.g. if its
value is Pydantic's ``MISSING`` sentinel) the field will be erased from
the relation data.

Args:
obj: an object with attributes to save to the relation data, typically
a Pydantic ``BaseModel`` subclass or dataclass.
Expand Down Expand Up @@ -1915,7 +1919,11 @@ def _add_transfer(self):
values = {field: getattr(obj, field) for field in fields}

# Encode each value, and then pass it over to Juju.
data = {field: encoder(values[attr]) for attr, field in sorted(fields.items())}
# Missing values are erased from the databag using empty string values.
data = {
field: encoder(values[attr]) if attr in values else ''
for attr, field in sorted(fields.items())
}
self.data[dst].update(data)


Expand Down
33 changes: 33 additions & 0 deletions test/test_model_relation_data_class.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@
except ImportError:
pydantic = None

try:
from pydantic.experimental.missing_sentinel import MISSING
except ImportError:
MISSING = None # type: ignore

import ops
from ops import testing

Expand Down Expand Up @@ -213,6 +218,34 @@ def databag_class(self) -> type[DatabagProtocol]:
_test_classes.extend((MyPydanticDataclassCharm, MyPydanticBaseModelCharm))


if pydantic and MISSING is not None:

class MissingPydanticDatabag(pydantic.BaseModel):
foo: str
bar: int = pydantic.Field(default=0, ge=0)
baz: list[str] = pydantic.Field(default_factory=list)
quux: Nested = pydantic.Field(default_factory=Nested)
miss: str | MISSING = MISSING # type: ignore

@pydantic.field_validator('baz')
@classmethod
def check_foo_not_in_baz(cls, baz: list[str], values: Any):
data = cast('dict[str, Any]', values.data)
foo = data.get('foo')
if foo in baz:
raise ValueError('foo cannot be in baz')
return baz

model_config = pydantic.ConfigDict(validate_assignment=True)

class MissingPydanticBaseModelCharm(BaseTestCharm):
@property
def databag_class(self) -> type[DatabagProtocol]:
return MissingPydanticDatabag

_test_classes.append(MissingPydanticBaseModelCharm)


@pytest.mark.parametrize('charm_class', _test_classes)
def test_relation_load_simple(charm_class: type[BaseTestCharm]):
class Charm(charm_class):
Expand Down