diff --git a/mex/common/models/base.py b/mex/common/models/base.py index 10e709e5..a6475550 100644 --- a/mex/common/models/base.py +++ b/mex/common/models/base.py @@ -185,8 +185,15 @@ def verify_computed_field_consistency( data with consistent computed fields. """ if not cls.model_computed_fields: + # no computed fields: exit early + return handler(data) + if isinstance(data, cls): + # data is a model instance: we can assume no computed field was set, + # because pydantic would throw an AttributeError if you tried return handler(data) if not isinstance(data, MutableMapping): + # data is not a dictionary: we can't "pop" values from that, + # so we can't safely do a before/after comparison raise AssertionError( "Input should be a valid dictionary, validating other types is not " "supported for models with computed fields." diff --git a/mex/common/organigram/transform.py b/mex/common/organigram/transform.py index 5e0defcf..2ee60da5 100644 --- a/mex/common/organigram/transform.py +++ b/mex/common/organigram/transform.py @@ -48,14 +48,7 @@ def transform_organigram_units_to_organizational_units( if parent_unit := extracted_unit_by_id_in_primary_source.get( parent_identifier_in_primary_source ): - # Create a copy, because extracted data instances are immutable - # because of `BaseEntity.verify_computed_field_consistency` - extracted_unit = ExtractedOrganizationalUnit.model_validate( - { - **extracted_unit.model_dump(), - "parentUnit": MergedOrganizationalUnitIdentifier( - parent_unit.stableTargetId - ), - } + extracted_unit.parentUnit = MergedOrganizationalUnitIdentifier( + parent_unit.stableTargetId ) yield extracted_unit diff --git a/tests/models/test_base.py b/tests/models/test_base.py index 622679ba..71157c29 100644 --- a/tests/models/test_base.py +++ b/tests/models/test_base.py @@ -89,13 +89,17 @@ class Shelter(Pet): Shelter(inhabitants="foo") # type: ignore -def test_verify_computed_field_consistency() -> None: - class Computer(BaseModel): +class Computer(BaseModel): + + ram: int = 16 + + @computed_field # type: ignore[misc] + @property + def cpus(self) -> int: + return 42 - @computed_field # type: ignore[misc] - @property - def cpus(self) -> int: - return 42 + +def test_verify_computed_field_consistency() -> None: computer = Computer.model_validate({"cpus": 42}) assert computer.cpus == 42 @@ -105,7 +109,7 @@ def cpus(self) -> int: match="Input should be a valid dictionary, validating other types is not " "supported for models with computed fields.", ): - Computer.model_validate(computer) + Computer.model_validate('{"cpus": 1}') with pytest.raises(ValidationError, match="Cannot set computed fields"): Computer.model_validate({"cpus": 1}) @@ -114,6 +118,19 @@ def cpus(self) -> int: Computer(cpus=99) +def test_field_assignment_on_model_with_computed_field() -> None: + computer = Computer() + + # computed field cannot be set + with pytest.raises( + AttributeError, match="property 'cpus' of 'Computer' object has no setter" + ): + computer.cpus = 99 + + # non-computed field works as expected + computer.ram = 32 + + class DummyBaseModel(BaseModel): foo: str | None = None