Skip to content

Commit dc169d7

Browse files
Have settings dictionary generated on the fly for saving by a Thing method
1 parent 6c5aa3f commit dc169d7

File tree

5 files changed

+52
-22
lines changed

5 files changed

+52
-22
lines changed

src/labthings_fastapi/decorators/__init__.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
from ..descriptors import (
3838
ActionDescriptor,
3939
PropertyDescriptor,
40+
SettingDescriptor,
4041
EndpointDescriptor,
4142
HTTPMethod,
4243
)
@@ -91,25 +92,24 @@ def __get__(self, obj, objtype=None):
9192
getter=func,
9293
)
9394

94-
def persistent_thing_property(func: Callable) -> PropertyDescriptor:
95-
"""Mark a method of a Thing as a Property
95+
def thing_setting(func: Callable) -> SettingDescriptor:
96+
"""Mark a method of a Thing as a Setting.
9697
97-
We replace the function with a `Descriptor` that's a
98-
subclass of `PropertyDescriptor`
98+
A setting is a property that persists between runs
9999
100-
TODO: try https://stackoverflow.com/questions/54413434/type-hinting-with-descriptors
100+
We replace the function with a `Descriptor` that's a
101+
subclass of `SettingDescriptor`
101102
"""
102103

103-
class PropertyDescriptorSubclass(PropertyDescriptor):
104+
class SettingDescriptorSubclass(SettingDescriptor):
104105
def __get__(self, obj, objtype=None):
105106
return super().__get__(obj, objtype)
106107

107-
return PropertyDescriptorSubclass(
108+
return SettingDescriptorSubclass(
108109
return_type(func),
109110
readonly=True,
110111
observable=False,
111112
getter=func,
112-
persistent=True,
113113
)
114114

115115

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from .action import ActionDescriptor as ActionDescriptor
22
from .property import PropertyDescriptor as PropertyDescriptor
3+
from .property import SettingDescriptor as SettingDescriptor
34
from .endpoint import EndpointDescriptor as EndpointDescriptor
45
from .endpoint import HTTPMethod as HTTPMethod

src/labthings_fastapi/descriptors/property.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,6 @@ def __init__(
3939
title: Optional[str] = None,
4040
getter: Optional[Callable] = None,
4141
setter: Optional[Callable] = None,
42-
persistent: bool = False,
4342
):
4443
if getter and initial_value is not None:
4544
raise ValueError("getter and an initial value are mutually exclusive.")
@@ -54,7 +53,6 @@ def __init__(
5453
# The lines below allow _getter and _setter to be specified by subclasses
5554
self._setter = setter or getattr(self, "_setter", None)
5655
self._getter = getter or getattr(self, "_getter", None)
57-
self.persistent = persistent
5856
self.save_location = None
5957
# Try to generate a DataSchema, so that we can raise an error that's easy to
6058
# link to the offending PropertyDescriptor
@@ -110,8 +108,6 @@ def __set__(self, obj, value):
110108
obj.__dict__[self.name] = value
111109
if self._setter:
112110
self._setter(obj, value)
113-
if self.persistent:
114-
logging.warning(f'This should save {self.name} to {self.save_location}')
115111
self.emit_changed_event(obj, value)
116112

117113
def _observers_set(self, obj):
@@ -227,3 +223,15 @@ def setter(self, func: Callable) -> Self:
227223
self._setter = func
228224
self.readonly = False
229225
return self
226+
227+
228+
class SettingDescriptor(PropertyDescriptor):
229+
230+
@property
231+
def persistent(self):
232+
return True
233+
234+
def __set__(self, obj, value):
235+
"""Set the property's value"""
236+
super().__set__(obj, value)
237+
obj.save_settings()

src/labthings_fastapi/server/__init__.py

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818
from ..thing_description.model import ThingDescription
1919
from ..dependencies.thing_server import _thing_servers
2020
from ..outputs.blob import BlobDataManager
21-
from ..utilities import class_attributes
2221

2322

2423
class ThingServer:
@@ -86,11 +85,7 @@ def add_thing(self, thing: Thing, path: str):
8685
thing._labthings_thing_settings = ThingSettings(
8786
os.path.join(settings_folder, "settings.json")
8887
)
89-
for name, attribute in class_attributes(thing):
90-
if hasattr(attribute, "property_affordance"):
91-
if attribute.persistent:
92-
attribute.save_location = os.path.join(settings_folder, "settings.json")
93-
thing.attach_to_server(self, path)
88+
thing.attach_to_server(self, path, os.path.join(settings_folder, "settings.json"))
9489

9590
@asynccontextmanager
9691
async def lifespan(self, app: FastAPI):

src/labthings_fastapi/thing.py

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,15 @@
1010
from __future__ import annotations
1111
from typing import TYPE_CHECKING, Optional
1212
from collections.abc import Mapping
13+
import logging
1314
from fastapi.encoders import jsonable_encoder
1415
from fastapi import Request
1516
from anyio.abc import ObjectSendStream
1617
from anyio.from_thread import BlockingPortal
1718
from anyio.to_thread import run_sync
19+
20+
from pydantic import BaseModel
21+
1822
from .descriptors import PropertyDescriptor, ActionDescriptor
1923
from .thing_description.model import ThingDescription, NoSecurityScheme
2024
from .utilities import class_attributes
@@ -35,9 +39,6 @@ class Thing:
3539
a particular function - it will correspond to a path on the server, and a Thing
3640
Description document.
3741
38-
Your Thing should be a subclass of Thing - note that we don't define `__init__` so
39-
you are free to initialise as you like and don't need to call `super().__init__`.
40-
4142
## Subclassing Notes
4243
4344
* `__init__`: You should accept any arguments you need to configure the Thing
@@ -85,10 +86,11 @@ async def __aexit__(self, exc_t, exc_v, exc_tb):
8586
if hasattr(self, "__exit__"):
8687
return await run_sync(self.__exit__, exc_t, exc_v, exc_tb)
8788

88-
def attach_to_server(self, server: ThingServer, path: str):
89+
def attach_to_server(self, server: ThingServer, path: str, setting_storage_path: str):
8990
"""Add HTTP handlers to an app for all Interaction Affordances"""
9091
self.path = path
9192
self.action_manager: ActionManager = server.action_manager
93+
self._setting_storage_path = setting_storage_path
9294

9395
for _name, item in class_attributes(self):
9496
try:
@@ -113,6 +115,30 @@ def thing_description(request: Request) -> ThingDescription:
113115
async def websocket(ws: WebSocket):
114116
await websocket_endpoint(self, ws)
115117

118+
119+
_settings: Optional[list[str]] = None
120+
121+
@property
122+
def settings(self):
123+
if self._settings is None:
124+
self._settings = []
125+
for name, attribute in class_attributes(self):
126+
if hasattr(attribute, "property_affordance") and hasattr(attribute, "persistent"):
127+
self._settings.append(name)
128+
return self._settings
129+
130+
_setting_storage_path: Optional[str] = None
131+
132+
@property
133+
def setting_storage_path(self) -> ThingSettings:
134+
"""The storage path for settings. This is set at runtime."""
135+
return self._setting_storage_path
136+
137+
def save_settings(self):
138+
if self.settings is not None:
139+
setting_dict = {name: self.__dict__[name] for name in self.settings}
140+
logging.warning(f'This should save {setting_dict} to {self._setting_storage_path}')
141+
116142
_labthings_thing_settings: Optional[ThingSettings] = None
117143

118144
@property

0 commit comments

Comments
 (0)