Skip to content

Commit dd8158b

Browse files
Change the names for setting/property descriptors, add some docstrings
1 parent 76684b9 commit dd8158b

File tree

6 files changed

+49
-32
lines changed

6 files changed

+49
-32
lines changed

src/labthings_fastapi/client/in_server.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
from pydantic import BaseModel
1717
from labthings_fastapi.descriptors.action import ActionDescriptor
1818

19-
from labthings_fastapi.descriptors.property import PropertyDescriptor
19+
from labthings_fastapi.descriptors.property import ThingProperty
2020
from labthings_fastapi.utilities import attributes
2121
from . import PropertyClientDescriptor
2222
from ..thing import Thing
@@ -123,15 +123,15 @@ def action_method(self, **kwargs):
123123

124124

125125
def add_property(
126-
attrs: dict[str, Any], property_name: str, property: PropertyDescriptor
126+
attrs: dict[str, Any], property_name: str, property: ThingProperty
127127
) -> None:
128128
"""Add a property to a DirectThingClient subclass"""
129129
attrs[property_name] = property_descriptor(
130130
property_name,
131131
property.model,
132132
description=property.description,
133133
writeable=not property.readonly,
134-
readable=True, # TODO: make this configurable in PropertyDescriptor
134+
readable=True, # TODO: make this configurable in ThingProperty
135135
)
136136

137137

@@ -163,7 +163,7 @@ def init_proxy(self, request: Request, **dependencies: Mapping[str, Any]):
163163
}
164164
dependencies: list[inspect.Parameter] = []
165165
for name, item in attributes(thing_class):
166-
if isinstance(item, PropertyDescriptor):
166+
if isinstance(item, ThingProperty):
167167
# TODO: What about properties that don't use descriptors? Fall back to http?
168168
add_property(client_attrs, name, item)
169169
elif isinstance(item, ActionDescriptor):

src/labthings_fastapi/decorators/__init__.py

Lines changed: 23 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,8 @@
3636
from typing import Optional, Callable
3737
from ..descriptors import (
3838
ActionDescriptor,
39-
PropertyDescriptor,
40-
SettingDescriptor,
39+
ThingProperty,
40+
ThingSetting,
4141
EndpointDescriptor,
4242
HTTPMethod,
4343
)
@@ -72,33 +72,42 @@ def thing_action(func: Optional[Callable] = None, **kwargs):
7272
return partial(mark_thing_action, **kwargs)
7373

7474

75-
def thing_property(func: Callable) -> PropertyDescriptor:
75+
def thing_property(func: Callable) -> ThingProperty:
7676
"""Mark a method of a Thing as a Property
7777
78-
Replace the function with a `Descriptor` that's a
79-
`PropertyDescriptor`
80-
81-
TODO: try https://stackoverflow.com/questions/54413434/type-hinting-with-descriptors
78+
As properties are accessed over the HTTP API they need to be JSON serialisable
79+
only return standard python types, or Pydantic BaseModels
8280
"""
8381

