Skip to content

Commit 7ad0755

Browse files
committed
Fix test_property tests
This caught a few errors in my rearrangement - most notably that only DataProperties were getting added to the HTTP API. I've fixed things so all properties now appear on HTTP, and improved model generation so it has helpful errors and works for both data and functional properties. I also removed the numpy mypy plugin because the warning got annoying.
1 parent 54b19f0 commit 7ad0755

File tree

6 files changed

+206
-118
lines changed

6 files changed

+206
-118
lines changed

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ preview = true
9393
"docs/*" = ["D", "DOC"]
9494

9595
[tool.mypy]
96-
plugins = ["pydantic.mypy", "numpy.typing.mypy_plugin"]
96+
plugins = ["pydantic.mypy"]
9797

9898
[tool.flake8]
9999
extend-ignore = [

src/labthings_fastapi/base_descriptor.py

Lines changed: 46 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,31 @@
1717
"""The value returned by the descriptor, when called on an instance."""
1818

1919

20-
class NameNotSetError(AttributeError):
21-
"""Descriptor name is not yet available.
20+
class DescriptorNotAddedToClassError(RuntimeError):
21+
"""Descriptor has not yet been added to a class.
2222
23-
This error is raised if the name of an affordance is accessed
24-
before ``__set_name__`` has been called on the descriptor.
23+
This error is raised if certain properties of descriptors are accessed
24+
before ``__set_name__`` has been called on the descriptor. ``__set_name``
25+
is part of the descriptor protocol, and is called when a class is defined
26+
to notify the descriptor of its name and owning class.
27+
28+
If you see this error, it often means that a descriptor has been instantiated
29+
but not attached to a class, for example:
30+
31+
.. code-block:: python
32+
33+
import labthings as lt
34+
35+
36+
class Test(lt.Thing):
37+
myprop: int = lt.property(0) # This is OK
38+
39+
40+
orphaned_prop: int = lt.property(0) # Not OK
41+
42+
Test.myprop.model # Evaluates to a pydantic model
43+
44+
orphaned_prop.model # Raises this exception
2545
"""
2646

2747

@@ -44,6 +64,9 @@ def __init__(self) -> None:
4464
# We set the instance __doc__ to None so the descriptor class docstring
4565
# doesn't get picked up by OpenAPI/Thing Description.
4666
self.__doc__ = None
67+
# We explicitly check when __set_name__ is called, so we can raise helpful
68+
# errors
69+
self._set_name_called: bool = False
4770

4871
def __set_name__(self, owner: type[Thing], name: str) -> None:
4972
r"""Take note of the name to which the descriptor is assigned.
@@ -58,6 +81,21 @@ def __set_name__(self, owner: type[Thing], name: str) -> None:
5881
# Remember the name to which we're assigned. Accessed by the read only
5982
# property ``name``.
6083
self._name = name
84+
self._set_name_called = True
85+
86+
def assert_set_name_called(self):
87+
"""Raise an exception if ``__set_name__`` has not yet been called.
88+
89+
:raises DescriptorNotAddedToClassError: if ``__set_name__`` has not yet
90+
been called.
91+
"""
92+
if not self._set_name_called:
93+
raise DescriptorNotAddedToClassError(
94+
f"{self.__class__.__name__} must be assigned to an attribute of "
95+
"a class, as part of the class definition. This exception is "
96+
"raised because `__set_name__` has not yet been called, which "
97+
"usually means it was not instantiated as a class attribute."
98+
)
6199

62100
@property
63101
def name(self) -> str:
@@ -74,8 +112,10 @@ def name(self) -> str:
74112
:raises NameNotSetError: if it is accessed before the descriptor
75113
has been notified of its name.
76114
"""
77-
if self._name is None:
78-
raise NameNotSetError
115+
self.assert_set_name_called()
116+
assert self._name is not None
117+
# The assert statement is mostly for typing: if assert_set_name_called
118+
# doesn't raise an error, self._name has been set.
79119
return self._name
80120

81121
@property

src/labthings_fastapi/exceptions.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ def __init__(self, message: str | None = None):
2121
2222
:param message: the optional message.
2323
"""
24-
# type: ignore[call-arg] is used here because mypy can't know
24+
# We ignore call-arg within this function because mypy can't know
2525
# that this is a mixin, and super() will be an exception (which does
2626
# accept a string argument to `__init__`).
2727
doc = inspect.cleandoc(self.__doc__) if self.__doc__ else None

src/labthings_fastapi/thing.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121

2222
from pydantic import BaseModel
2323

24-
from .thing_property import BaseProperty, ThingSetting
24+
from .thing_property import DataProperty, ThingSetting
2525
from .descriptors import ActionDescriptor
2626
from .thing_description._model import ThingDescription, NoSecurityScheme
2727
from .utilities import class_attributes
@@ -348,7 +348,7 @@ def observe_property(self, property_name: str, stream: ObjectSendStream) -> None
348348
:raise KeyError: if the requested name is not defined on this Thing.
349349
"""
350350
prop = getattr(self.__class__, property_name)
351-
if not isinstance(prop, BaseProperty):
351+
if not isinstance(prop, DataProperty):
352352
raise KeyError(f"{property_name} is not a LabThings Property")
353353
prop._observers_set(self).add(stream)
354354

0 commit comments

Comments
 (0)