From a7625c6e31b7264567158d664043267f70b0023c Mon Sep 17 00:00:00 2001 From: Julian Stirling Date: Mon, 30 Jun 2025 09:35:28 +0100 Subject: [PATCH] Adding typehints and overloads for typing properties --- src/labthings_fastapi/descriptors/property.py | 31 ++++++++++++++++--- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/src/labthings_fastapi/descriptors/property.py b/src/labthings_fastapi/descriptors/property.py index f51a31cf..3fd4dfc6 100644 --- a/src/labthings_fastapi/descriptors/property.py +++ b/src/labthings_fastapi/descriptors/property.py @@ -3,7 +3,17 @@ """ from __future__ import annotations -from typing import TYPE_CHECKING, Annotated, Any, Callable, Optional +from typing import ( + TYPE_CHECKING, + Annotated, + Any, + Callable, + Optional, + Generic, + Type, + TypeVar, + overload, +) from weakref import WeakSet from typing_extensions import Self @@ -20,8 +30,11 @@ if TYPE_CHECKING: from ..thing import Thing +Value = TypeVar("Value") +Descriptor = TypeVar("Descriptor") -class ThingProperty: + +class ThingProperty(Generic[Value]): """A property that can be accessed via the HTTP API By default, a ThingProperty is "dumb", i.e. it acts just like @@ -39,7 +52,7 @@ def __init__( observable: bool = False, description: Optional[str] = None, title: Optional[str] = None, - getter: Optional[Callable] = None, + getter: Optional[Callable[[Thing, Type[Thing]], Value]] = None, setter: Optional[Callable] = None, ): if getter and initial_value is not None: @@ -76,7 +89,15 @@ def description(self): """A description of the property""" return self._description or get_docstring(self._getter, remove_summary=True) - def __get__(self, obj, type=None) -> Any: + @overload + def __get__(self, obj: None, owner: Type[Thing]) -> Descriptor: + """Called when an attribute is accessed via class not an instance""" + + @overload + def __get__(self, obj: Thing, owner: Type[Thing]) -> Value: + """Called when an attribute is accessed on an instance variable""" + + def __get__(self, obj: Optional[Thing], owner: Type[Thing]) -> Value | Descriptor: """The value of the property If `obj` is none (i.e. we are getting the attribute of the class), @@ -187,7 +208,7 @@ def set_property(body): # We'll annotate body later description=f"## {self.title}\n\n{self.description or ''}", ) def get_property(): - return self.__get__(thing) + return self.__get__(thing, type(thing)) def property_affordance( self, thing: Thing, path: Optional[str] = None