Skip to content

Commit 54b19f0

Browse files
committed
Update examples and test suite
Tests now all collect, though several are still failing and the websocket test is hanging. The DocstringToMessage mixin was not working, this is now fixed. I now use ... as a default value for `default` in properties/settings, because None is a legitimate value.
1 parent da345fb commit 54b19f0

File tree

10 files changed

+64
-61
lines changed

10 files changed

+64
-61
lines changed

docs/source/quickstart/counter.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,8 @@ def slowly_increase_counter(self) -> None:
2222
time.sleep(1)
2323
self.increment_counter()
2424

25-
counter = lt.ThingProperty(
26-
model=int, initial_value=0, readonly=True, description="A pointless counter"
27-
)
25+
counter: int = lt.property(0, readonly=True)
26+
"A pointless counter"
2827

2928

3029
if __name__ == "__main__":

src/labthings_fastapi/example_things/__init__.py

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@
77
import time
88
from typing import Any, Optional, Annotated
99
from labthings_fastapi.thing import Thing
10-
from labthings_fastapi.decorators import thing_action, thing_property
11-
from labthings_fastapi.descriptors import ThingProperty
10+
from labthings_fastapi.decorators import thing_action
11+
from labthings_fastapi.thing_property import property as lt_property
1212
from pydantic import Field
1313

1414

@@ -94,15 +94,11 @@ def slowly_increase_counter(self, increments: int = 60, delay: float = 1):
9494
time.sleep(delay)
9595
self.increment_counter()
9696

97-
counter = ThingProperty(
98-
model=int, initial_value=0, readonly=True, description="A pointless counter"
99-
)
97+
counter: int = lt_property(0, readonly=True)
98+
"A pointless counter"
10099

101-
foo = ThingProperty(
102-
model=str,
103-
initial_value="Example",
104-
description="A pointless string for demo purposes.",
105-
)
100+
foo: str = lt_property("Example")
101+
"A pointless string for demo purposes."
106102

107103
@thing_action
108104
def action_without_arguments(self) -> None:
@@ -129,7 +125,7 @@ def broken_action(self):
129125
"""
130126
raise RuntimeError("This is a broken action")
131127

132-
@thing_property
128+
@lt_property
133129
def broken_property(self):
134130
"""Raise an exception when the property is accessed.
135131

src/labthings_fastapi/exceptions.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ class DocstringToMessage:
1616

1717
append_to_message: bool = True
1818

19-
def __init__(self, message: str | None):
19+
def __init__(self, message: str | None = None):
2020
"""Initialise an error with a message or its docstring.
2121
2222
:param message: the optional message.

src/labthings_fastapi/thing_property.py

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ class attribute. Documentation is in strings immediately following the
4646

4747
from __future__ import annotations
4848
import builtins
49+
from types import EllipsisType
4950
from typing import (
5051
Annotated,
5152
Callable,
@@ -135,7 +136,7 @@ def property(default_factory: ValueFactory, readonly: bool = False) -> Value: ..
135136

136137

137138
def property(
138-
default: Value | ValueGetter | None = None,
139+
default: Value | ValueGetter | EllipsisType = ...,
139140
*,
140141
default_factory: ValueFactory | None = None,
141142
readonly: bool = False,
@@ -195,6 +196,11 @@ def property(
195196
as ``mypy`` will check that the default is valid for the type of the
196197
field, and won't raise an error about assigning, for example, an
197198
instance of ``DataProperty[int]`` to a field annotated as ``int``.
199+
200+
Finally, the type of the ``default`` argument includes `.EllipsisType`
201+
so that we can use ``...`` as its default value. This allows us to
202+
distinguish between ``default`` not being set (``...``) and a desired
203+
default value of ``None``.
198204
"""
199205
if callable(default):
200206
# If the default is callable, we're being used as a decorator
@@ -231,7 +237,7 @@ class DataProperty(BaseProperty[Value], Generic[Value]):
231237

