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
0 commit comments