84-
return PropertyDescriptor(
82+
# Replace the function with a `Descriptor` that's a `ThingProperty`
83+
84+
# TODO: try https://stackoverflow.com/questions/54413434/type-hinting-with-descriptors
85+
86+
return ThingProperty(
8587
return_type(func),
8688
readonly=True,
8789
observable=False,
8890
getter=func,
8991
)
9092

9193

92-
def thing_setting(func: Callable) -> SettingDescriptor:
94+
def thing_setting(func: Callable) -> ThingSetting:
9395
"""Mark a method of a Thing as a Setting.
9496
95-
A setting is a property that persists between runs
97+
When creating a Setting you must always create a setter as it is used to load
98+
from disk.
9699
97-
Replace the function with a `Descriptor` that's a
98-
`SettingDescriptor`
99-
"""
100+
A setting is a property that persists between runs.
101+
102+
As settings are accessed over the HTTP API and saved to disk they need to be
103+
JSON serialisable only return standard python types, or Pydantic BaseModels.
100104
101-
return SettingDescriptor(
105+
If the type is a pydantic BaseModel, then the setter must also be able to accept
106+
the dictionary representation of this BaseModel as this is what will be used to
107+
set the Setting when loading from disk on starting the server.
108+
"""
109+
# Replace the function with a `Descriptor` that's a `ThingSetting`
110+
return ThingSetting(
102111
return_type(func),
103112
readonly=True,
104113
observable=False,
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from .action import ActionDescriptor as ActionDescriptor
2-
from .property import PropertyDescriptor as PropertyDescriptor
3-
from .property import SettingDescriptor as SettingDescriptor
2+
from .property import ThingProperty as ThingProperty
3+
from .property import ThingSetting as ThingSetting
44
from .endpoint import EndpointDescriptor as EndpointDescriptor
55
from .endpoint import HTTPMethod as HTTPMethod

src/labthings_fastapi/descriptors/property.py

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,10 @@
1818
from ..thing import Thing
1919

2020

21-
class PropertyDescriptor:
21+
class ThingProperty:
2222
"""A property that can be accessed via the HTTP API
2323
24-
By default, a PropertyDescriptor is "dumb", i.e. it acts just like
24+
By default, a ThingProperty is "dumb", i.e. it acts just like
2525
a normal variable.
2626
"""
2727

@@ -53,7 +53,7 @@ def __init__(
5353
self._setter = setter or getattr(self, "_setter", None)
5454
self._getter = getter or getattr(self, "_getter", None)
5555
# Try to generate a DataSchema, so that we can raise an error that's easy to
56-
# link to the offending PropertyDescriptor
56+
# link to the offending ThingProperty
5757
type_to_dataschema(self.model)
5858

5959
def __set_name__(self, owner, name: str):
@@ -214,7 +214,7 @@ def getter(self, func: Callable) -> Self:
214214
def setter(self, func: Callable) -> Self:
215215
"""Decorator to set the property's value
216216
217-
PropertyDescriptors are variabes - so they will return the value they hold
217+
ThingPropertys are variabes - so they will return the value they hold
218218
when they are accessed. However, they can run code when they are set: this
219219
decorator sets a function as that code.
220220
"""
@@ -223,7 +223,15 @@ def setter(self, func: Callable) -> Self:
223223
return self
224224

225225

226-
class SettingDescriptor(PropertyDescriptor):
226+
class ThingSetting(ThingProperty):
227+
"""A setting can be accessed via the HTTP API and is persistent between sessions
228+
229+
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.
232+
233+
The setting otherwise acts just like a normal variable.
234+
"""
227235
@property
228236
def persistent(self):
229237
return True

src/labthings_fastapi/example_things/__init__.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from typing import Optional, Annotated
77
from labthings_fastapi.thing import Thing
88
from labthings_fastapi.decorators import thing_action, thing_property
9-
from labthings_fastapi.descriptors import PropertyDescriptor
9+
from labthings_fastapi.descriptors import ThingProperty
1010
from pydantic import Field
1111

1212

@@ -73,11 +73,11 @@ def slowly_increase_counter(self, increments: int = 60, delay: float = 1):
7373
time.sleep(delay)
7474
self.increment_counter()
7575

76-
counter = PropertyDescriptor(
76+
counter = ThingProperty(
7777
model=int, initial_value=0, readonly=True, description="A pointless counter"
7878
)
7979

80-
foo = PropertyDescriptor(
80+
foo = ThingProperty(
8181
model=str,
8282
initial_value="Example",
8383
description="A pointless string for demo purposes.",

src/labthings_fastapi/thing.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222

2323
from pydantic import BaseModel
2424

25-
from .descriptors import PropertyDescriptor, ActionDescriptor
25+
from .descriptors import ThingProperty, ThingSetting, ActionDescriptor
2626
from .thing_description.model import ThingDescription, NoSecurityScheme
2727
from .utilities import class_attributes
2828
from .thing_description import validation
@@ -56,7 +56,7 @@ class Thing:
5656
code that will close down your hardware. It's equivalent to a `finally:` block.
5757
* Properties and Actions are defined using decorators: the `@thing_action` decorator
5858
declares a method to be an action, which will run when it's triggered, and the
59-
`@thing_property` decorator (or `PropertyDescriptor` descriptor) does the same for
59+
`@thing_property` decorator (or `ThingProperty` descriptor) does the same for
6060
a property. See the documentation on those functions for more detail.
6161
* `title` will be used in various places as the human-readable name of your Thing,
6262
so it makes sense to set this in a subclass.
@@ -131,7 +131,7 @@ def settings(self):
131131

132132
self._settings = {}
133133
for name, attr in class_attributes(self):
134-
if hasattr(attr, "property_affordance") and hasattr(attr, "persistent"):
134+
if isinstance(attr, ThingSetting):
135135
self._settings[name] = attr
136136
return self._settings
137137

@@ -264,7 +264,7 @@ def thing_description_dict(
264264
def observe_property(self, property_name: str, stream: ObjectSendStream):
265265
"""Register a stream to receive property change notifications"""
266266
prop = getattr(self.__class__, property_name)
267-
if not isinstance(prop, PropertyDescriptor):
267+
if not isinstance(prop, ThingProperty):
268268
raise KeyError(f"{property_name} is not a LabThings Property")
269269
prop._observers_set(self).add(stream)
270270

0 commit comments

Comments
 (0)