232238
def __init__(
233239
self,
234-
default: Value | None = None,
240+
default: Value | EllipsisType = ...,
235241
*,
236242
default_factory: ValueFactory | None,
237243
readonly: bool = False,
@@ -256,7 +262,9 @@ def __init__(
256262
``__init__``.
257263
258264
:param default: the default value. This or ``default_factory`` must
259-
be provided.
265+
be provided. Note that, as ``None`` is a valid default value,
266+
this uses ``...`` instead as a way of checking whether ``default``
267+
has been set.
260268
:param default_factory: a function that returns the default value.
261269
This is appropriate for datatypes such as lists, where using
262270
a mutable default value can lead to odd behaviour.
@@ -268,13 +276,17 @@ def __init__(
268276
factory function are specified.
269277
:raises MissingDefaultError: if no default is provided.
270278
"""
271-
if default_factory is not None:
272-
if default is not None:
273-
raise OverspecifiedDefaultError()
274-
self._default_value: Value = default_factory()
275-
if default is None:
279+
if default_factory is not None and default is not ...:
280+
raise OverspecifiedDefaultError()
281+
if default_factory is None and default is ...:
276282
raise MissingDefaultError()
277-
self._default_value: Value = default
283+
# The default value will come from whichever of `default` and `default_factory`
284+
# is specified. The two checks above ensure that exactly one will exist.
285+
self._default_value: Value
286+
if default_factory:
287+
self._default_value = default_factory
288+
else:
289+
self._default_value = default
278290
self.readonly = readonly
279291
self._type: type | None = None # Will be set in __set_name__
280292

@@ -498,7 +510,7 @@ def property_affordance(
498510

499511

500512
def setting(
501-
default: Value | None = None,
513+
default: Value | EllipsisType = ...,
502514
*,
503515
default_factory: ValueFactory | None = None,
504516
readonly: bool = False,

tests/test_action_cancel.py

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,13 @@
1010

1111

1212
class CancellableCountingThing(lt.Thing):
13-
counter = lt.ThingProperty(int, 0, observable=False)
14-
check = lt.ThingProperty(
15-
bool,
16-
False,
17-
observable=False,
18-
description=(
19-
"This variable is used to check that the action can detect a cancel event "
20-
"and react by performing another task, in this case, setting this variable."
21-
),
22-
)
13+
counter: int = lt.property(0)
14+
check: bool = lt.property(False)
15+
"""Whether the count has been cancelled.
16+
17+
This variable is used to check that the action can detect a cancel event
18+
and react by performing another task, in this case, setting this variable.
19+
"""
2320

2421
@lt.thing_action
2522
def count_slowly(self, cancel: lt.deps.CancelHook, n: int = 10):

tests/test_action_manager.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,8 @@ def increment_counter(self):
1313
"""Increment the counter"""
1414
self.counter += 1
1515

16-
counter = lt.ThingProperty(
17-
model=int, initial_value=0, readonly=True, description="A pointless counter"
18-
)
16+
counter: int = lt.property(0, readonly=True)
17+
"A pointless counter"
1918

2019

2120
thing = TestThing()

tests/test_dependency_metadata.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ def __init__(self):
1313
lt.Thing.__init__(self)
1414
self._a = 0
1515

16-
@lt.thing_property
16+
@lt.property
1717
def a(self):
1818
return self._a
1919

tests/test_properties.py

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,18 +9,21 @@
99

1010

1111
class TestThing(lt.Thing):
12-
boolprop = lt.ThingProperty(bool, False, description="A boolean property")
13-
stringprop = lt.ThingProperty(str, "foo", description="A string property")
12+
boolprop: bool = lt.property(False)
13+
"A boolean property"
14+
15+
stringprop: str = lt.property("foo")
16+
"A string property"
1417

1518
_undoc = None
1619

17-
@lt.thing_property
20+
@lt.property
1821
def undoc(self):
1922
return self._undoc
2023

2124
_float = 1.0
2225

23-
@lt.thing_property
26+
@lt.property
2427
def floatprop(self) -> float:
2528
return self._float
2629

@@ -45,12 +48,12 @@ def toggle_boolprop_from_thread(self):
4548

4649
def test_instantiation_with_type():
4750
"""
48-
Check the internal model (data type) of the ThingSetting descriptor is a BaseModel
51+
Check the internal model (data type) of the DataProperty descriptor is a BaseModel
4952
5053
To send the data over HTTP LabThings-FastAPI uses Pydantic models to describe data
5154
types.
5255
"""
53-
prop = lt.ThingProperty(bool, False)
56+
prop: bool = lt.property(False)
5457
assert issubclass(prop.model, BaseModel)
5558

5659

@@ -59,7 +62,7 @@ class MyModel(BaseModel):
5962
a: int = 1
6063
b: float = 2.0
6164

62-
prop = lt.ThingProperty(MyModel, MyModel())
65+
prop: MyModel = lt.property(MyModel())
6366
assert prop.model is MyModel
6467

6568

@@ -71,7 +74,7 @@ def test_property_get_and_set():
7174
assert after_value.json() == test_str
7275

7376

74-
def test_ThingProperty():
77+
def test_boolprop():
7578
with TestClient(server.app) as client:
7679
r = client.get("/thing/boolprop")
7780
assert r.json() is False
@@ -123,7 +126,7 @@ def test_setting_from_thread():
123126

124127

125128
def test_setting_without_event_loop():
126-
"""Test that an exception is raised if updating a ThingProperty
129+
"""Test that an exception is raised if updating a DataProperty
127130
without connecting the Thing to a running server with an event loop.
128131
"""
129132
# This test may need to change, if we change the intended behaviour

tests/test_settings.py

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -11,21 +11,17 @@
1111

1212

1313
class TestThing(lt.Thing):
14-
boolsetting = lt.ThingSetting(bool, False, description="A boolean setting")
15-
stringsetting = lt.ThingSetting(str, "foo", description="A string setting")
16-
dictsetting = lt.ThingSetting(
17-
dict, {"a": 1, "b": 2}, description="A dictionary setting"
18-
)
14+
boolsetting: bool = lt.setting(False)
15+
"A boolean setting"
1916

20-
_float = 1.0
17+
stringsetting: str = lt.setting("foo")
18+
"A string setting"
2119

22-
@lt.thing_setting
23-
def floatsetting(self) -> float:
24-
return self._float
20+
dictsetting: dict = lt.setting(default_factory=lambda: {"a": 1, "b": 2})
21+
"A dictionary setting"
2522

26-
@floatsetting.setter
27-
def floatsetting(self, value: float):
28-
self._float = value
23+
floatsetting: float = lt.setting(1.0)
24+
"A float setting"
2925

3026
@lt.thing_action
3127
def toggle_boolsetting(self):

tests/test_thing_lifecycle.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33

44

55
class TestThing(lt.Thing):
6-
alive = lt.ThingProperty(bool, False, description="Is the thing alive?")
6+
alive: bool = lt.property(False)
7+
"Whether the thing is alive."
78

89
def __enter__(self):
910
print("setting up TestThing from __enter__")

0 commit comments

Comments
 (0)