From bfa313ea9338a4ea56b689323c07411221392de6 Mon Sep 17 00:00:00 2001 From: Richard Bowman Date: Mon, 12 Jan 2026 14:45:48 +0000 Subject: [PATCH 1/2] User a `_set_` prefix on property setters. This updates test and example code to use a `_set_` prefix on setters as standard. --- docs/source/properties.rst | 12 ++++++++---- src/labthings_fastapi/base_descriptor.py | 2 +- src/labthings_fastapi/properties.py | 6 +++--- .../old_dependency_tests/test_dependency_metadata.py | 2 +- tests/test_base_descriptor.py | 4 ++-- tests/test_properties.py | 6 +++--- tests/test_property.py | 6 +++--- tests/test_settings.py | 4 ++-- tests/test_websocket.py | 2 +- typing_tests/thing_properties.py | 8 ++++---- 10 files changed, 28 insertions(+), 24 deletions(-) diff --git a/docs/source/properties.rst b/docs/source/properties.rst index 23299af2..cbd3a20c 100644 --- a/docs/source/properties.rst +++ b/docs/source/properties.rst @@ -83,11 +83,15 @@ Functional properties may also have a "setter" method, which is called when the return self.my_property * 2 @twice_my_property.setter - def twice_my_property(self, value: int): + def _set_twice_my_property(self, value: int): """Set the value of twice_my_property.""" self.my_property = value // 2 -Adding a setter makes the property read-write (if only a getter is present, it must be read-only). +Adding a setter makes the property read-write (if only a getter is present, it must be read-only). + +.. note:: + + The setter method for regular Python properties is usually named the same as the property itself (e.g. ``def twice_my_property(self, value: int)``). Unfortunately, doing this with LabThings properties causes problems for static type checkers such as `mypy`\ . We therefore recommend you prefix setters with ``_set_`` (e.g. ``def _set_twice_my_property(self, value: int)``). This is optional, and doesn't change the way the property works - but it is useful if you need `mypy` to work on your code, and don't want to ignore every property setter. It is possible to make a property read-only for clients by setting its ``readonly`` attribute: this has the same behaviour as for data properties. @@ -105,7 +109,7 @@ It is possible to make a property read-only for clients by setting its ``readonl return self.my_property * 2 @twice_my_property.setter - def twice_my_property(self, value: int): + def _set_twice_my_property(self, value: int): """Set the value of twice_my_property.""" self.my_property = value // 2 @@ -143,7 +147,7 @@ We can modify the previous example to show how to add constraints to both data a return self._humidity @humidity.setter - def humidity(self, value: float): + def _set_humidity(self, value: float): """Set the current humidity percentage.""" self._humidity = value diff --git a/src/labthings_fastapi/base_descriptor.py b/src/labthings_fastapi/base_descriptor.py index 3b9c9a2c..35b0436f 100644 --- a/src/labthings_fastapi/base_descriptor.py +++ b/src/labthings_fastapi/base_descriptor.py @@ -124,7 +124,7 @@ def prop4(self): return True @prop4.setter - def set_prop4(self, val): + def _set_prop4(self, val): "A setter for prop4 that is not named prop4." pass diff --git a/src/labthings_fastapi/properties.py b/src/labthings_fastapi/properties.py index be1cc014..5be2998e 100644 --- a/src/labthings_fastapi/properties.py +++ b/src/labthings_fastapi/properties.py @@ -27,7 +27,7 @@ def remaining(self) -> int: return self.target - self.count @remaining.setter - def remaining(self, value: int) -> None: + def _set_remaining(self, value: int) -> None: self.target = self.count + value The first two properties are simple variables: they may be read and assigned @@ -740,7 +740,7 @@ def myprop(self) -> int: return self._myprop @myprop.setter - def set_myprop(self, val: int) -> None: + def _set_myprop(self, val: int) -> None: self._myprop = val myprop.readonly = True # Prevent client code from setting it @@ -753,7 +753,7 @@ def set_myprop(self, val: int) -> None: ``mypy`` raising an error that the getter has been redefined with a different type. The behaviour is identical whether the setter and getter have the same name or not. The only difference is that the `.Thing` - will have an additional method called ``set_myprop`` in the example + will have an additional method called ``_set_myprop`` in the example above. :param fset: The new setter function. diff --git a/tests/old_dependency_tests/test_dependency_metadata.py b/tests/old_dependency_tests/test_dependency_metadata.py index a563cc3a..677b0ad3 100644 --- a/tests/old_dependency_tests/test_dependency_metadata.py +++ b/tests/old_dependency_tests/test_dependency_metadata.py @@ -25,7 +25,7 @@ def a(self): return self._a @a.setter - def a(self, value): + def _set_a(self, value): self._a = value @property diff --git a/tests/test_base_descriptor.py b/tests/test_base_descriptor.py index 91b409f1..4a11b6ed 100644 --- a/tests/test_base_descriptor.py +++ b/tests/test_base_descriptor.py @@ -266,13 +266,13 @@ def prop1(self): return False @prop1.setter - def set_prop1(self, val): + def _set_prop1(self, val): pass # The exception occurs at the end of the class definition, so check we include # the property names. assert "prop1" in str(excinfo.value) - assert "set_prop1" in str(excinfo.value) + assert "_set_prop1" in str(excinfo.value) # For good measure, check reuse across classes is also prevented. class FirstExampleClass: diff --git a/tests/test_properties.py b/tests/test_properties.py index 2d9a9f15..4bf27bfd 100644 --- a/tests/test_properties.py +++ b/tests/test_properties.py @@ -44,7 +44,7 @@ def floatprop(self) -> float: return self._float @floatprop.setter - def floatprop(self, value: float): + def _set_floatprop(self, value: float): self._float = value @lt.action @@ -82,7 +82,7 @@ def constrained_functional_int(self) -> int: return self._constrained_functional_int @constrained_functional_int.setter - def set_constrained_functional_int(self, value: int): + def _set_constrained_functional_int(self, value: int): self._constrained_functional_int = value constrained_functional_int.constraints = {"ge": 0, "le": 10} @@ -92,7 +92,7 @@ def constrained_functional_str_setting(self) -> str: return self._constrained_functional_str_setting @constrained_functional_str_setting.setter - def set_constrained_functional_str_setting(self, value: str): + def _set_constrained_functional_str_setting(self, value: str): self._constrained_functional_str_setting = value constrained_functional_str_setting.constraints = { diff --git a/tests/test_property.py b/tests/test_property.py index 3afe9b12..2ba34417 100644 --- a/tests/test_property.py +++ b/tests/test_property.py @@ -271,14 +271,14 @@ def prop(self) -> bool: """An example getter.""" @prop.setter - def set_prop(self, val: bool) -> None: + def _set_prop(self, val: bool) -> None: """A setter named differently.""" pass assert isinstance(Example.prop, FunctionalProperty) assert Example.prop.name == "prop" - assert not isinstance(Example.set_prop, FunctionalProperty) - assert callable(Example.set_prop) + assert not isinstance(Example._set_prop, FunctionalProperty) + assert callable(Example._set_prop) def test_premature_api_and_affordance(mocker): diff --git a/tests/test_settings.py b/tests/test_settings.py index 8ee13556..19357748 100644 --- a/tests/test_settings.py +++ b/tests/test_settings.py @@ -36,7 +36,7 @@ def floatsetting(self) -> float: return self._floatsetting @floatsetting.setter - def floatsetting(self, value: float): + def _set_floatsetting(self, value: float): self._floatsetting = value @lt.setting @@ -50,7 +50,7 @@ def localonlysetting(self) -> str: return self._localonlysetting @localonlysetting.setter - def localonlysetting(self, value: str): + def _set_localonlysetting(self, value: str): self._localonlysetting = value localonlysetting.readonly = True diff --git a/tests/test_websocket.py b/tests/test_websocket.py index ecf00282..2781e04f 100644 --- a/tests/test_websocket.py +++ b/tests/test_websocket.py @@ -31,7 +31,7 @@ def funcprop(self) -> int: return 0 @funcprop.setter - def set_funcprop(self, val: int) -> None: + def _set_funcprop(self, val: int) -> None: pass @lt.action diff --git a/typing_tests/thing_properties.py b/typing_tests/thing_properties.py index fba5c394..591c7be3 100644 --- a/typing_tests/thing_properties.py +++ b/typing_tests/thing_properties.py @@ -217,7 +217,7 @@ def intprop2(self) -> int: return 0 @intprop2.setter - def set_intprop2(self, value: int) -> None: + def _set_intprop2(self, value: int) -> None: """Setter for intprop2.""" pass @@ -232,7 +232,7 @@ def intprop3(self) -> int: return 0 @intprop3.setter - def set_intprop3(self, value: str) -> None: + def _set_intprop3(self, value: str) -> None: """Setter for intprop3. It's got the wrong type so should fail.""" pass @@ -246,7 +246,7 @@ def fprop(self) -> int: return 0 @fprop.setter - def set_fprop(self, value: int) -> None: + def _set_fprop(self, value: int) -> None: """Setter for fprop. Type checking should pass.""" pass @@ -256,7 +256,7 @@ def strprop(self) -> str: return "Hello world!" @strprop.setter # type: ignore[no-redef] - def strprop(self, val: str) -> None: + def _set_strprop(self, val: str) -> None: """A setter with the same name as the getter. This is the convention for `builtins.property` but `mypy` does not From 3cd035c856040ffc28ea5d16306c30bb5a73c0a1 Mon Sep 17 00:00:00 2001 From: Richard Bowman Date: Mon, 12 Jan 2026 16:26:44 +0000 Subject: [PATCH 2/2] Revert an overzealous change I changed the name of a property setter that was deliberately the same as the getter. This reverts that change so mypy passes again. --- typing_tests/thing_properties.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/typing_tests/thing_properties.py b/typing_tests/thing_properties.py index 591c7be3..ee9a421a 100644 --- a/typing_tests/thing_properties.py +++ b/typing_tests/thing_properties.py @@ -256,7 +256,7 @@ def strprop(self) -> str: return "Hello world!" @strprop.setter # type: ignore[no-redef] - def _set_strprop(self, val: str) -> None: + def strprop(self, val: str) -> None: """A setter with the same name as the getter. This is the convention for `builtins.property` but `mypy` does not