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
4 changes: 4 additions & 0 deletions src/labthings_fastapi/properties.py
Original file line number Diff line number Diff line change
Expand Up @@ -460,7 +460,7 @@
:return: the default value of this property.
:raises FeatureNotAvailableError: as this must be overridden.
"""
raise FeatureNotAvailableError(

Check warning on line 463 in src/labthings_fastapi/properties.py

View workflow job for this annotation

GitHub Actions / coverage

463 line is not covered with tests
f"{obj.name if obj else self.__class__}.{self.name} can't return a "
f"default, as it's not supported by {self.__class__}."
)
Expand All @@ -478,7 +478,7 @@
:param obj: the `~lt.Thing` instance we want to reset.
:raises FeatureNotAvailableError: as only some subclasses implement resetting.
"""
raise FeatureNotAvailableError(

Check warning on line 481 in src/labthings_fastapi/properties.py

View workflow job for this annotation

GitHub Actions / coverage

481 line is not covered with tests
f"{obj.name}.{self.name} cannot be reset, as it's not supported by "
f"{self.__class__}."
)
Expand Down Expand Up @@ -558,7 +558,7 @@
),
)
def reset() -> None:
self.reset(thing)

Check warning on line 561 in src/labthings_fastapi/properties.py

View workflow job for this annotation

GitHub Actions / coverage

561 line is not covered with tests

def property_affordance(
self, thing: Owner, path: str | None = None
Expand Down Expand Up @@ -855,12 +855,16 @@
super().__init__(constraints=constraints)
self._fget = fget
self._type = return_type(self._fget)
if fget.__doc__:
# If there is a docstring on the getter, use it as the property's docstring.
# BaseDescriptor parses __doc__ to generate the title and description.
self.__doc__ = fget.__doc__
if self._type is None:
msg = (

Check warning on line 863 in src/labthings_fastapi/properties.py

View workflow job for this annotation

GitHub Actions / coverage

863 line is not covered with tests
f"{fget} does not have a valid type. "
"Return type annotations are required for property getters."
)
raise MissingTypeError(msg)

Check warning on line 867 in src/labthings_fastapi/properties.py

View workflow job for this annotation

GitHub Actions / coverage

867 line is not covered with tests
self._fset: Callable[[Owner, Value], None] | None = None # setter function
# `_freset` should reset the property to its default value.
self._freset: (
Expand Down Expand Up @@ -895,10 +899,10 @@
:param fget: The new getter function.
:return: this descriptor (i.e. ``self``). This allows use as a decorator.
"""
self._fget = fget
self._type = return_type(self._fget)
self.__doc__ = fget.__doc__
return self

Check warning on line 905 in src/labthings_fastapi/properties.py

View workflow job for this annotation

GitHub Actions / coverage

902-905 lines are not covered with tests

def setter(self, fset: Callable[[Owner, Value], None]) -> Self:
r"""Set the setter function of the property.
Expand Down Expand Up @@ -970,7 +974,7 @@
# Don't return the descriptor if it's named differently.
# see typing notes in docstring.
return fset # type: ignore[return-value]
return self

Check warning on line 977 in src/labthings_fastapi/properties.py

View workflow job for this annotation

GitHub Actions / coverage

977 line is not covered with tests

def instance_get(self, obj: Owner) -> Value:
"""Get the value of the property.
Expand All @@ -993,7 +997,7 @@
:raises ReadOnlyPropertyError: if the property cannot be set.
"""
if self.fset is None:
raise ReadOnlyPropertyError(f"Property {self.name} of {obj} has no setter.")

Check warning on line 1000 in src/labthings_fastapi/properties.py

View workflow job for this annotation

GitHub Actions / coverage

1000 line is not covered with tests
if FEATURE_FLAGS.validate_properties_on_set:
property_info = self.descriptor_info(obj)
value = property_info.validate(value)
Expand Down Expand Up @@ -1392,7 +1396,7 @@

:raises NotImplementedError: this method should be implemented in subclasses.
"""
raise NotImplementedError("This method should be implemented in subclasses.")

Check warning on line 1399 in src/labthings_fastapi/properties.py

View workflow job for this annotation

GitHub Actions / coverage

1399 line is not covered with tests

def descriptor_info(self, owner: Owner | None = None) -> SettingInfo[Owner, Value]:
r"""Return an object that allows access to this descriptor's metadata.
Expand Down
47 changes: 47 additions & 0 deletions tests/test_properties.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,25 @@ def __init__(self, **kwargs):
def undoc(self):
return self._undoc

@lt.property
def intprop_with_description(self) -> int:
"""An integer functional property with a description.

The description is the body of the docstring.
"""
return 42

floatprop_with_description: float = lt.property(default=0)
"""A float data property with a description.

The description is the body of the docstring.
"""

_float = 1.0

@lt.property
def floatprop(self) -> float:
"""A floating point property, defined with a function."""
return self._float

@floatprop.setter
Expand All @@ -52,6 +67,7 @@ def _set_floatprop(self, value: float):

@lt.action
def toggle_boolprop(self):
"""Toggle the boolean property."""
self.boolprop = not self.boolprop

@lt.action
Expand Down Expand Up @@ -82,6 +98,7 @@ def toggle_boolprop_from_thread(self):

@lt.property
def constrained_functional_int(self) -> int:
"""A functional property with constraints."""
return self._constrained_functional_int

@constrained_functional_int.setter
Expand All @@ -92,6 +109,7 @@ def _set_constrained_functional_int(self, value: int):

@lt.setting
def constrained_functional_str_setting(self) -> str:
"""A setting with constraints."""
return self._constrained_functional_str_setting

@constrained_functional_str_setting.setter
Expand Down Expand Up @@ -565,3 +583,32 @@ def test_propertyinfo():

assert "not a property" not in PropertyTestThing.properties
assert "not a property" not in thing.properties


@pytest.mark.parametrize(
("name", "title", "description"),
[
("boolprop", "A boolean property.", ...),
("undoc", "undoc", None),
(
"intprop_with_description",
"An integer functional property with a description.",
"The description is the body of the docstring.",
),
(
"floatprop_with_description",
"A float data property with a description.",
"The description is the body of the docstring.",
),
("floatprop", "A floating point property, defined with a function.", ...),
],
)
def test_title_and_description(name, title, description):
"""Check title and description propagate correctly."""
thing = create_thing_without_server(PropertyTestThing)
prop = thing.properties[name]
assert prop.title == title
if description is ...:
description = title
# If a description is present, ignore any trailing whitespace.
assert (prop.description.rstrip() if prop.description else None) == description
Loading