Skip to content

Commit 2bf597e

Browse files
committed
Implement default and reset for properties
This works on DataProperty and raises a specific exception for FunctionalProperty. It is not yet exposed over HTTP, but is tested in Python.
1 parent 90c0a60 commit 2bf597e

3 files changed

Lines changed: 129 additions & 1 deletion

File tree

src/labthings_fastapi/exceptions.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,3 +202,14 @@ class NotBoundToInstanceError(RuntimeError):
202202
generated from a `.Thing` class. Usually, they should be accessed via a
203203
`.Thing` instance, in which case they will be bound.
204204
"""
205+
206+
207+
class FeatureNotAvailable(NotImplementedError):
208+
"""A feature is not available.
209+
210+
There are some methods provided by base classes where implementation is optional.
211+
These methods raise `FeatureNotAvailable` if they are not implemented.
212+
213+
Currently this is done for the default value of properties, and their reset
214+
method.
215+
"""

src/labthings_fastapi/properties.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ class attribute. Documentation is in strings immediately following the
8383
FieldTypedBaseDescriptorInfo,
8484
)
8585
from .exceptions import (
86+
FeatureNotAvailable,
8687
NotConnectedToServerError,
8788
ReadOnlyPropertyError,
8889
MissingTypeError,
@@ -393,6 +394,38 @@ def model(self) -> type[BaseModel]:
393394
)
394395
return self._model
395396

397+
def default(self, obj: Owner | None) -> Value:
398+
"""Return the default value of this property.
399+
400+
:param obj: the `.Thing` instance on which we are looking for the default.
401+
or `None` if referring to the class. For now, this is ignored.
402+
403+
:return: the default value of this property.
404+
:raises FeatureNotAvailable: as this must be overridden.
405+
"""
406+
raise FeatureNotAvailable(
407+
f"{obj.name if obj else self.__class__}.{self.name} cannot be reset, "
408+
f"as it's not supported by {self.__class__}."
409+
)
410+
411+
def reset(self, obj: Owner) -> None:
412+
"""Reset the property's value to a default state.
413+
414+
If there is a defined default value for the property, this method
415+
should reset the property to that default.
416+
417+
Not every property is expected to implement ``reset`` so it is important
418+
to handle `.FeatureNotAvailable` exceptions, which will be raised if this
419+
method is not overridden.
420+
421+
:param thing: the `.Thing` instance we want to reset.
422+
:raises FeatureNotAvailable: as only some subclasses implement resetting.
423+
"""
424+
raise FeatureNotAvailable(
425+
f"{obj.name}.{self.name} cannot be reset, as it's not supported by "
426+
f"{self.__class__}."
427+
)
428+
396429
def add_to_fastapi(self, app: FastAPI, thing: Owner) -> None:
397430
"""Add this action to a FastAPI app, bound to a particular Thing.
398431
@@ -612,6 +645,23 @@ def __set__(
612645
if emit_changed_event:
613646
self.emit_changed_event(obj, value)
614647

648+
def default(self, obj: Owner | None) -> Value:
649+
"""Return the default value of this property.
650+
651+
Note that this implementation is independent of the `.Thing` instance,
652+
as there's currently no way to specify a per-instance default.
653+
654+
:return: the default value of this property.
655+
"""
656+
return self._default_factory()
657+
658+
def reset(self, obj: Owner) -> None:
659+
r"""Reset the property to its default value.
660+
661+
This resets to the value returned by ``default`` for `.DataProperty`\ .
662+
"""
663+
self.__set__(obj, self.default(obj))
664+
615665
def _observers_set(self, obj: Thing) -> WeakSet:
616666
"""Return the observers of this property.
617667
@@ -863,6 +913,25 @@ def model_instance(self) -> BaseModel: # noqa: DOC201
863913
raise TypeError(msg)
864914
return cls(root=value)
865915

916+
@builtins.property
917+
def default(self) -> Value:
918+
"""The default value of this property.
919+
920+
.. warning::
921+
Note that this is an optional feature, so calling code must handle
922+
`.FeatureNotAvailable` exceptions.
923+
"""
924+
return self.get_descriptor().default(self.owning_object)
925+
926+
def reset(self) -> None:
927+
"""Reset the property to a default value.
928+
929+
.. warning::
930+
Note that this is an optional feature, so calling code must handle
931+
`.FeatureNotAvailable` exceptions.
932+
"""
933+
return self.get_descriptor().reset(self.owning_object_or_error())
934+
866935
def validate(self, value: Any) -> Value:
867936
"""Use the validation logic in `self.model`.
868937

tests/test_property.py

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,12 @@
2525
default_factory_from_arguments,
2626
)
2727
from labthings_fastapi.base_descriptor import DescriptorAddedToClassTwiceError
28-
from labthings_fastapi.exceptions import MissingTypeError, NotConnectedToServerError
28+
from labthings_fastapi.exceptions import (
29+
FeatureNotAvailable,
30+
MissingTypeError,
31+
NotBoundToInstanceError,
32+
NotConnectedToServerError,
33+
)
2934
import labthings_fastapi as lt
3035
from labthings_fastapi.testing import create_thing_without_server
3136
from .utilities import raises_or_is_caused_by
@@ -456,3 +461,46 @@ def _set_funcprop(self, val: int) -> None:
456461
"ro_functional_property_with_setter",
457462
]:
458463
assert td.properties[name].readOnly is True
464+
465+
466+
def test_default_and_reset():
467+
"""Test retrieving property defaults, and resetting to default."""
468+
469+
class Example(lt.Thing):
470+
intprop: int = lt.property(default=42)
471+
listprop: list[str] = lt.property(default_factory=lambda: ["a", "list"])
472+
473+
@lt.property
474+
def strprop(self) -> str:
475+
return "Hello World!"
476+
477+
example = create_thing_without_server(Example)
478+
479+
# Defaults should be available on classes and instances
480+
for thing in [example, Example]:
481+
# We shoulld get expected values for defaults
482+
assert thing.properties["intprop"].default == 42
483+
assert thing.properties["listprop"].default == ["a", "list"]
484+
# Defaults are not available for FunctionalProperties
485+
with pytest.raises(FeatureNotAvailable):
486+
_ = thing.properties["strprop"].default
487+
488+
# Resetting to default isn't available on classes
489+
for name in ["intprop", "listprop", "strprop"]:
490+
with pytest.raises(NotBoundToInstanceError):
491+
thing.properties[name].reset()
492+
493+
# Resetting should work for DataProperty
494+
example.intprop = 43
495+
assert example.intprop == 43
496+
example.properties["intprop"].reset()
497+
assert example.intprop == 42
498+
499+
example.listprop = []
500+
assert example.listprop == []
501+
example.properties["listprop"].reset()
502+
assert example.listprop == ["a", "list"]
503+
504+
# Resetting won't work for FunctionalProperty
505+
with pytest.raises(FeatureNotAvailable):
506+
example.properties["strprop"].reset()

0 commit comments

Comments
 (0)