Skip to content

Commit cee96bc

Browse files
committed
Move __set__ from BaseDescriptor to FieldTypedBaseDescriptor
Downstream in OpenFlexure, unit tests often patch live objects to mock their actions. This stopped working when `BaseDescriptor` gained a `__set__` method. Moving `__set__` to `FieldTypedBaseDescriptor` doesn't change any meaningful LabThings functionality but should restore the tests for OpenFlexure.
1 parent 1981383 commit cee96bc

File tree

1 file changed

+47
-32
lines changed

1 file changed

+47
-32
lines changed

src/labthings_fastapi/base_descriptor.py

Lines changed: 47 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -335,22 +335,6 @@ def get(self) -> Value:
335335
descriptor = self.get_descriptor()
336336
return descriptor.__get__(self.owning_object_or_error())
337337

338-
def set(self, value: Value) -> None:
339-
"""Set the value of the descriptor.
340-
341-
This method may only be called if the DescriptorInfo object is bound to a
342-
`.Thing` instance. It will raise an error if called on a class.
343-
344-
:param value: the new value.
345-
346-
:raises NotBoundToInstanceError: if called on an unbound info object.
347-
"""
348-
if not self.is_bound:
349-
msg = f"We can't set the value of {self.name} when called on a class."
350-
raise NotBoundToInstanceError(msg)
351-
descriptor = self.get_descriptor()
352-
descriptor.__set__(self.owning_object_or_error(), value)
353-
354338
def __eq__(self, other: Any) -> bool:
355339
"""Determine if this object is equal to another one.
356340
@@ -404,6 +388,11 @@ class Example:
404388
assert p.name == "my_prop"
405389
assert p.title == "My Property."
406390
assert p.description.startswith("This is")
391+
392+
`.BaseDescriptor` is a "non-data descriptor" (meaning it doesn't implement
393+
``__set__``). This allows it to be overwritten by assigning to an object's
394+
attribute, which can be useful in test code. This can easily be changed in
395+
subclasses by implementing ``__set__``\ .
407396
"""
408397

409398
def __init__(self) -> None:
@@ -593,21 +582,6 @@ def instance_get(self, obj: Owner) -> Value:
593582
"See BaseDescriptor.__instance_get__ for details."
594583
)
595584

596-
def __set__(self, obj: Owner, value: Value) -> None:
597-
"""Mark the `BaseDescriptor` as a data descriptor.
598-
599-
Even for read-only descriptors, it's important to define a ``__set__`` method.
600-
The presence of this method prevents Python overwriting the descriptor when
601-
a value is assigned. This base implementation returns an `AttributeError` to
602-
signal that the descriptor is read-only. Overriding it with a method that
603-
does not raise an exception will allow the descriptor to be written to.
604-
605-
:param obj: The object on which to set the value.
606-
:param value: The value to set the descriptor to.
607-
:raises AttributeError: always, as this is read-only by default.
608-
"""
609-
raise AttributeError("This attribute is read-only.")
610-
611585
def _descriptor_info(
612586
self, info_class: type[DescriptorInfoT], obj: Owner | None = None
613587
) -> DescriptorInfoT:
@@ -669,9 +643,35 @@ def value_type(self) -> type[Value]:
669643
"""The type of the descriptor's value."""
670644
return self.get_descriptor().value_type
671645

646+
def set(self, value: Value) -> None:
647+
"""Set the value of the descriptor.
648+
649+
This method may only be called if the DescriptorInfo object is bound to a
650+
`.Thing` instance. It will raise an error if called on a class.
651+
652+
:param value: the new value.
653+
654+
:raises NotBoundToInstanceError: if called on an unbound info object.
655+
"""
656+
if not self.is_bound:
657+
msg = f"We can't set the value of {self.name} when called on a class."
658+
raise NotBoundToInstanceError(msg)
659+
descriptor = self.get_descriptor()
660+
descriptor.__set__(self.owning_object_or_error(), value)
661+
672662

673663
class FieldTypedBaseDescriptor(Generic[Owner, Value], BaseDescriptor[Owner, Value]):
674-
"""A BaseDescriptor that determines its type like a dataclass field."""
664+
r"""A `.BaseDescriptor` that determines its type like a dataclass field.
665+
666+
This adds two things to `.BaseDescriptor`\ :
667+
668+
1. Descriptors inheriting from this class will inspect the type annotations of
669+
their owning class when determining ``value_type``\ .
670+
2. This class and its children will be "data descriptors" because there is a
671+
stub implementation of ``__set__``\ . This means that the attribute may not
672+
be assigned to (unless ``__set__`` is overridden). This is the behaviour
673+
that `builtins.property` has.
674+
"""
675675

676676
def __init__(self) -> None:
677677
"""Initialise the FieldTypedBaseDescriptor.
@@ -852,6 +852,21 @@ def descriptor_info(
852852
"""
853853
return self._descriptor_info(FieldTypedBaseDescriptorInfo, owner)
854854

855+
def __set__(self, obj: Owner, value: Value) -> None:
856+
"""Mark the `BaseDescriptor` as a data descriptor.
857+
858+
Even for read-only descriptors, it's important to define a ``__set__`` method.
859+
The presence of this method prevents Python overwriting the descriptor when
860+
a value is assigned. This base implementation returns an `AttributeError` to
861+
signal that the descriptor is read-only. Overriding it with a method that
862+
does not raise an exception will allow the descriptor to be written to.
863+
864+
:param obj: The object on which to set the value.
865+
:param value: The value to set the descriptor to.
866+
:raises AttributeError: always, as this is read-only by default.
867+
"""
868+
raise AttributeError("This attribute is read-only.")
869+
855870

856871
class DescriptorInfoCollection(
857872
Mapping[str, DescriptorInfoT],

0 commit comments

Comments
 (0)