Skip to content

Commit 919db28

Browse files
Add clarification that settings must be explicitly set to save
Add unit test, that is clear that docstrings should be updated if the behaviour changes
1 parent 06eaf4a commit 919db28

File tree

3 files changed

+53
-2
lines changed

3 files changed

+53
-2
lines changed

src/labthings_fastapi/decorators/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,12 @@ def thing_setting(func: Callable) -> ThingSetting:
111111
If the type is a pydantic BaseModel, then the setter must also be able to accept
112112
the dictionary representation of this BaseModel as this is what will be used to
113113
set the Setting when loading from disk on starting the server.
114+
115+
Note: If a setting updated, rather than explicitly set this will not trigger saving.
116+
For example: if a Thing has a setting called `dictsetting` holding the dictionary
117+
`{"a": 1, "b": 2}` then `self.dictsetting = {"a": 2, "b": 2}` would trigger saving
118+
but `self.dictsetting[a] = 2` would not, as the setter for `dictsetting` is never
119+
called.
114120
"""
115121
# Replace the function with a `Descriptor` that's a `ThingSetting`
116122
return ThingSetting(

src/labthings_fastapi/descriptors/property.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -224,7 +224,7 @@ def getter(self, func: Callable) -> Self:
224224
def setter(self, func: Callable) -> Self:
225225
"""Decorator to set the property's value
226226
227-
ThingPropertys are variabes - so they will return the value they hold
227+
ThingPropertys are variables - so they will return the value they hold
228228
when they are accessed. However, they can run code when they are set: this
229229
decorator sets a function as that code.
230230
"""
@@ -239,6 +239,12 @@ class ThingSetting(ThingProperty):
239239
A ThingSetting is a ThingProperty with extra functionality for triggering
240240
a Thing to save its settings.
241241
242+
Note: If a setting updated, rather than explicitly set this will not trigger saving.
243+
For example: if a Thing has a setting called `dictsetting` holding the dictionary
244+
`{"a": 1, "b": 2}` then `self.dictsetting = {"a": 2, "b": 2}` would trigger saving
245+
but `self.dictsetting[a] = 2` would not, as the setter for `dictsetting` is never
246+
called.
247+
242248
The setting otherwise acts just like a normal variable.
243249
"""
244250

tests/test_settings.py

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@
1616
class TestThing(Thing):
1717
boolsetting = ThingSetting(bool, False, description="A boolean setting")
1818
stringsetting = ThingSetting(str, "foo", description="A string setting")
19+
dictsetting = ThingSetting(
20+
dict, {"a": 1, "b": 2}, description="A dictionary setting"
21+
)
1922

2023
_float = 1.0
2124

@@ -42,15 +45,20 @@ def _get_setting_file(server, thingpath):
4245
return os.path.normpath(path)
4346

4447

45-
def _settings_dict(boolsetting=False, floatsetting=1.0, stringsetting="foo"):
48+
def _settings_dict(
49+
boolsetting=False, floatsetting=1.0, stringsetting="foo", dictsetting=None
50+
):
4651
"""Return the expected settings dictionary
4752
4853
Parameters can be updated from default values
4954
"""
55+
if dictsetting is None:
56+
dictsetting = {"a": 1, "b": 2}
5057
return {
5158
"boolsetting": boolsetting,
5259
"floatsetting": floatsetting,
5360
"stringsetting": stringsetting,
61+
"dictsetting": dictsetting,
5462
}
5563

5664

@@ -91,6 +99,37 @@ def test_settings_save(thing, server):
9199
assert json.load(file_obj) == _settings_dict(floatsetting=2.0)
92100

93101

102+
def test_settings_dict_save(thing, server):
103+
"""Check settings are saved if the dict is updated in full"""
104+
setting_file = _get_setting_file(server, "/thing")
105+
server.add_thing(thing, "/thing")
106+
# No setting file created when first added
107+
assert not os.path.isfile(setting_file)
108+
with TestClient(server.app):
109+
thing.dictsetting = {"c": 3}
110+
assert os.path.isfile(setting_file)
111+
with open(setting_file, "r", encoding="utf-8") as file_obj:
112+
# Check settings on file match expected dictionary
113+
assert json.load(file_obj) == _settings_dict(dictsetting={"c": 3})
114+
115+
116+
def test_settings_dict_internal_update(thing, server):
117+
"""Confirm settings are not saved if the internal value of a dictionary is updated
118+
119+
This behaviour is not ideal, but it is documented. If the behaviour is updated
120+
then the documentation should be updated and this test removed
121+
"""
122+
setting_file = _get_setting_file(server, "/thing")
123+
server.add_thing(thing, "/thing")
124+
# No setting file created when first added
125+
assert not os.path.isfile(setting_file)
126+
with TestClient(server.app):
127+
thing.dictsetting["a"] = 4
128+
# As only an internal member of the dictornary was set, the saving was not
129+
# triggered.
130+
assert not os.path.isfile(setting_file)
131+
132+
94133
def test_settings_load(thing, server):
95134
"""Check settings can be loaded from disk when added to server"""
96135
setting_file = _get_setting_file(server, "/thing")

0 commit comments

Comments
 (0)