Skip to content

Commit e4afc1b

Browse files
Update docstrings for Thing Setting changes
1 parent 941b520 commit e4afc1b

File tree

3 files changed

+57
-31
lines changed

3 files changed

+57
-31
lines changed

src/labthings_fastapi/decorators/__init__.py

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -73,16 +73,17 @@ def thing_action(func: Optional[Callable] = None, **kwargs):
7373

7474

7575
def thing_property(func: Callable) -> ThingProperty:
76-
"""Mark a method of a Thing as a Property
76+
"""Mark a method of a Thing as a LabThings Property
77+
78+
This should be used as a decorator with a getter and a setter
79+
just like a standard python property decorator. If extra functionality
80+
is not required in the decorator, then using the ThingProperty class
81+
directly may allow for clearer code
7782
7883
As properties are accessed over the HTTP API they need to be JSON serialisable
7984
only return standard python types, or Pydantic BaseModels
8085
"""
81-
8286
# Replace the function with a `Descriptor` that's a `ThingProperty`
83-
84-
# TODO: try https://stackoverflow.com/questions/54413434/type-hinting-with-descriptors
85-
8687
return ThingProperty(
8788
return_type(func),
8889
readonly=True,
@@ -92,13 +93,18 @@ def thing_property(func: Callable) -> ThingProperty:
9293

9394

9495
def thing_setting(func: Callable) -> ThingSetting:
95-
"""Mark a method of a Thing as a Setting.
96-
97-
When creating a Setting you must always create a setter as it is used to load
98-
from disk.
96+
"""Mark a method of a Thing as a LabThings Setting.
9997
10098
A setting is a property that persists between runs.
10199
100+
This should be used as a decorator with a getter and a setter
101+
just like a standard python property decorator. If extra functionality
102+
is not required in the decorator, then using the ThingSetting class
103+
directly may allow for clearer code
104+
105+
When creating a Setting using this decorator you must always create a setter
106+
as it is used to load the value from disk.
107+
102108
As settings are accessed over the HTTP API and saved to disk they need to be
103109
JSON serialisable only return standard python types, or Pydantic BaseModels.
104110

src/labthings_fastapi/descriptors/property.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -227,23 +227,23 @@ class ThingSetting(ThingProperty):
227227
"""A setting can be accessed via the HTTP API and is persistent between sessions
228228
229229
A ThingSetting is a ThingProperty with extra functionality for triggering
230-
a Thing to save its settings, and for setting a property without emitting an event so
231-
that the setting can be set from disk before the server is fully started.
230+
a Thing to save its settings.
232231
233232
The setting otherwise acts just like a normal variable.
234233
"""
235234

236-
@property
237-
def persistent(self):
238-
return True
239-
240235
def __set__(self, obj, value):
241236
"""Set the property's value"""
242237
super().__set__(obj, value)
243238
obj.save_settings()
244239

245240
def set_without_emit(self, obj, value):
246-
"""Set the property's value, but do not emit. This is called during initial setup"""
241+
"""Set the property's value, but do not emit event to notify the server
242+
243+
This function is not expected to be used externally. It is called during
244+
initial setup so that the setting can be set from disk before the server
245+
is fully started.
246+
"""
247247
obj.__dict__[self.name] = value
248248
if self._setter:
249249
self._setter(obj, value)

src/labthings_fastapi/thing.py

Lines changed: 35 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,18 @@ async def __aexit__(self, exc_t, exc_v, exc_tb):
9494
def attach_to_server(
9595
self, server: ThingServer, path: str, setting_storage_path: str
9696
):
97-
"""Add HTTP handlers to an app for all Interaction Affordances"""
97+
"""Attatch this thing to the server.
98+
99+
Things need to be attached to a server before use to function correctly.
100+
101+
:param server: The server to attach this Thing to
102+
:param settings_storage_path: The path on disk to save the any Thing Settings
103+
to. This should be the path to a json file. If it does not exist it will be
104+
created.
105+
106+
Wc3 Web Of Things explanation:
107+
This will add HTTP handlers to an app for all Interaction Affordances
108+
"""
98109
self.path = path
99110
self.action_manager: ActionManager = server.action_manager
100111
self.load_settings(setting_storage_path)
@@ -122,28 +133,37 @@ def thing_description(request: Request) -> ThingDescription:
122133
async def websocket(ws: WebSocket):
123134
await websocket_endpoint(self, ws)
124135

125-
_settings: Optional[list[str]] = None
136+
# A private variable to hold the list of settings so it doesn't need to be
137+
# iterated through each time it is read
138+
_settings_store: Optional[dict[str, ThingSetting]] = None
126139

127140
@property
128-
def settings(self):
129-
if self._settings is not None:
130-
return self._settings
141+
def _settings(self) -> Optional[dict[str, ThingSetting]]:
142+
"""A private property that returns a dict of all settings for this Thing
143+
144+
Each dict key is the name of the setting, the corresponding value is the
145+
ThingSetting class (a descriptor). This can be used to directly get the
146+
descriptor so that the value can be set without emitting signals, such
147+
as on startup.
148+
"""
149+
if self._settings_store is not None:
150+
return self._settings_store
131151

132-
self._settings = {}
152+
self._settings_store = {}
133153
for name, attr in class_attributes(self):
134154
if isinstance(attr, ThingSetting):
135-
self._settings[name] = attr
136-
return self._settings
155+
self._settings_store[name] = attr
156+
return self._settings_store
137157

138158
_setting_storage_path: Optional[str] = None
139159

140160
@property
141161
def setting_storage_path(self) -> Optional[str]:
142-
"""The storage path for settings. This is set at runtime."""
162+
"""The storage path for settings. This is set as the Thing is added to a server"""
143163
return self._setting_storage_path
144164

145165
def load_settings(self, setting_storage_path):
146-
"""Load settings from json. Called when connecting to the server."""
166+
"""Load settings from json. This is run when the Thing is added to a server"""
147167
# Ensure that the settings path isn't set during loading or saving will be triggered
148168
self._setting_storage_path = None
149169
thing_name = type(self).__name__
@@ -152,8 +172,8 @@ def load_settings(self, setting_storage_path):
152172
with open(setting_storage_path, "r", encoding="utf-8") as file_obj:
153173
setting_dict = json.load(file_obj)
154174
for key, value in setting_dict.items():
155-
if key in self.settings:
156-
self.settings[key].set_without_emit(self, value)
175+
if key in self._settings:
176+
self._settings[key].set_without_emit(self, value)
157177
else:
158178
_LOGGER.warning(
159179
"Cannot set %s from persistent storage as %s has no matching setting.",
@@ -165,10 +185,10 @@ def load_settings(self, setting_storage_path):
165185
self._setting_storage_path = setting_storage_path
166186

167187
def save_settings(self):
168-
"""Save settings to JSON. This is called when a setting is updated with a setter"""
169-
if self.settings is not None:
188+
"""Save settings to JSON. This is called whenever a setting is updated"""
189+
if self._settings is not None:
170190
setting_dict = {}
171-
for name in self.settings.keys():
191+
for name in self._settings.keys():
172192
value = getattr(self, name)
173193
if isinstance(value, BaseModel):
174194
value = value.model_dump()

0 commit comments

Comments
 (0)