Skip to content

Commit 2fb64cf

Browse files
committed
Validate properties in __set__.
This uses the existing validation logic in `PropertyInfo` to check assignments made in Python as well as set operations over HTTP.
1 parent 90c0a60 commit 2fb64cf

3 files changed

Lines changed: 35 additions & 5 deletions

File tree

src/labthings_fastapi/properties.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -608,7 +608,8 @@ def __set__(
608608
:param value: the new value for the property.
609609
:param emit_changed_event: whether to emit a changed event.
610610
"""
611-
obj.__dict__[self.name] = value
611+
property_info = self.descriptor_info(obj)
612+
obj.__dict__[self.name] = property_info.validate(value)
612613
if emit_changed_event:
613614
self.emit_changed_event(obj, value)
614615

@@ -811,13 +812,19 @@ def instance_get(self, obj: Owner) -> Value:
811812
def __set__(self, obj: Owner, value: Value) -> None:
812813
"""Set the value of the property.
813814
815+
This will validate the value against the property's model, and an error
816+
will be raised if the value is not valid.
817+
814818
:param obj: the `.Thing` on which the attribute is accessed.
815819
:param value: the value of the property.
816820
817821
:raises ReadOnlyPropertyError: if the property cannot be set.
818822
"""
819823
if self.fset is None:
820824
raise ReadOnlyPropertyError(f"Property {self.name} of {obj} has no setter.")
825+
property_info = self.descriptor_info(obj)
826+
value = property_info.validate(value)
827+
821828
self.fset(obj, value)
822829

823830

tests/test_properties.py

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -378,21 +378,44 @@ def test_setting_without_event_loop():
378378

379379

380380
@pytest.mark.parametrize("prop_info", CONSTRAINED_PROPS)
381-
def test_constrained_properties(prop_info):
382-
"""Test that constraints on property values generate correct models."""
381+
def test_constrained_properties(prop_info, mocker):
382+
"""Test that constraints on property values generate correct models.
383+
384+
This also tests the `validate` method and checks validation happens
385+
on assignment to the property in Python. Further checks over http
386+
are made in later tests.
387+
"""
383388
prop = prop_info.prop
384389
assert prop.value_type is prop_info.value_type
385390
m = prop.model
386391
assert issubclass(m, RootModel)
392+
mock_thing = mocker.Mock(spec=PropertyTestThing)
393+
mock_thing._thing_server_interface = mocker.Mock()
394+
descriptorinfo = prop.descriptor_info(mock_thing)
395+
assert isinstance(descriptorinfo, PropertyInfo)
387396
for ann in prop_info.constraints:
388397
assert any(meta == ann for meta in m.model_fields["root"].metadata)
389398
for valid in prop_info.valid_values:
399+
# Check the model can be created
390400
instance = m(root=valid)
401+
# Check the value passes through the model
391402
validated = instance.model_dump()
392403
assert validated == valid or validated is valid # `is` for NaN
404+
# Check the descriptorinfo object also validates
405+
# (this is what we get from thing.properties["name"])
406+
validated = descriptorinfo.validate(valid)
407+
assert validated == valid or validated is valid # `is` for NaN
408+
# Check that assignment works
409+
prop.__set__(mock_thing, valid)
410+
validated = prop.__get__(mock_thing)
411+
assert validated == valid or validated is valid # `is` for NaN
393412
for invalid in prop_info.invalid_values:
394413
with pytest.raises(ValidationError):
395414
_ = m(root=invalid)
415+
with pytest.raises(ValidationError):
416+
descriptorinfo.validate(invalid)
417+
with pytest.raises(ValidationError):
418+
prop.__set__(mock_thing, invalid)
396419

397420

398421
def convert_inf_nan(value):

tests/test_property.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -365,8 +365,8 @@ class BadIntModel(pydantic.BaseModel):
365365
# Check that a broken `_model` raises the right error
366366
# See above for where we manually set badprop._model to something that's
367367
# not a rootmodel.
368-
example.badprop = 3
369-
assert example.badprop == 3
368+
with pytest.raises(TypeError):
369+
example.badprop = 3 # Validation will fail here because of the bad model.
370370
with pytest.raises(TypeError):
371371
_ = example.properties["badprop"].model_instance
372372
with pytest.raises(TypeError):

0 commit comments

Comments
 (0)