From 8d0e8bf27ad4db63b5934b934329bdc2b9916b63 Mon Sep 17 00:00:00 2001 From: ju1ius Date: Tue, 14 Nov 2023 09:14:37 +0100 Subject: [PATCH] adds type hints to public APIs Closes #110 --- observ/dict_proxy.py | 2 +- observ/list_proxy.py | 2 +- observ/proxy.py | 35 ++++++++++++++++++++++------------- observ/set_proxy.py | 2 +- observ/store.py | 9 ++++++--- observ/watcher.py | 23 ++++++++++++----------- tests/test_collections.py | 4 ++++ 7 files changed, 47 insertions(+), 30 deletions(-) diff --git a/observ/dict_proxy.py b/observ/dict_proxy.py index 2e37659..a0bf60b 100644 --- a/observ/dict_proxy.py +++ b/observ/dict_proxy.py @@ -51,7 +51,7 @@ } -class DictProxyBase(Proxy): +class DictProxyBase(Proxy[dict]): def _orphaned_keydeps(self): return set(proxy_db.attrs(self)["keydep"].keys()) - set(self.target.keys()) diff --git a/observ/list_proxy.py b/observ/list_proxy.py index e377ede..7e7c8ca 100644 --- a/observ/list_proxy.py +++ b/observ/list_proxy.py @@ -45,7 +45,7 @@ } -class ListProxyBase(Proxy): +class ListProxyBase(Proxy[list]): pass diff --git a/observ/proxy.py b/observ/proxy.py index 899ddc2..8ec891a 100644 --- a/observ/proxy.py +++ b/observ/proxy.py @@ -1,9 +1,12 @@ from functools import partial +from typing import cast, Generic, TypedDict, TypeVar from .proxy_db import proxy_db +T = TypeVar("T") -class Proxy: + +class Proxy(Generic[T]): """ Proxy for an object/target. @@ -16,9 +19,9 @@ class Proxy: """ __hash__ = None - __slots__ = ["target", "readonly", "shallow", "__weakref__"] + __slots__ = ("target", "readonly", "shallow", "__weakref__") - def __init__(self, target, readonly=False, shallow=False): + def __init__(self, target: T, readonly=False, shallow=False): self.target = target self.readonly = readonly self.shallow = shallow @@ -33,7 +36,7 @@ def __del__(self): TYPE_LOOKUP = {} -def proxy(target, readonly=False, shallow=False): +def proxy(target: T, readonly=False, shallow=False) -> T: """ Returns a Proxy for the given object. If a proxy for the given configuration already exists, it will return that instead of @@ -67,14 +70,20 @@ def proxy(target, readonly=False, shallow=False): return proxy_type(target, readonly=readonly, shallow=shallow) if isinstance(target, tuple): - return tuple(proxy(x, readonly=readonly, shallow=shallow) for x in target) + return cast( + T, tuple(proxy(x, readonly=readonly, shallow=shallow) for x in target) + ) # We can't proxy a plain value - return target + return cast(T, target) + + +class Ref(TypedDict, Generic[T]): + value: T -def ref(target): - return proxy({"value": target}) +def ref(target: T) -> Ref[T]: + return proxy(Ref(value=target)) reactive = proxy @@ -83,7 +92,7 @@ def ref(target): shallow_readonly = partial(proxy, shallow=True, readonly=True) -def to_raw(target): +def to_raw(target: Proxy[T] | T) -> T: """ Returns a raw object from which any trace of proxy has been replaced with its wrapped target value. @@ -92,15 +101,15 @@ def to_raw(target): return to_raw(target.target) if isinstance(target, list): - return [to_raw(t) for t in target] + return cast(T, [to_raw(t) for t in target]) if isinstance(target, dict): - return {key: to_raw(value) for key, value in target.items()} + return cast(T, {key: to_raw(value) for key, value in target.items()}) if isinstance(target, tuple): - return tuple(to_raw(t) for t in target) + return cast(T, tuple(to_raw(t) for t in target)) if isinstance(target, set): - return {to_raw(t) for t in target} + return cast(T, {to_raw(t) for t in target}) return target diff --git a/observ/set_proxy.py b/observ/set_proxy.py index d99d633..e049d8b 100644 --- a/observ/set_proxy.py +++ b/observ/set_proxy.py @@ -54,7 +54,7 @@ } -class SetProxyBase(Proxy): +class SetProxyBase(Proxy[set]): pass diff --git a/observ/store.py b/observ/store.py index a983b67..a493423 100644 --- a/observ/store.py +++ b/observ/store.py @@ -1,5 +1,5 @@ from functools import partial, wraps -from typing import Callable, Collection, TypeVar +from typing import Callable, Generic, TypeVar import patchdiff @@ -51,12 +51,15 @@ def decorator_computed(fn: T) -> T: return decorator_computed(_fn) -class Store: +S = TypeVar("S") + + +class Store(Generic[S]): """ Store that tracks mutations to state in order to enable undo/redo functionality """ - def __init__(self, state: Collection, strict=True): + def __init__(self, state: S, strict=True): """ Creates a store with the given state as the initial state. When `strict` is False, calling mutations that do not result diff --git a/observ/watcher.py b/observ/watcher.py index 096e713..e2c6db1 100644 --- a/observ/watcher.py +++ b/observ/watcher.py @@ -9,27 +9,28 @@ from functools import partial, wraps import inspect from itertools import count -from typing import Any, Callable, Optional, TypeVar +from typing import Any, Callable, Generic, TypeVar from weakref import ref, WeakSet from .dep import Dep from .dict_proxy import DictProxyBase from .list_proxy import ListProxyBase -from .proxy import Proxy from .scheduler import scheduler from .set_proxy import SetProxyBase -T = TypeVar("T", bound=Callable[[], Any]) +T = TypeVar("T") +Watchable = Callable[[], T] | T +WatchCallback = Callable[[], Any] | Callable[[T], Any] | Callable[[T, T], Any] def watch( - fn: Callable[[], Any] | Proxy | list[Proxy], - callback: Optional[Callable] = None, + fn: Watchable[T], + callback: WatchCallback[T] | None = None, sync: bool = False, deep: bool | None = None, immediate: bool = False, -): +) -> Watcher[T]: watcher = Watcher(fn, sync=sync, lazy=False, deep=deep, callback=callback) if immediate: watcher.dirty = True @@ -42,8 +43,8 @@ def watch( watch_effect = partial(watch, immediate=False, deep=True, callback=None) -def computed(_fn=None, *, deep=True): - def decorator_computed(fn: T) -> T: +def computed(_fn: Callable[[], T] | None = None, *, deep=True) -> Callable[[], T]: + def decorator_computed(fn: Callable[[], T]) -> Callable[[], T]: """ Create a watcher for an expression. Note: make sure fn doesn't need any arguments to run @@ -113,7 +114,7 @@ class WrongNumberOfArgumentsError(TypeError): pass -class Watcher: +class Watcher(Generic[T]): __slots__ = [ "id", "fn", @@ -132,11 +133,11 @@ class Watcher: def __init__( self, - fn: Callable[[], Any] | Proxy | list[Proxy], + fn: Watchable[T], sync: bool = False, lazy: bool = True, deep: bool | None = None, - callback: Callable = None, + callback: WatchCallback[T] | None = None, ) -> None: """ sync: Ignore the scheduler diff --git a/tests/test_collections.py b/tests/test_collections.py index d380460..dac6d62 100644 --- a/tests/test_collections.py +++ b/tests/test_collections.py @@ -55,6 +55,10 @@ "readonly", "shallow", "__weakref__", + # exclude attributes added by typing.Generic + "_is_protocol", + "__orig_bases__", + "__parameters__", }