diff --git a/src/labthings_fastapi/actions.py b/src/labthings_fastapi/actions.py index 95b9910..f75325e 100644 --- a/src/labthings_fastapi/actions.py +++ b/src/labthings_fastapi/actions.py @@ -247,10 +247,10 @@ def response(self, request: Optional[Request] = None) -> InvocationModel: ] # The line below confuses MyPy because self.action **evaluates to** a Descriptor # object (i.e. we don't call __get__ on the descriptor). - return self.action.invocation_model( # type: ignore[call-overload] + return self.action.invocation_model( # type: ignore[attr-defined] status=self.status, id=self.id, - action=self.thing.path + self.action.name, # type: ignore[call-overload] + action=self.thing.path + self.action.name, # type: ignore[attr-defined] href=href, timeStarted=self._start_time, timeCompleted=self._end_time, @@ -290,7 +290,7 @@ def run(self) -> None: """ # self.action evaluates to an ActionDescriptor. This confuses mypy, # which thinks we are calling ActionDescriptor.__get__. - action: ActionDescriptor = self.action # type: ignore[call-overload] + action: ActionDescriptor = self.action # type: ignore[assignment] logger = self.thing.logger # The line below saves records matching our ID to ``self._log`` add_thing_log_destination(self.id, self._log) @@ -445,10 +445,7 @@ def list_invocations( i.response(request=request) for i in self.invocations if thing is None or i.thing == thing - if action is None or i.action == action # type: ignore[call-overload] - # i.action evaluates to an ActionDescriptor, which confuses mypy - it - # thinks we are calling ActionDescriptor.__get__ but this isn't ever - # called. + if action is None or i.action == action ] def expire_invocations(self) -> None: @@ -626,7 +623,7 @@ def delete_invocation(id: uuid.UUID) -> None: class ActionDescriptor( - BaseDescriptor[Callable[ActionParams, ActionReturn]], + BaseDescriptor[OwnerT, Callable[ActionParams, ActionReturn]], Generic[ActionParams, ActionReturn, OwnerT], ): """Wrap actions to enable them to be run over HTTP. @@ -691,7 +688,7 @@ def __init__( ) self.invocation_model.__name__ = f"{name}_invocation" - def __set_name__(self, owner: type[Thing], name: str) -> None: + def __set_name__(self, owner: type[OwnerT], name: str) -> None: """Ensure the action name matches the function name. It's assumed in a few places that the function name and the @@ -709,7 +706,7 @@ def __set_name__(self, owner: type[Thing], name: str) -> None: f"'{self.func.__name__}'", ) - def instance_get(self, obj: Thing) -> Callable[ActionParams, ActionReturn]: + def instance_get(self, obj: OwnerT) -> Callable[ActionParams, ActionReturn]: """Return the function, bound to an object as for a normal method. This currently doesn't validate the arguments, though it may do so @@ -721,10 +718,7 @@ def instance_get(self, obj: Thing) -> Callable[ActionParams, ActionReturn]: descriptor. :return: the action function, bound to ``obj``. """ - # `obj` should be of type `OwnerT`, but `BaseDescriptor` currently - # isn't generic in the type of the owning Thing, so we can't express - # that here. - return partial(self.func, obj) # type: ignore[arg-type] + return partial(self.func, obj) def _observers_set(self, obj: Thing) -> WeakSet: """Return a set used to notify changes. diff --git a/src/labthings_fastapi/base_descriptor.py b/src/labthings_fastapi/base_descriptor.py index 35b0436..f229c1d 100644 --- a/src/labthings_fastapi/base_descriptor.py +++ b/src/labthings_fastapi/base_descriptor.py @@ -26,6 +26,12 @@ Value = TypeVar("Value") """The value returned by the descriptor, when called on an instance.""" +Owner = TypeVar("Owner", bound="Thing") +"""A Thing subclass that owns a descriptor.""" + +Descriptor = TypeVar("Descriptor", bound="BaseDescriptor") +"""The type of a descriptor that's referred to by a `BaseDescriptorInfo` object.""" + class DescriptorNotAddedToClassError(RuntimeError): """Descriptor has not yet been added to a class. @@ -138,7 +144,7 @@ def _set_prop4(self, val): """ -class BaseDescriptor(Generic[Value]): +class BaseDescriptor(Generic[Owner, Value]): r"""A base class for descriptors in LabThings-FastAPI. This class implements several behaviours common to descriptors in LabThings: @@ -184,7 +190,7 @@ def __init__(self) -> None: self._set_name_called: bool = False self._owner_name: str = "" - def __set_name__(self, owner: type[Thing], name: str) -> None: + def __set_name__(self, owner: type[Owner], name: str) -> None: r"""Take note of the name to which the descriptor is assigned. This is called when the descriptor is assigned to an attribute of a class. @@ -306,12 +312,12 @@ def description(self) -> str | None: # I have ignored D105 (missing docstrings) on the overloads - these should not # exist on @overload definitions. @overload - def __get__(self, obj: Thing, type: type | None = None) -> Value: ... + def __get__(self, obj: Owner, type: type | None = None) -> Value: ... @overload def __get__(self, obj: None, type: type) -> Self: ... - def __get__(self, obj: Thing | None, type: type | None = None) -> Value | Self: + def __get__(self, obj: Owner | None, type: type | None = None) -> Value | Self: """Return the value or the descriptor, as per `property`. If ``obj`` is ``None`` (i.e. the descriptor is accessed as a class attribute), @@ -331,7 +337,7 @@ def __get__(self, obj: Thing | None, type: type | None = None) -> Value | Self: return self.instance_get(obj) return self - def instance_get(self, obj: Thing) -> Value: + def instance_get(self, obj: Owner) -> Value: """Return the value of the descriptor. This method is called from ``__get__`` if the descriptor is accessed as an @@ -357,7 +363,7 @@ def instance_get(self, obj: Thing) -> Value: ) -class FieldTypedBaseDescriptor(Generic[Value], BaseDescriptor[Value]): +class FieldTypedBaseDescriptor(Generic[Owner, Value], BaseDescriptor[Owner, Value]): """A BaseDescriptor that determines its type like a dataclass field.""" def __init__(self) -> None: @@ -379,7 +385,7 @@ def __init__(self) -> None: # the object on which they are defined, to provide the context for the # evaluation. - def __set_name__(self, owner: type[Thing], name: str) -> None: + def __set_name__(self, owner: type[Owner], name: str) -> None: r"""Take note of the name and type. This function is where we determine the type of the property. It may @@ -431,7 +437,7 @@ class MyThing(Thing): # __orig_class__ is set on generic classes when they are instantiated # with a subscripted type. It is not available during __init__, which # is why we check for it here. - self._type = typing.get_args(self.__orig_class__)[0] + self._type = typing.get_args(self.__orig_class__)[1] if isinstance(self._type, typing.ForwardRef): raise MissingTypeError( f"{owner}.{name} is a subscripted descriptor, where the " diff --git a/src/labthings_fastapi/properties.py b/src/labthings_fastapi/properties.py index 5be2998..c948c9d 100644 --- a/src/labthings_fastapi/properties.py +++ b/src/labthings_fastapi/properties.py @@ -54,7 +54,6 @@ class attribute. Documentation is in strings immediately following the Any, Callable, Generic, - TypeAlias, TypeVar, overload, TYPE_CHECKING, @@ -133,29 +132,16 @@ class MissingDefaultError(ValueError): Value = TypeVar("Value") -if TYPE_CHECKING: - # It's hard to type check methods, because the type of ``self`` - # will be a subclass of `.Thing`, and `Callable` types are - # contravariant in their arguments (i.e. - # ``Callable[[SpecificThing,], Value])`` is not a subtype of - # ``Callable[[Thing,], Value]``. - # It is probably not particularly important for us to check th - # type of ``self`` when decorating methods, so it is left as - # ``Any`` to avoid the major confusion that would result from - # trying to type it more tightly. - # - # Note: in ``@overload`` definitions, it's sometimes necessary - # to avoid the use of these aliases, as ``mypy`` can't - # pick which variant is in use without the explicit `Callable`. - ValueFactory: TypeAlias = Callable[[], Value] - ValueGetter: TypeAlias = Callable[[Any], Value] - ValueSetter: TypeAlias = Callable[[Any, Value], None] +"""The value returned by a property.""" + +Owner = TypeVar("Owner", bound="Thing") +"""The `.Thing` instance on which a property is bound.""" def default_factory_from_arguments( default: Value | EllipsisType = ..., - default_factory: ValueFactory | None = None, -) -> ValueFactory: + default_factory: Callable[[], Value] | None = None, +) -> Callable[[], Value]: """Process default arguments to get a default factory function. This function takes the ``default`` and ``default_factory`` arguments @@ -198,9 +184,11 @@ def default_factory_from_arguments( raise OverspecifiedDefaultError() if default is not ...: - def default_factory() -> Value: + def dummy_default_factory() -> Value: return default + default_factory = dummy_default_factory + if not callable(default_factory): raise MissingDefaultError("The default_factory must be callable.") return default_factory @@ -209,8 +197,8 @@ def default_factory() -> Value: # See comment at the top of the file regarding ignored linter rules. @overload # use as a decorator @property def property( - getter: Callable[[Any], Value], -) -> FunctionalProperty[Value]: ... + getter: Callable[[Owner], Value], +) -> FunctionalProperty[Owner, Value]: ... @overload # use as `field: int = property(default=0)` @@ -226,13 +214,13 @@ def property( def property( - getter: ValueGetter | EllipsisType = ..., + getter: Callable[[Owner], Value] | EllipsisType = ..., *, default: Value | EllipsisType = ..., - default_factory: ValueFactory | None = None, + default_factory: Callable[[], Value] | None = None, readonly: bool = False, **constraints: Any, -) -> Value | FunctionalProperty[Value]: +) -> Value | FunctionalProperty[Owner, Value]: r"""Define a Property on a `.Thing`\ . This function may be used to define :ref:`properties` in @@ -328,7 +316,7 @@ def property( ) -class BaseProperty(FieldTypedBaseDescriptor[Value], Generic[Value]): +class BaseProperty(FieldTypedBaseDescriptor[Owner, Value], Generic[Owner, Value]): """A descriptor that marks Properties on Things. This class is used to determine whether an attribute of a `.Thing` should @@ -396,7 +384,7 @@ def model(self) -> type[BaseModel]: ) return self._model - def add_to_fastapi(self, app: FastAPI, thing: Thing) -> None: + def add_to_fastapi(self, app: FastAPI, thing: Owner) -> None: """Add this action to a FastAPI app, bound to a particular Thing. :param app: The FastAPI application we are adding endpoints to. @@ -487,7 +475,7 @@ def property_affordance( } ) - def __set__(self, obj: Thing, value: Any) -> None: + def __set__(self, obj: Owner, value: Any) -> None: """Set the property (stub method). This is a stub ``__set__`` method to mark this as a data descriptor. @@ -501,7 +489,7 @@ def __set__(self, obj: Thing, value: Any) -> None: ) -class DataProperty(BaseProperty[Value], Generic[Value]): +class DataProperty(BaseProperty[Owner, Value], Generic[Owner, Value]): """A Property descriptor that acts like a regular variable. `.DataProperty` descriptors remember their value, and can be read and @@ -521,7 +509,7 @@ def __init__( # noqa: DOC101,DOC103 def __init__( # noqa: DOC101,DOC103 self, *, - default_factory: ValueFactory, + default_factory: Callable[[], Value], readonly: bool = False, constraints: Mapping[str, Any] | None = None, ) -> None: ... @@ -530,7 +518,7 @@ def __init__( self, default: Value | EllipsisType = ..., *, - default_factory: ValueFactory | None = None, + default_factory: Callable[[], Value] | None = None, readonly: bool = False, constraints: Mapping[str, Any] | None = None, ) -> None: @@ -573,7 +561,7 @@ def __init__( ) self.readonly = readonly - def instance_get(self, obj: Thing) -> Value: + def instance_get(self, obj: Owner) -> Value: """Return the property's value. This will supply a default if the property has not yet been set. @@ -588,7 +576,7 @@ def instance_get(self, obj: Thing) -> Value: return obj.__dict__[self.name] def __set__( - self, obj: Thing, value: Value, emit_changed_event: bool = True + self, obj: Owner, value: Value, emit_changed_event: bool = True ) -> None: """Set the property's value. @@ -653,7 +641,7 @@ async def emit_changed_event_async(self, obj: Thing, value: Value) -> None: ) -class FunctionalProperty(BaseProperty[Value], Generic[Value]): +class FunctionalProperty(BaseProperty[Owner, Value], Generic[Owner, Value]): """A property that uses a getter and a setter. For properties that should work like variables, use `.DataProperty`. For @@ -665,7 +653,7 @@ class FunctionalProperty(BaseProperty[Value], Generic[Value]): def __init__( self, - fget: ValueGetter, + fget: Callable[[Owner], Value], constraints: Mapping[str, Any] | None = None, ) -> None: """Set up a FunctionalProperty. @@ -683,7 +671,7 @@ def __init__( :raises MissingTypeError: if the getter does not have a return type annotation. """ super().__init__(constraints=constraints) - self._fget: ValueGetter = fget + self._fget = fget self._type = return_type(self._fget) if self._type is None: msg = ( @@ -691,20 +679,20 @@ def __init__( "Return type annotations are required for property getters." ) raise MissingTypeError(msg) - self._fset: ValueSetter | None = None + self._fset: Callable[[Owner, Value], None] | None = None self.readonly: bool = True @builtins.property - def fget(self) -> ValueGetter: # noqa: DOC201 + def fget(self) -> Callable[[Owner], Value]: # noqa: DOC201 """The getter function.""" return self._fget @builtins.property - def fset(self) -> ValueSetter | None: # noqa: DOC201 + def fset(self) -> Callable[[Owner, Value], None] | None: # noqa: DOC201 """The setter function.""" return self._fset - def getter(self, fget: ValueGetter) -> Self: + def getter(self, fget: Callable[[Owner], Value]) -> Self: """Set the getter function of the property. This function returns the descriptor, so it may be used as a decorator. @@ -718,7 +706,7 @@ def getter(self, fget: ValueGetter) -> Self: self.__doc__ = fget.__doc__ return self - def setter(self, fset: ValueSetter) -> Self: + def setter(self, fset: Callable[[Owner, Value], None]) -> Self: r"""Set the setter function of the property. This function returns the descriptor, so it may be used as a decorator. @@ -790,7 +778,7 @@ def _set_myprop(self, val: int) -> None: return fset # type: ignore[return-value] return self - def instance_get(self, obj: Thing) -> Value: + def instance_get(self, obj: Owner) -> Value: """Get the value of the property. :param obj: the `.Thing` on which the attribute is accessed. @@ -798,7 +786,7 @@ def instance_get(self, obj: Thing) -> Value: """ return self.fget(obj) - def __set__(self, obj: Thing, value: Value) -> None: + def __set__(self, obj: Owner, value: Value) -> None: """Set the value of the property. :param obj: the `.Thing` on which the attribute is accessed. @@ -813,8 +801,8 @@ def __set__(self, obj: Thing, value: Value) -> None: @overload # use as a decorator @setting def setting( - getter: Callable[[Any], Value], -) -> FunctionalSetting[Value]: ... + getter: Callable[[Owner], Value], +) -> FunctionalSetting[Owner, Value]: ... @overload # use as `field: int = setting(default=0)`` @@ -828,13 +816,13 @@ def setting( def setting( - getter: ValueGetter | EllipsisType = ..., + getter: Callable[[Owner], Value] | EllipsisType = ..., *, default: Value | EllipsisType = ..., - default_factory: ValueFactory | None = None, + default_factory: Callable[[], Value] | None = None, readonly: bool = False, **constraints: Any, -) -> FunctionalSetting[Value] | Value: +) -> FunctionalSetting[Owner, Value] | Value: r"""Define a Setting on a `.Thing`\ . A setting is a property that is saved to disk. @@ -920,7 +908,7 @@ def setting( ) -class BaseSetting(BaseProperty[Value], Generic[Value]): +class BaseSetting(BaseProperty[Owner, Value], Generic[Owner, Value]): r"""A base class for settings. This is a subclass of `.BaseProperty` that is used to define settings. @@ -928,7 +916,7 @@ class BaseSetting(BaseProperty[Value], Generic[Value]): two concrete implementations: `.DataSetting` and `.FunctionalSetting`\ . """ - def set_without_emit(self, obj: Thing, value: Value) -> None: + def set_without_emit(self, obj: Owner, value: Value) -> None: """Set the setting's value without emitting an event. This is used to set the setting's value without notifying observers. @@ -943,7 +931,9 @@ def set_without_emit(self, obj: Thing, value: Value) -> None: raise NotImplementedError("This method should be implemented in subclasses.") -class DataSetting(DataProperty[Value], BaseSetting[Value], Generic[Value]): +class DataSetting( + DataProperty[Owner, Value], BaseSetting[Owner, Value], Generic[Owner, Value] +): """A `.DataProperty` that persists on disk. A setting can be accessed via the HTTP API and is persistent between sessions. @@ -961,7 +951,7 @@ class DataSetting(DataProperty[Value], BaseSetting[Value], Generic[Value]): """ def __set__( - self, obj: Thing, value: Value, emit_changed_event: bool = True + self, obj: Owner, value: Value, emit_changed_event: bool = True ) -> None: """Set the setting's value. @@ -974,7 +964,7 @@ def __set__( super().__set__(obj, value, emit_changed_event) obj.save_settings() - def set_without_emit(self, obj: Thing, value: Value) -> None: + def set_without_emit(self, obj: Owner, value: Value) -> None: """Set the property's value, but do not emit event to notify the server. This function is not expected to be used externally. It is called during @@ -987,7 +977,9 @@ def set_without_emit(self, obj: Thing, value: Value) -> None: super().__set__(obj, value, emit_changed_event=False) -class FunctionalSetting(FunctionalProperty[Value], BaseSetting[Value], Generic[Value]): +class FunctionalSetting( + FunctionalProperty[Owner, Value], BaseSetting[Owner, Value], Generic[Owner, Value] +): """A `.FunctionalProperty` that persists on disk. A setting can be accessed via the HTTP API and is persistent between sessions. @@ -1005,7 +997,7 @@ class FunctionalSetting(FunctionalProperty[Value], BaseSetting[Value], Generic[V getter and a setter function. """ - def __set__(self, obj: Thing, value: Value) -> None: + def __set__(self, obj: Owner, value: Value) -> None: """Set the setting's value. This will cause the settings to be saved to disk. @@ -1016,7 +1008,7 @@ def __set__(self, obj: Thing, value: Value) -> None: super().__set__(obj, value) obj.save_settings() - def set_without_emit(self, obj: Thing, value: Value) -> None: + def set_without_emit(self, obj: Owner, value: Value) -> None: """Set the property's value, but do not emit event to notify the server. This function is not expected to be used externally. It is called during diff --git a/src/labthings_fastapi/thing_slots.py b/src/labthings_fastapi/thing_slots.py index 5d953fa..5c5ea74 100644 --- a/src/labthings_fastapi/thing_slots.py +++ b/src/labthings_fastapi/thing_slots.py @@ -59,7 +59,9 @@ def say_hello(self) -> str: ) -class ThingSlot(Generic[ConnectedThings], FieldTypedBaseDescriptor[ConnectedThings]): +class ThingSlot( + Generic[ConnectedThings], FieldTypedBaseDescriptor["Thing", ConnectedThings] +): r"""Descriptor that instructs the server to supply other Things. A `.ThingSlot` provides either one or several diff --git a/tests/test_base_descriptor.py b/tests/test_base_descriptor.py index 4a11b6e..ed69985 100644 --- a/tests/test_base_descriptor.py +++ b/tests/test_base_descriptor.py @@ -9,9 +9,10 @@ ) from .utilities import raises_or_is_caused_by from labthings_fastapi.exceptions import MissingTypeError, InconsistentTypeError +import labthings_fastapi as lt -class MockProperty(BaseDescriptor[str]): +class MockProperty(BaseDescriptor[lt.Thing, str]): """A mock property class.""" # The line below isn't defined on a `Thing`, so mypy @@ -299,10 +300,10 @@ class FieldTypedExample: """An example with field-typed descriptors.""" int_or_str_prop: int | str = FieldTypedBaseDescriptor() - int_or_str_subscript = FieldTypedBaseDescriptor[int | str]() + int_or_str_subscript = FieldTypedBaseDescriptor[lt.Thing, int | str]() int_or_str_stringified: "int | str" = FieldTypedBaseDescriptor() customprop: CustomType = FieldTypedBaseDescriptor() - customprop_subscript = FieldTypedBaseDescriptor[CustomType]() + customprop_subscript = FieldTypedBaseDescriptor[lt.Thing, CustomType]() futureprop: "FutureType" = FieldTypedBaseDescriptor() @@ -408,7 +409,7 @@ class Example3: with raises_or_is_caused_by(MissingTypeError) as excinfo: class Example4: - field6 = FieldTypedBaseDescriptor["str"]() + field6 = FieldTypedBaseDescriptor[lt.Thing, "str"]() msg = str(excinfo.value) assert "forward reference" in msg @@ -421,7 +422,7 @@ def test_mismatched_types(): with raises_or_is_caused_by(InconsistentTypeError): class Example3: - field: int = FieldTypedBaseDescriptor[str]() + field: int = FieldTypedBaseDescriptor[lt.Thing, str]() def test_double_specified_types(): @@ -432,7 +433,7 @@ def test_double_specified_types(): """ class Example4: - field: int | None = FieldTypedBaseDescriptor[int | None]() + field: int | None = FieldTypedBaseDescriptor[lt.Thing, int | None]() assert Example4.field.value_type == int | None @@ -448,4 +449,4 @@ def test_stringified_vs_unstringified_mismatch(): with raises_or_is_caused_by(InconsistentTypeError): class Example5: - field: "int" = FieldTypedBaseDescriptor[int]() + field: "int" = FieldTypedBaseDescriptor[lt.Thing, int]() diff --git a/typing_tests/thing_properties.py b/typing_tests/thing_properties.py index ee9a421..0a5962a 100644 --- a/typing_tests/thing_properties.py +++ b/typing_tests/thing_properties.py @@ -66,7 +66,7 @@ def strprop(self: typing.Any) -> str: return "foo" -assert_type(strprop, FunctionalProperty[str]) +assert_type(strprop, FunctionalProperty[typing.Any, str]) class TestPropertyDefaultsMatch(lt.Thing): @@ -138,34 +138,38 @@ class TestExplicitDescriptor(lt.Thing): the underlying class as well. """ - intprop1 = lt.DataProperty[int](default=0) + intprop1 = lt.DataProperty["TestExplicitDescriptor", int](default=0) """A DataProperty that should not cause mypy errors.""" - intprop2 = lt.DataProperty[int](default_factory=int_factory) + intprop2 = lt.DataProperty["TestExplicitDescriptor", int]( + default_factory=int_factory + ) """The factory matches the type hint, so this should be OK.""" - intprop3 = lt.DataProperty[int](default_factory=optional_int_factory) - """Uses a factory function that doesn't match the type hint. - - This ought to cause mypy to throw an error, as the factory function can - return None, but at time of writing this doesn't happen. - - This error is caught correctly when called via `lt.property`. - """ + intprop3 = lt.DataProperty["TestExplicitDescriptor", int]( + default_factory=optional_int_factory, # type: ignore[arg-type] + ) + """Uses a factory function that doesn't match the type hint.""" - intprop4 = lt.DataProperty[int](default="foo") # type: ignore[call-overload] + intprop4 = lt.DataProperty["TestExplicitDescriptor", int](default="foo") # type: ignore[call-overload] """This property should cause an error, as the default is a string.""" - intprop5 = lt.DataProperty[int]() # type: ignore[call-overload] + intprop5 = lt.DataProperty["TestExplicitDescriptor", int]() # type: ignore[call-overload] """This property should cause mypy to throw an error, as it has no default.""" - optionalintprop1 = lt.DataProperty[int | None](default=None) + optionalintprop1 = lt.DataProperty["TestExplicitDescriptor", int | None]( + default=None + ) """A DataProperty that should not cause mypy errors.""" - optionalintprop2 = lt.DataProperty[int | None](default_factory=optional_int_factory) + optionalintprop2 = lt.DataProperty["TestExplicitDescriptor", int | None]( + default_factory=optional_int_factory + ) """This property should not cause mypy errors: the factory matches the type hint.""" - optionalintprop3 = lt.DataProperty[int | None](default_factory=int_factory) + optionalintprop3 = lt.DataProperty["TestExplicitDescriptor", int | None]( + default_factory=int_factory + ) """Uses a factory function that is a subset of the type hint.""" @@ -181,19 +185,21 @@ class TestExplicitDescriptor(lt.Thing): assert_type(test_explicit_descriptor.optionalintprop3, int | None) # Check class attributes are typed correctly. -assert_type(TestExplicitDescriptor.intprop1, lt.DataProperty[int]) -assert_type(TestExplicitDescriptor.intprop2, lt.DataProperty[int]) -assert_type(TestExplicitDescriptor.intprop3, lt.DataProperty[int]) +cls: typing.TypeAlias = TestExplicitDescriptor +assert_type(cls.intprop1, lt.DataProperty[cls, int]) +assert_type(cls.intprop2, lt.DataProperty[cls, int]) +assert_type(cls.intprop3, lt.DataProperty[cls, int]) -assert_type(TestExplicitDescriptor.optionalintprop1, lt.DataProperty[int | None]) -assert_type(TestExplicitDescriptor.optionalintprop2, lt.DataProperty[int | None]) -assert_type(TestExplicitDescriptor.optionalintprop3, lt.DataProperty[int | None]) +assert_type(cls.optionalintprop1, lt.DataProperty[cls, int | None]) +assert_type(cls.optionalintprop2, lt.DataProperty[cls, int | None]) +assert_type(cls.optionalintprop3, lt.DataProperty[cls, int | None]) +Owner = typing.TypeVar("Owner", bound=lt.Thing) Val = typing.TypeVar("Val") -def f_property(getter: typing.Callable[..., Val]) -> FunctionalProperty[Val]: +def f_property(getter: typing.Callable[[Owner], Val]) -> FunctionalProperty[Owner, Val]: """A function that returns a FunctionalProperty with a getter.""" return FunctionalProperty(getter) @@ -231,7 +237,7 @@ def intprop3(self) -> int: """This getter is fine, but the setter should fail type checking.""" return 0 - @intprop3.setter + @intprop3.setter # type: ignore[arg-type] def _set_intprop3(self, value: str) -> None: """Setter for intprop3. It's got the wrong type so should fail.""" pass @@ -267,15 +273,16 @@ def strprop(self, val: str) -> None: pass -assert_type(TestFunctionalProperty.intprop1, FunctionalProperty[int]) -assert_type(TestFunctionalProperty.intprop2, FunctionalProperty[int]) -assert_type(TestFunctionalProperty.intprop3, FunctionalProperty[int]) -assert_type(TestFunctionalProperty.fprop, FunctionalProperty[int]) +cls1: typing.TypeAlias = TestFunctionalProperty +assert_type(cls1.intprop1, FunctionalProperty[cls1, int]) +assert_type(cls1.intprop2, FunctionalProperty[cls1, int]) +assert_type(cls1.intprop3, FunctionalProperty[cls1, int]) +assert_type(cls1.fprop, FunctionalProperty[cls1, int]) # Don't check ``strprop`` because it caused an error and thus will # not have the right type, even though the error is ignored. -test_functional_property = create_thing_without_server(TestFunctionalProperty) -assert_type(test_functional_property, TestFunctionalProperty) +test_functional_property = create_thing_without_server(cls1) +assert_type(test_functional_property, cls1) assert_type(test_functional_property.intprop1, int) assert_type(test_functional_property.intprop2, int) assert_type(test_functional_property.intprop3, int)