From d0cca62fa69b9b199ce98a56edc7736cccbdeef2 Mon Sep 17 00:00:00 2001 From: Korijn van Golen Date: Tue, 2 Jan 2024 16:12:29 +0100 Subject: [PATCH 01/16] initial boiler plate for object proxy --- observ/dict_proxy.py | 7 ++- observ/list_proxy.py | 6 ++- observ/object_proxy.py | 96 ++++++++++++++++++++++++++++++++++++++ observ/proxy.py | 4 +- observ/set_proxy.py | 7 ++- observ/watcher.py | 3 ++ tests/test_object_proxy.py | 16 +++++++ 7 files changed, 134 insertions(+), 5 deletions(-) create mode 100644 observ/object_proxy.py create mode 100644 tests/test_object_proxy.py diff --git a/observ/dict_proxy.py b/observ/dict_proxy.py index 8b8b774..97772b7 100644 --- a/observ/dict_proxy.py +++ b/observ/dict_proxy.py @@ -75,4 +75,9 @@ def readonly_dict_proxy_init(self, target, shallow=False, **kwargs): }, ) -TYPE_LOOKUP[dict] = (DictProxy, ReadonlyDictProxy) + +def type_test(target): + return isinstance(target, dict) + + +TYPE_LOOKUP[type_test] = (DictProxy, ReadonlyDictProxy) diff --git a/observ/list_proxy.py b/observ/list_proxy.py index a7c2386..005095f 100644 --- a/observ/list_proxy.py +++ b/observ/list_proxy.py @@ -69,4 +69,8 @@ def readonly_list_proxy_init(self, target, shallow=False, **kwargs): ) -TYPE_LOOKUP[list] = (ListProxy, ReadonlyListProxy) +def type_test(target): + return isinstance(target, list) + + +TYPE_LOOKUP[type_test] = (ListProxy, ReadonlyListProxy) diff --git a/observ/object_proxy.py b/observ/object_proxy.py new file mode 100644 index 0000000..3828067 --- /dev/null +++ b/observ/object_proxy.py @@ -0,0 +1,96 @@ +from .proxy import TYPE_LOOKUP, Proxy +from .proxy_db import proxy_db +from .traps import construct_methods_traps_dict, trap_map, trap_map_readonly + +###################### +# UNTRAPPED ATTRIBUTES +###################### + +# CREATION HOOKS: +# '__init__', +# '__init_subclass__', +# '__new__', + +# PICKLE HOOKS: +# '__reduce__', +# '__reduce_ex__', +# '__getstate__', + +# WEAKREF STORAGE: +# '__weakref__', + +# FORMATTING HOOKS: +# '__format__', + +# INTERNALS: +# '__dict__', + +########### +# QUESTIONS +########### +# I don't think we can maintain per-key reactivity +# like we do with dicts. For starters we can't +# query the initial state since there is no .keys() + +# There are many _optional_ magic methods, such as __iter__ +# How do we support those without overcomplicating the traps? + +object_traps = { + "READERS": { + '__class__', + '__dir__', + '__doc__', + '__eq__', + '__ge__', + '__gt__', + '__hash__', + '__le__', + '__lt__', + '__module__', + '__ne__', + '__repr__', + '__sizeof__', + '__str__', + '__subclasshook__', + # also fires for method access :/ + '__getattribute__', + }, + "WRITERS": { + '__setattr__', + "__delattr__", + }, +} + + +class ObjectProxyBase(Proxy[object]): + def _orphaned_keydeps(self): + return set(proxy_db.attrs(self)["keydep"].keys()) - set(vars(self.target).keys()) + + +def readonly_object_proxy_init(self, target, shallow=False, **kwargs): + super(ReadonlyObjectProxy, self).__init__( + target, shallow=shallow, **{**kwargs, "readonly": True} + ) + + +ObjectProxy = type( + "ObjectProxy", + (ObjectProxyBase,), + construct_methods_traps_dict(object, object_traps, trap_map), +) +ReadonlyObjectProxy = type( + "ReadonlyObjectProxy", + (ObjectProxyBase,), + { + "__init__": readonly_object_proxy_init, + **construct_methods_traps_dict(object, object_traps, trap_map_readonly), + }, +) + + +def type_test(target): + # exclude builtin objects + return isinstance(target, object) and target.__module__ != object.__module__ + + +TYPE_LOOKUP[type_test] = (ObjectProxy, ReadonlyObjectProxy) diff --git a/observ/proxy.py b/observ/proxy.py index 2119ae3..c67d15d 100644 --- a/observ/proxy.py +++ b/observ/proxy.py @@ -66,8 +66,8 @@ def proxy(target: T, readonly=False, shallow=False) -> T: return existing_proxy # Create a new proxy - for target_type, (writable_proxy_type, readonly_proxy_type) in TYPE_LOOKUP.items(): - if isinstance(target, target_type): + for type_test, (writable_proxy_type, readonly_proxy_type) in TYPE_LOOKUP.items(): + if type_test(target): proxy_type = readonly_proxy_type if readonly else writable_proxy_type return proxy_type(target, readonly=readonly, shallow=shallow) diff --git a/observ/set_proxy.py b/observ/set_proxy.py index 5a46ed8..1ec6c71 100644 --- a/observ/set_proxy.py +++ b/observ/set_proxy.py @@ -75,4 +75,9 @@ def readonly_set_proxy_init(self, target, shallow=False, **kwargs): }, ) -TYPE_LOOKUP[set] = (SetProxy, ReadonlySetProxy) + +def type_test(target): + return isinstance(target, set) + + +TYPE_LOOKUP[type_test] = (SetProxy, ReadonlySetProxy) diff --git a/observ/watcher.py b/observ/watcher.py index 01c4887..b450be8 100644 --- a/observ/watcher.py +++ b/observ/watcher.py @@ -16,6 +16,7 @@ from .dep import Dep from .dict_proxy import DictProxyBase from .list_proxy import ListProxyBase +from .object_proxy import ObjectProxyBase from .scheduler import scheduler from .set_proxy import SetProxyBase @@ -83,6 +84,8 @@ def traverse(obj, seen=None): val_iter = iter(obj.values()) elif isinstance(obj, (list, ListProxyBase, set, SetProxyBase, tuple)): val_iter = iter(obj) + elif isinstance(obj, (ObjectProxyBase)): + val_iter = iter(vars(obj).values()) else: return # track which objects we have already seen to support(!) full traversal diff --git a/tests/test_object_proxy.py b/tests/test_object_proxy.py new file mode 100644 index 0000000..69fe9ad --- /dev/null +++ b/tests/test_object_proxy.py @@ -0,0 +1,16 @@ +from dataclasses import dataclass + +from observ.object_proxy import ObjectProxy +from observ.proxy import proxy +from observ.traps import ReadonlyError + + +@dataclass +class Foo: + bar: int + + +def test_dataclass_proxy(): + data = Foo(bar=5) + proxied = proxy(data) + assert isinstance(proxied, ObjectProxy) From 0befb3f9cb884abe8d750a377a6126be8ca80c3a Mon Sep 17 00:00:00 2001 From: Korijn van Golen Date: Tue, 2 Jan 2024 16:16:14 +0100 Subject: [PATCH 02/16] improve traverse --- observ/object_proxy.py | 2 ++ observ/watcher.py | 8 ++++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/observ/object_proxy.py b/observ/object_proxy.py index 3828067..bbb31b8 100644 --- a/observ/object_proxy.py +++ b/observ/object_proxy.py @@ -35,6 +35,8 @@ # There are many _optional_ magic methods, such as __iter__ # How do we support those without overcomplicating the traps? + + object_traps = { "READERS": { '__class__', diff --git a/observ/watcher.py b/observ/watcher.py index b450be8..770ed40 100644 --- a/observ/watcher.py +++ b/observ/watcher.py @@ -84,10 +84,14 @@ def traverse(obj, seen=None): val_iter = iter(obj.values()) elif isinstance(obj, (list, ListProxyBase, set, SetProxyBase, tuple)): val_iter = iter(obj) - elif isinstance(obj, (ObjectProxyBase)): - val_iter = iter(vars(obj).values()) + elif isinstance(obj, (object, ObjectProxyBase)): + try: + val_iter = iter(vars(obj).values()) + except TypeError: + return else: return + # track which objects we have already seen to support(!) full traversal # of datastructures with cycles # NOTE: a set would provide faster containment checks From 7a916b184350c3d67499b0990c2358f5053b4952 Mon Sep 17 00:00:00 2001 From: Korijn van Golen Date: Thu, 4 Jan 2024 16:22:29 +0100 Subject: [PATCH 03/16] eh --- observ/object_proxy.py | 141 ++++++++++++++----------------------- observ/proxy_db.py | 2 + observ/watcher.py | 9 +-- tests/test_object_proxy.py | 21 +++--- 4 files changed, 72 insertions(+), 101 deletions(-) diff --git a/observ/object_proxy.py b/observ/object_proxy.py index bbb31b8..43cc251 100644 --- a/observ/object_proxy.py +++ b/observ/object_proxy.py @@ -1,98 +1,65 @@ -from .proxy import TYPE_LOOKUP, Proxy -from .proxy_db import proxy_db -from .traps import construct_methods_traps_dict, trap_map, trap_map_readonly - -###################### -# UNTRAPPED ATTRIBUTES -###################### - -# CREATION HOOKS: -# '__init__', -# '__init_subclass__', -# '__new__', - -# PICKLE HOOKS: -# '__reduce__', -# '__reduce_ex__', -# '__getstate__', - -# WEAKREF STORAGE: -# '__weakref__', - -# FORMATTING HOOKS: -# '__format__', - -# INTERNALS: -# '__dict__', - -########### -# QUESTIONS -########### -# I don't think we can maintain per-key reactivity -# like we do with dicts. For starters we can't -# query the initial state since there is no .keys() - -# There are many _optional_ magic methods, such as __iter__ -# How do we support those without overcomplicating the traps? - - - -object_traps = { - "READERS": { - '__class__', - '__dir__', - '__doc__', - '__eq__', - '__ge__', - '__gt__', - '__hash__', - '__le__', - '__lt__', - '__module__', - '__ne__', - '__repr__', - '__sizeof__', - '__str__', - '__subclasshook__', - # also fires for method access :/ - '__getattribute__', - }, - "WRITERS": { - '__setattr__', - "__delattr__", - }, -} - - -class ObjectProxyBase(Proxy[object]): - def _orphaned_keydeps(self): - return set(proxy_db.attrs(self)["keydep"].keys()) - set(vars(self.target).keys()) +from operator import xor +from .dep import Dep +from .proxy import proxy +from .proxy_db import proxy_db -def readonly_object_proxy_init(self, target, shallow=False, **kwargs): - super(ReadonlyObjectProxy, self).__init__( - target, shallow=shallow, **{**kwargs, "readonly": True} - ) +from .proxy import TYPE_LOOKUP, Proxy +from .proxy_db import proxy_db -ObjectProxy = type( - "ObjectProxy", - (ObjectProxyBase,), - construct_methods_traps_dict(object, object_traps, trap_map), -) -ReadonlyObjectProxy = type( - "ReadonlyObjectProxy", - (ObjectProxyBase,), - { - "__init__": readonly_object_proxy_init, - **construct_methods_traps_dict(object, object_traps, trap_map_readonly), - }, -) +class ObjectProxy(Proxy): + def __getattribute__(self, name): + if name in Proxy.__slots__: + return super().__getattribute__(name) + + if Dep.stack: + keydeps = proxy_db.attrs(self)["keydep"] + if name not in keydeps: + keydeps[name] = Dep() + keydeps[name].depend() + value = getattr(self.target, name) + if self.shallow: + return value + return proxy(value, readonly=self.readonly) + + def __setattr__(self, name, value): + if name in Proxy.__slots__: + return super().__setattr__(name, value) + + attrs = proxy_db.attrs(self) + is_new = name not in attrs["keydep"] + old_value = getattr(self.target, name, None) # if not is_new else None + retval = setattr(self.target, name, value) + new_value = getattr(self.target, name, None) + if is_new: + attrs["keydep"][name] = Dep() + if xor(old_value is None, new_value is None) or old_value != new_value: + attrs["keydep"][name].notify() + attrs["dep"].notify() + return retval + + def __delattr__(self, name): + if name in Proxy.__slots__: + return super().__delattr__(name) + retval = delattr(self.target, name) + attrs = proxy_db.attrs(self) + attrs["dep"].notify() + attrs["keydep"][name].notify() + del attrs["keydep"][name] + return retval + + +class ReadonlyObjectProxy(ObjectProxy): + def __init__(self, target, shallow=False, **kwargs): + super().__init__( + target, shallow=shallow, **{**kwargs, "readonly": True} + ) def type_test(target): # exclude builtin objects - return isinstance(target, object) and target.__module__ != object.__module__ + return isinstance(target, object) and type(target).__module__ != object.__module__ TYPE_LOOKUP[type_test] = (ObjectProxy, ReadonlyObjectProxy) diff --git a/observ/proxy_db.py b/observ/proxy_db.py index 9475371..8c7fe55 100644 --- a/observ/proxy_db.py +++ b/observ/proxy_db.py @@ -54,6 +54,8 @@ def reference(self, proxy): } if isinstance(proxy.target, dict): attrs["keydep"] = {key: Dep() for key in proxy.target.keys()} + else: + attrs["keydep"] = {} self.db[obj_id] = { "target": proxy.target, "attrs": attrs, # dep, keydep diff --git a/observ/watcher.py b/observ/watcher.py index 770ed40..c6ff951 100644 --- a/observ/watcher.py +++ b/observ/watcher.py @@ -7,6 +7,7 @@ import asyncio import inspect +from itertools import chain from collections.abc import Awaitable, Container from functools import partial, wraps from itertools import count @@ -16,7 +17,7 @@ from .dep import Dep from .dict_proxy import DictProxyBase from .list_proxy import ListProxyBase -from .object_proxy import ObjectProxyBase +from .object_proxy import ObjectProxy from .scheduler import scheduler from .set_proxy import SetProxyBase @@ -84,11 +85,11 @@ def traverse(obj, seen=None): val_iter = iter(obj.values()) elif isinstance(obj, (list, ListProxyBase, set, SetProxyBase, tuple)): val_iter = iter(obj) - elif isinstance(obj, (object, ObjectProxyBase)): + elif isinstance(obj, (object, ObjectProxy)): try: val_iter = iter(vars(obj).values()) except TypeError: - return + val_iter = (getattr(obj, slot) for slot in chain.from_iterable(getattr(cls, '__slots__', []) for cls in type(obj).__mro__)) else: return @@ -104,7 +105,7 @@ def traverse(obj, seen=None): for v in val_iter: if ( isinstance( - v, (dict, DictProxyBase, list, ListProxyBase, set, SetProxyBase, tuple) + v, (dict, DictProxyBase, list, ListProxyBase, set, SetProxyBase, tuple, object, ObjectProxy) ) ) and v not in seen: traverse(v, seen=seen) diff --git a/tests/test_object_proxy.py b/tests/test_object_proxy.py index 69fe9ad..c23b742 100644 --- a/tests/test_object_proxy.py +++ b/tests/test_object_proxy.py @@ -1,16 +1,17 @@ -from dataclasses import dataclass - from observ.object_proxy import ObjectProxy -from observ.proxy import proxy -from observ.traps import ReadonlyError -@dataclass class Foo: - bar: int + __slots__ = ("bar",) + + def __init__(self, bar=5): + self.bar = bar + +def test_object_proxying(): + obj = Foo(bar=5) + proxy = ObjectProxy(obj) + assert proxy.bar == 5 + proxy.bar = 6 + assert proxy.bar == 6 -def test_dataclass_proxy(): - data = Foo(bar=5) - proxied = proxy(data) - assert isinstance(proxied, ObjectProxy) From 52fd892a2c8723367dc2c73c1c55cd2b0c16874e Mon Sep 17 00:00:00 2001 From: Korijn van Golen Date: Fri, 5 Jan 2024 14:10:19 +0100 Subject: [PATCH 04/16] add object proxying --- observ/dict_proxy.py | 2 +- observ/object_proxy.py | 79 ++++++++++++++++++++++++++++---------- observ/object_utils.py | 24 ++++++++++++ observ/proxy.py | 18 +++++---- observ/proxy_db.py | 23 ++++++----- observ/traps.py | 41 ++++++++++---------- observ/watcher.py | 16 ++------ tests/test_collections.py | 6 +-- tests/test_object_proxy.py | 66 +++++++++++++++++++++++++++++-- tests/test_proxy.py | 18 ++++----- tests/test_usage.py | 9 ++--- 11 files changed, 209 insertions(+), 93 deletions(-) create mode 100644 observ/object_utils.py diff --git a/observ/dict_proxy.py b/observ/dict_proxy.py index 97772b7..a7361fe 100644 --- a/observ/dict_proxy.py +++ b/observ/dict_proxy.py @@ -52,7 +52,7 @@ class DictProxyBase(Proxy[dict]): def _orphaned_keydeps(self): - return set(proxy_db.attrs(self)["keydep"].keys()) - set(self.target.keys()) + return set(proxy_db.attrs(self)["keydep"].keys()) - set(self.__target__.keys()) def readonly_dict_proxy_init(self, target, shallow=False, **kwargs): diff --git a/observ/object_proxy.py b/observ/object_proxy.py index 43cc251..4acaee4 100644 --- a/observ/object_proxy.py +++ b/observ/object_proxy.py @@ -1,11 +1,10 @@ from operator import xor from .dep import Dep -from .proxy import proxy -from .proxy_db import proxy_db - -from .proxy import TYPE_LOOKUP, Proxy +from .object_utils import get_object_attrs +from .proxy import TYPE_LOOKUP, Proxy, proxy from .proxy_db import proxy_db +from .traps import ReadonlyError class ObjectProxy(Proxy): @@ -13,26 +12,50 @@ def __getattribute__(self, name): if name in Proxy.__slots__: return super().__getattribute__(name) + target = self.__target__ + target_attrs = get_object_attrs(target) + if name not in target_attrs: + return getattr(target, name) + if Dep.stack: keydeps = proxy_db.attrs(self)["keydep"] if name not in keydeps: keydeps[name] = Dep() keydeps[name].depend() - value = getattr(self.target, name) - if self.shallow: + value = getattr(target, name) + if self.__shallow__: return value - return proxy(value, readonly=self.readonly) + return proxy(value, readonly=self.__readonly__) def __setattr__(self, name, value): if name in Proxy.__slots__: return super().__setattr__(name, value) + if self.__readonly__: + raise ReadonlyError() + + target = self.__target__ + target_attrs = get_object_attrs(target) attrs = proxy_db.attrs(self) - is_new = name not in attrs["keydep"] - old_value = getattr(self.target, name, None) # if not is_new else None - retval = setattr(self.target, name, value) - new_value = getattr(self.target, name, None) - if is_new: + is_new_keydep = name not in attrs["keydep"] + is_new_target_attr = name not in target_attrs + + old_value = getattr(target, name, None) if not is_new_target_attr else None + retval = setattr(target, name, value) + + if is_new_keydep: + # we have not determined earlier that this attr + # should be reactive, so let's determine it now + target_attrs_new = get_object_attrs(target) + if name not in target_attrs_new: + # the set attr is not stateful (e.g. someone + # is attaching a bound method) + # so no need to track this modification + # TODO: we could cache this maybe? + return retval + + new_value = getattr(target, name, None) + if is_new_keydep: attrs["keydep"][name] = Dep() if xor(old_value is None, new_value is None) or old_value != new_value: attrs["keydep"][name].notify() @@ -42,24 +65,38 @@ def __setattr__(self, name, value): def __delattr__(self, name): if name in Proxy.__slots__: return super().__delattr__(name) - retval = delattr(self.target, name) + + if self.__readonly__: + raise ReadonlyError() + + target = self.__target__ + target_attrs = get_object_attrs(target) attrs = proxy_db.attrs(self) - attrs["dep"].notify() - attrs["keydep"][name].notify() - del attrs["keydep"][name] + is_keydep = name in attrs["keydep"] + is_target_attr = name in target_attrs + + retval = delattr(target, name) + + if is_target_attr: + attrs["dep"].notify() + if is_keydep: + attrs["keydep"][name].notify() + del attrs["keydep"][name] + return retval class ReadonlyObjectProxy(ObjectProxy): def __init__(self, target, shallow=False, **kwargs): - super().__init__( - target, shallow=shallow, **{**kwargs, "readonly": True} - ) + super().__init__(target, shallow=shallow, **{**kwargs, "readonly": True}) def type_test(target): - # exclude builtin objects - return isinstance(target, object) and type(target).__module__ != object.__module__ + # exclude builtin objects and objects for which we have better proxies available + return ( + not isinstance(target, (list, set, dict, tuple)) + and type(target).__module__ != object.__module__ + ) TYPE_LOOKUP[type_test] = (ObjectProxy, ReadonlyObjectProxy) diff --git a/observ/object_utils.py b/observ/object_utils.py new file mode 100644 index 0000000..2f3255b --- /dev/null +++ b/observ/object_utils.py @@ -0,0 +1,24 @@ +from functools import cache +from itertools import chain + + +@cache +def get_class_slots(cls): + """utility to collect all __slots__ entries for a given type and its supertypes""" + # collect via iterables for performance + # deduplicate via set + return set( + chain.from_iterable(getattr(cls, "__slots__", []) for cls in cls.__mro__) + ) + + +def get_object_attrs(obj): + """utility to collect all stateful attributes of an object""" + # __slots__ from full class ancestry + attrs = list(get_class_slots(type(obj))) + try: + # all __dict__ entries + attrs.extend(vars(obj).keys()) + except TypeError: + pass + return attrs diff --git a/observ/proxy.py b/observ/proxy.py index c67d15d..55bf3e6 100644 --- a/observ/proxy.py +++ b/observ/proxy.py @@ -21,12 +21,14 @@ class Proxy(Generic[T]): """ __hash__ = None - __slots__ = ("target", "readonly", "shallow", "__weakref__") + # the slots have to be very unique since we also proxy objects + # which may define the attributes with the same names + __slots__ = ("__target__", "__readonly__", "__shallow__", "__weakref__") def __init__(self, target: T, readonly=False, shallow=False): - self.target = target - self.readonly = readonly - self.shallow = shallow + self.__target__ = target + self.__readonly__ = readonly + self.__shallow__ = shallow proxy_db.reference(self) def __del__(self): @@ -50,14 +52,14 @@ def proxy(target: T, readonly=False, shallow=False) -> T: # The object may be a proxy already, so check if it matches the # given configuration (readonly and shallow) if isinstance(target, Proxy): - if readonly == target.readonly and shallow == target.shallow: + if readonly == target.__readonly__ and shallow == target.__shallow__: return target else: # If the configuration does not match, # unwrap the target from the proxy so that the right # kind of proxy can be returned in the next part of # this function - target = target.target + target = target.__target__ # Note that at this point, target is always a non-proxy object # Check the proxy_db to see if there's already a proxy for the target object @@ -106,7 +108,7 @@ def to_raw(target: Proxy[T] | T) -> T: with its wrapped target value. """ if isinstance(target, Proxy): - return to_raw(target.target) + return to_raw(target.__target__) if isinstance(target, list): return cast(T, [to_raw(t) for t in target]) @@ -120,4 +122,6 @@ def to_raw(target: Proxy[T] | T) -> T: if isinstance(target, set): return cast(T, {to_raw(t) for t in target}) + # TODO: I don't think we can implement to_raw for objects + return target diff --git a/observ/proxy_db.py b/observ/proxy_db.py index 8c7fe55..8fcae2c 100644 --- a/observ/proxy_db.py +++ b/observ/proxy_db.py @@ -3,6 +3,7 @@ from weakref import WeakValueDictionary from .dep import Dep +from .object_utils import get_object_attrs class ProxyDb: @@ -46,18 +47,19 @@ def reference(self, proxy): """ Adds a reference to the collection for the wrapped object's id """ - obj_id = id(proxy.target) + target = proxy.__target__ + obj_id = id(target) if obj_id not in self.db: attrs = { "dep": Dep(), } - if isinstance(proxy.target, dict): - attrs["keydep"] = {key: Dep() for key in proxy.target.keys()} - else: - attrs["keydep"] = {} + if isinstance(target, dict): + attrs["keydep"] = {key: Dep() for key in target.keys()} + elif not isinstance(target, (list, set)): + attrs["keydep"] = {key: Dep() for key in get_object_attrs(target)} self.db[obj_id] = { - "target": proxy.target, + "target": target, "attrs": attrs, # dep, keydep # keyed on tuple(readonly, shallow) "proxies": WeakValueDictionary(), @@ -70,7 +72,7 @@ def reference(self, proxy): # Seems to be a tiny bit faster than checking beforehand if # there is already an existing value in the proxies dict result = self.db[obj_id]["proxies"].setdefault( - (proxy.readonly, proxy.shallow), proxy + (proxy.__readonly__, proxy.__shallow__), proxy ) if result is not proxy: raise RuntimeError("Proxy with existing configuration already in db") @@ -79,7 +81,7 @@ def dereference(self, proxy): """ Removes a reference from the database for the given proxy """ - obj_id = id(proxy.target) + obj_id = id(proxy.__target__) if obj_id not in self.db: # When there are failing tests, it might happen that proxies # are garbage collected at a point where the proxy_db is already @@ -93,13 +95,14 @@ def dereference(self, proxy): # for the target object if len(self.db[obj_id]["proxies"]) == 1: ref_count = sys.getrefcount(self.db[obj_id]["target"]) - # Ref count is still 3 here because of the reference through proxy.target + # Ref count is still 3 here because of the reference + # through proxy.__target__ if ref_count <= 3: # We are the last to hold a reference! del self.db[obj_id] def attrs(self, proxy): - return self.db[id(proxy.target)]["attrs"] + return self.db[id(proxy.__target__)]["attrs"] def get_proxy(self, target, readonly=False, shallow=False): """ diff --git a/observ/traps.py b/observ/traps.py index 315a6c9..db6357e 100644 --- a/observ/traps.py +++ b/observ/traps.py @@ -21,10 +21,10 @@ def read_trap(method, obj_cls): def trap(self, *args, **kwargs): if Dep.stack: proxy_db.attrs(self)["dep"].depend() - value = fn(self.target, *args, **kwargs) - if self.shallow: + value = fn(self.__target__, *args, **kwargs) + if self.__shallow__: return value - return proxy(value, readonly=self.readonly) + return proxy(value, readonly=self.__readonly__) return trap @@ -36,15 +36,16 @@ def iterate_trap(method, obj_cls): def trap(self, *args, **kwargs): if Dep.stack: proxy_db.attrs(self)["dep"].depend() - iterator = fn(self.target, *args, **kwargs) - if self.shallow: + iterator = fn(self.__target__, *args, **kwargs) + if self.__shallow__: return iterator if method == "items": return ( - (key, proxy(value, readonly=self.readonly)) for key, value in iterator + (key, proxy(value, readonly=self.__readonly__)) + for key, value in iterator ) else: - proxied = partial(proxy, readonly=self.readonly) + proxied = partial(proxy, readonly=self.__readonly__) return map(proxied, iterator) return trap @@ -61,10 +62,10 @@ def trap(self, *args, **kwargs): if key not in keydeps: keydeps[key] = Dep() keydeps[key].depend() - value = fn(self.target, *args, **kwargs) - if self.shallow: + value = fn(self.__target__, *args, **kwargs) + if self.__shallow__: return value - return proxy(value, readonly=self.readonly) + return proxy(value, readonly=self.__readonly__) return trap @@ -74,13 +75,13 @@ def write_trap(method, obj_cls): @wraps(fn) def trap(self, *args, **kwargs): - old = self.target.copy() - retval = fn(self.target, *args, **kwargs) + old = self.__target__.copy() + retval = fn(self.__target__, *args, **kwargs) attrs = proxy_db.attrs(self) if obj_cls == dict: change_detected = False keydeps = attrs["keydep"] - for key, val in self.target.items(): + for key, val in self.__target__.items(): if old.get(key) is not val: if key in keydeps: keydeps[key].notify() @@ -90,7 +91,7 @@ def trap(self, *args, **kwargs): if change_detected: attrs["dep"].notify() else: # list and set - if self.target != old: + if self.__target__ != old: attrs["dep"].notify() return retval @@ -107,13 +108,13 @@ def trap(self, *args, **kwargs): key = args[0] attrs = proxy_db.attrs(self) is_new = key not in attrs["keydep"] - old_value = getitem_fn(self.target, key) if not is_new else None - retval = fn(self.target, *args, **kwargs) - if method == "setdefault" and not self.shallow: + old_value = getitem_fn(self.__target__, key) if not is_new else None + retval = fn(self.__target__, *args, **kwargs) + if method == "setdefault" and not self.__shallow__: # This method is only available when readonly is false retval = proxy(retval) - new_value = getitem_fn(self.target, key) + new_value = getitem_fn(self.__target__, key) if is_new: attrs["keydep"][key] = Dep() if xor(old_value is None, new_value is None) or old_value != new_value: @@ -129,7 +130,7 @@ def delete_trap(method, obj_cls): @wraps(fn) def trap(self, *args, **kwargs): - retval = fn(self.target, *args, **kwargs) + retval = fn(self.__target__, *args, **kwargs) attrs = proxy_db.attrs(self) attrs["dep"].notify() for key in self._orphaned_keydeps(): @@ -145,7 +146,7 @@ def delete_key_trap(method, obj_cls): @wraps(fn) def trap(self, *args, **kwargs): - retval = fn(self.target, *args, **kwargs) + retval = fn(self.__target__, *args, **kwargs) key = args[0] attrs = proxy_db.attrs(self) attrs["dep"].notify() diff --git a/observ/watcher.py b/observ/watcher.py index c6ff951..c721037 100644 --- a/observ/watcher.py +++ b/observ/watcher.py @@ -7,7 +7,6 @@ import asyncio import inspect -from itertools import chain from collections.abc import Awaitable, Container from functools import partial, wraps from itertools import count @@ -17,7 +16,7 @@ from .dep import Dep from .dict_proxy import DictProxyBase from .list_proxy import ListProxyBase -from .object_proxy import ObjectProxy +from .object_utils import get_object_attrs from .scheduler import scheduler from .set_proxy import SetProxyBase @@ -85,13 +84,8 @@ def traverse(obj, seen=None): val_iter = iter(obj.values()) elif isinstance(obj, (list, ListProxyBase, set, SetProxyBase, tuple)): val_iter = iter(obj) - elif isinstance(obj, (object, ObjectProxy)): - try: - val_iter = iter(vars(obj).values()) - except TypeError: - val_iter = (getattr(obj, slot) for slot in chain.from_iterable(getattr(cls, '__slots__', []) for cls in type(obj).__mro__)) else: - return + val_iter = (getattr(obj, attr) for attr in get_object_attrs(obj)) # track which objects we have already seen to support(!) full traversal # of datastructures with cycles @@ -103,11 +97,7 @@ def traverse(obj, seen=None): seen = [] seen.append(obj) for v in val_iter: - if ( - isinstance( - v, (dict, DictProxyBase, list, ListProxyBase, set, SetProxyBase, tuple, object, ObjectProxy) - ) - ) and v not in seen: + if v not in seen: traverse(v, seen=seen) diff --git a/tests/test_collections.py b/tests/test_collections.py index f0321d6..c4c0184 100644 --- a/tests/test_collections.py +++ b/tests/test_collections.py @@ -50,9 +50,9 @@ "_orphaned_keydeps", # Following attributes are part of Proxy.__slots__ "__slots__", - "target", - "readonly", - "shallow", + "__target__", + "__readonly__", + "__shallow__", "__weakref__", # exclude attributes added by typing.Generic "_is_protocol", diff --git a/tests/test_object_proxy.py b/tests/test_object_proxy.py index c23b742..f71c28b 100644 --- a/tests/test_object_proxy.py +++ b/tests/test_object_proxy.py @@ -1,17 +1,77 @@ -from observ.object_proxy import ObjectProxy +import pytest +from observ.object_proxy import ObjectProxy, ReadonlyObjectProxy +from observ.object_utils import get_class_slots, get_object_attrs +from observ.proxy import proxy +from observ.traps import ReadonlyError -class Foo: + +class A: __slots__ = ("bar",) def __init__(self, bar=5): self.bar = bar +class B(A): + def __init__(self, baz=10, **kwargs): + super().__init__(**kwargs) + self.baz = baz + + +class C(B): + __slots__ = ("quux",) + + def __init__(self, quux=15, **kwargs): + super().__init__(**kwargs) + self.quux = quux + + +class D: + def __init__(self, bar=5): + self.bar = bar + + +def test_get_class_slots(): + assert get_class_slots(C) == {'bar', 'quux'} + assert get_class_slots(D) == set() + + +def test_get_object_attrs(): + assert set(get_object_attrs(A())) == {'bar'} + assert set(get_object_attrs(B())) == {'baz', 'bar'} + assert set(get_object_attrs(C())) == {'baz', 'bar', 'quux'} + assert set(get_object_attrs(D())) == {'bar'} + + d = D() + d.quuz = 20 + assert set(get_object_attrs(d)) == {'bar', 'quuz'} + + def test_object_proxying(): - obj = Foo(bar=5) + obj = B(bar=5) proxy = ObjectProxy(obj) assert proxy.bar == 5 proxy.bar = 6 assert proxy.bar == 6 + proxy.something_else = "toet" + assert proxy.something_else == "toet" + assert obj.something_else == "toet" + + +def test_readonly_proxy(): + proxied = proxy(C(bar=3)) + readonly_proxy = proxy(proxied, readonly=True) + another_proxied = proxy(readonly_proxy, readonly=False) + + assert isinstance(proxied, ObjectProxy) + assert isinstance(readonly_proxy, ReadonlyObjectProxy) + assert proxied is another_proxied + + with pytest.raises(ReadonlyError): + readonly_proxy.bar = 25 + assert proxied.bar == 3 + + another_proxied.bar = 25 + assert proxied.bar == 25 diff --git a/tests/test_proxy.py b/tests/test_proxy.py index 49fe792..8bad772 100644 --- a/tests/test_proxy.py +++ b/tests/test_proxy.py @@ -19,13 +19,13 @@ def test_proxy_lifecycle(): assert isinstance(wrapped_data, Proxy) assert obj_id in proxy_db.db - assert wrapped_data.target is data + assert wrapped_data.__target__ is data # Retrieve the proxy from the proxy_db referenced_proxy = proxy_db.get_proxy(data) assert referenced_proxy is wrapped_data - assert referenced_proxy.target is wrapped_data.target + assert referenced_proxy.__target__ is wrapped_data.__target__ # Destroy the second proxy del referenced_proxy @@ -49,23 +49,23 @@ def test_proxy_lifecycle(): def test_proxy_lifecycle_auto(): # Call proxy to wrap the data object wrapped_data = proxy({"foo": "bar"}) - obj_id = id(wrapped_data.target) + obj_id = id(wrapped_data.__target__) assert isinstance(wrapped_data, Proxy) assert obj_id in proxy_db.db - # assert wrapped_data.target is data + # assert wrapped_data.__target__ is data # Retrieve the proxy from the proxy_db - referenced_proxy = proxy_db.get_proxy(wrapped_data.target) + referenced_proxy = proxy_db.get_proxy(wrapped_data.__target__) assert referenced_proxy is wrapped_data - assert referenced_proxy.target is wrapped_data.target + assert referenced_proxy.__target__ is wrapped_data.__target__ # Destroy the second proxy del referenced_proxy assert obj_id in proxy_db.db - assert proxy_db.get_proxy(wrapped_data.target) is not None + assert proxy_db.get_proxy(wrapped_data.__target__) is not None # Destroy the original proxy del wrapped_data @@ -193,10 +193,10 @@ def test_embedded_tuple(): def test_shallow_proxy(): shallow_proxy = proxy({"foo": "bar", "baz": {"lorem": "ipsum"}}, shallow=True) assert isinstance(shallow_proxy, DictProxy) - assert shallow_proxy.shallow + assert shallow_proxy.__shallow__ deep_proxy = proxy(shallow_proxy) - assert not deep_proxy.shallow + assert not deep_proxy.__shallow__ def test_non_hashable(): diff --git a/tests/test_usage.py b/tests/test_usage.py index 8c0e65b..8e6cff4 100644 --- a/tests/test_usage.py +++ b/tests/test_usage.py @@ -541,7 +541,7 @@ def test_watch_real_deep(): watcher.callback.assert_called_once() assert isinstance(a["scene"]["objects"]["camera"]["position"], ListProxy) - assert not a["scene"]["objects"]["camera"]["position"].shallow + assert not a["scene"]["objects"]["camera"]["position"].__shallow__ watcher.callback = Mock() a["scene"]["objects"]["camera"]["position"][1] = 2 @@ -581,7 +581,7 @@ def obj_contains_proxy(obj): # this assertion confirms that the problematic case has been created # in other words, that the test was setup properly - assert obj_contains_proxy(a.target) + assert obj_contains_proxy(a.__target__) # check if we can still get a reference to the raw object using to_raw raw_pos = to_raw(a["scene"]["objects"]["mesh"]["position"]) @@ -593,9 +593,6 @@ def obj_contains_proxy(obj): def test_usage_class_instances(): - """This test documents that class instances are not reactive, - but they can still be part of reactive state if you want.""" - class Foo: def __init__(self): self.foo = 5 @@ -622,7 +619,7 @@ def _callback(): # write to a class attribute a[2].foo = 10 - assert called == 2 # class instances are NOT reactive + assert called == 3 def test_watch_get_non_existing(): From 24859641affe04cc358adba47ea8ba447350263b Mon Sep 17 00:00:00 2001 From: Korijn van Golen Date: Fri, 5 Jan 2024 14:43:07 +0100 Subject: [PATCH 05/16] support magic methods --- observ/object_proxy.py | 152 ++++++++++++++++++++++++++++++++++++- tests/test_object_proxy.py | 8 ++ tests/test_usage.py | 33 ++++++++ 3 files changed, 192 insertions(+), 1 deletion(-) diff --git a/observ/object_proxy.py b/observ/object_proxy.py index 4acaee4..0d240b7 100644 --- a/observ/object_proxy.py +++ b/observ/object_proxy.py @@ -7,7 +7,7 @@ from .traps import ReadonlyError -class ObjectProxy(Proxy): +class ObjectProxyBase(Proxy): def __getattribute__(self, name): if name in Proxy.__slots__: return super().__getattribute__(name) @@ -86,6 +86,156 @@ def __delattr__(self, name): return retval +def passthrough(method): + def trap(self, *args, **kwargs): + fn = getattr(self.__target__, method, None) + if fn is None: + raise TypeError(f"object of type '{type(self)}' has no {method}") + return fn(*args, **kwargs) + + return trap + + +magic_methods = [ + "__abs__", + "__add__", + "__aenter__", + "__aexit__", + "__aiter__", + "__and__", + "__anext__", + "__annotations__", + "__await__", + "__bases__", + "__bool__", + "__buffer__", + "__bytes__", + "__call__", + "__ceil__", + "__class__", + "__class_getitem__", + # '__classcell__', + "__closure__", + "__code__", + "__complex__", + "__contains__", + "__defaults__", + # '__del__', + # '__delattr__', + "__delete__", + "__delitem__", + "__dict__", + "__dir__", + "__divmod__", + "__doc__", + "__enter__", + "__eq__", + "__exit__", + "__file__", + "__float__", + "__floor__", + "__floordiv__", + "__format__", + "__func__", + "__future__", + "__ge__", + "__get__", + # "__getattr__", + # '__getattribute__', + "__getitem__", + "__globals__", + "__gt__", + "__hash__", + "__iadd__", + "__iand__", + "__ifloordiv__", + "__ilshift__", + "__imatmul__", + "__imod__", + "__import__", + "__imul__", + "__index__", + # '__init__', + "__init_subclass__", + "__instancecheck__", + "__int__", + "__invert__", + "__ior__", + "__ipow__", + "__irshift__", + "__isub__", + "__iter__", + "__itruediv__", + "__ixor__", + "__kwdefaults__", + "__le__", + "__len__", + "__length_hint__", + "__lshift__", + "__lt__", + "__match_args__", + "__matmul__", + "__missing__", + "__mod__", + "__module__", + "__mro__", + "__mro_entries__", + "__mul__", + "__name__", + "__ne__", + "__neg__", + # '__new__', + "__next__", + "__objclass__", + "__or__", + "__pos__", + "__pow__", + "__prepare__", + # '__qualname__', + "__radd__", + "__rand__", + "__rdivmod__", + "__release_buffer__", + "__repr__", + "__reversed__", + "__rfloordiv__", + "__rlshift__", + "__rmatmul__", + "__rmod__", + "__rmul__", + "__ror__", + "__round__", + "__rpow__", + "__rrshift__", + "__rshift__", + "__rsub__", + "__rtruediv__", + "__rxor__", + "__self__", + "__set__", + "__set_name__", + # '__setattr__', + "__setitem__", + # '__slots__', + "__str__", + "__sub__", + "__subclasscheck__", + "__traceback__", + "__truediv__", + "__trunc__", + "__type_params__", + "__weakref__", + "__xor__", +] + + +ObjectProxy = type( + "ObjectProxy", + (ObjectProxyBase,), + {method: passthrough(method) for method in magic_methods}, +) + + class ReadonlyObjectProxy(ObjectProxy): def __init__(self, target, shallow=False, **kwargs): super().__init__(target, shallow=shallow, **{**kwargs, "readonly": True}) diff --git a/tests/test_object_proxy.py b/tests/test_object_proxy.py index f71c28b..c67724a 100644 --- a/tests/test_object_proxy.py +++ b/tests/test_object_proxy.py @@ -75,3 +75,11 @@ def test_readonly_proxy(): another_proxied.bar = 25 assert proxied.bar == 25 + + with pytest.raises(ReadonlyError): + delattr(readonly_proxy, "baz") + delattr(another_proxied, "baz") + + assert not hasattr(proxied, "baz") + + diff --git a/tests/test_usage.py b/tests/test_usage.py index 8e6cff4..14aae14 100644 --- a/tests/test_usage.py +++ b/tests/test_usage.py @@ -1,3 +1,4 @@ +from dataclasses import dataclass from typing import Iterable from unittest.mock import Mock @@ -596,6 +597,9 @@ def test_usage_class_instances(): class Foo: def __init__(self): self.foo = 5 + + def __len__(self): + return self.foo a = reactive([1, 2, Foo()]) called = 0 @@ -621,6 +625,35 @@ def _callback(): a[2].foo = 10 assert called == 3 + # magic methods are supported + foo_len = computed(lambda: len(a[2])) + assert foo_len() == 10 + + +def test_usage_dataclass(): + @dataclass + class Foo: + bar: int + + a = reactive(Foo(bar=5)) + called = 0 + + def _callback(): + nonlocal called + called += 1 + + watcher = watch(lambda: a, _callback, sync=True, deep=True) + assert not watcher.dirty + assert called == 0 + + # write something + a.bar = 10 + assert called == 1 + + # magic methods are supported + str_foo = computed(lambda: repr(a)) + assert str_foo() == "test_usage_dataclass..Foo(bar=10)" + def test_watch_get_non_existing(): a = reactive({}) From 8cc6d193b190d6e3e1739925821e02efebf0f7d7 Mon Sep 17 00:00:00 2001 From: Korijn van Golen Date: Fri, 5 Jan 2024 14:50:37 +0100 Subject: [PATCH 06/16] more early exits in traverse --- observ/watcher.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/observ/watcher.py b/observ/watcher.py index c721037..df09961 100644 --- a/observ/watcher.py +++ b/observ/watcher.py @@ -81,11 +81,17 @@ def traverse(obj, seen=None): # we are only interested in traversing a fixed set of types # otherwise we can just exit if isinstance(obj, (dict, DictProxyBase)): + if not obj: + return val_iter = iter(obj.values()) elif isinstance(obj, (list, ListProxyBase, set, SetProxyBase, tuple)): + if not obj: + return val_iter = iter(obj) else: - val_iter = (getattr(obj, attr) for attr in get_object_attrs(obj)) + val_iter = [getattr(obj, attr) for attr in get_object_attrs(obj)] + if not val_iter: + return # track which objects we have already seen to support(!) full traversal # of datastructures with cycles From 2e4b1f1b89524f056c2cce3d2c51309754e3e1e7 Mon Sep 17 00:00:00 2001 From: Korijn van Golen Date: Fri, 5 Jan 2024 14:51:51 +0100 Subject: [PATCH 07/16] add some TODOs --- observ/object_proxy.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/observ/object_proxy.py b/observ/object_proxy.py index 0d240b7..7004c7d 100644 --- a/observ/object_proxy.py +++ b/observ/object_proxy.py @@ -51,7 +51,7 @@ def __setattr__(self, name, value): # the set attr is not stateful (e.g. someone # is attaching a bound method) # so no need to track this modification - # TODO: we could cache this maybe? + # TODO: we could cache this return retval new_value = getattr(target, name, None) @@ -90,6 +90,8 @@ def passthrough(method): def trap(self, *args, **kwargs): fn = getattr(self.__target__, method, None) if fn is None: + # TODO: we could cache this + # but it is possibly a class is dynamically modified later raise TypeError(f"object of type '{type(self)}' has no {method}") return fn(*args, **kwargs) From 7637104f6e266e3fb62792ce46ebf3df8169ef4e Mon Sep 17 00:00:00 2001 From: Korijn van Golen Date: Fri, 5 Jan 2024 14:52:34 +0100 Subject: [PATCH 08/16] ruff --- tests/test_object_proxy.py | 14 ++++++-------- tests/test_usage.py | 2 +- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/tests/test_object_proxy.py b/tests/test_object_proxy.py index c67724a..0f851f6 100644 --- a/tests/test_object_proxy.py +++ b/tests/test_object_proxy.py @@ -33,19 +33,19 @@ def __init__(self, bar=5): def test_get_class_slots(): - assert get_class_slots(C) == {'bar', 'quux'} + assert get_class_slots(C) == {"bar", "quux"} assert get_class_slots(D) == set() def test_get_object_attrs(): - assert set(get_object_attrs(A())) == {'bar'} - assert set(get_object_attrs(B())) == {'baz', 'bar'} - assert set(get_object_attrs(C())) == {'baz', 'bar', 'quux'} - assert set(get_object_attrs(D())) == {'bar'} + assert set(get_object_attrs(A())) == {"bar"} + assert set(get_object_attrs(B())) == {"baz", "bar"} + assert set(get_object_attrs(C())) == {"baz", "bar", "quux"} + assert set(get_object_attrs(D())) == {"bar"} d = D() d.quuz = 20 - assert set(get_object_attrs(d)) == {'bar', 'quuz'} + assert set(get_object_attrs(d)) == {"bar", "quuz"} def test_object_proxying(): @@ -81,5 +81,3 @@ def test_readonly_proxy(): delattr(another_proxied, "baz") assert not hasattr(proxied, "baz") - - diff --git a/tests/test_usage.py b/tests/test_usage.py index 14aae14..2ed8a65 100644 --- a/tests/test_usage.py +++ b/tests/test_usage.py @@ -597,7 +597,7 @@ def test_usage_class_instances(): class Foo: def __init__(self): self.foo = 5 - + def __len__(self): return self.foo From 067ff5f4c230ba56679de9f796ae5e567efb4e63 Mon Sep 17 00:00:00 2001 From: Korijn van Golen Date: Mon, 8 Jan 2024 10:32:40 +0100 Subject: [PATCH 09/16] address berend's comments --- observ/object_proxy.py | 15 +++++++-------- observ/object_utils.py | 7 +++++-- observ/proxy_db.py | 9 +++++---- 3 files changed, 17 insertions(+), 14 deletions(-) diff --git a/observ/object_proxy.py b/observ/object_proxy.py index 7004c7d..dcad734 100644 --- a/observ/object_proxy.py +++ b/observ/object_proxy.py @@ -51,7 +51,7 @@ def __setattr__(self, name, value): # the set attr is not stateful (e.g. someone # is attaching a bound method) # so no need to track this modification - # TODO: we could cache this + # TODO: we could cache this? return retval new_value = getattr(target, name, None) @@ -59,7 +59,6 @@ def __setattr__(self, name, value): attrs["keydep"][name] = Dep() if xor(old_value is None, new_value is None) or old_value != new_value: attrs["keydep"][name].notify() - attrs["dep"].notify() return retval def __delattr__(self, name): @@ -77,11 +76,9 @@ def __delattr__(self, name): retval = delattr(target, name) - if is_target_attr: - attrs["dep"].notify() - if is_keydep: - attrs["keydep"][name].notify() - del attrs["keydep"][name] + if is_target_attr and is_keydep: + attrs["keydep"][name].notify() + del attrs["keydep"][name] return retval @@ -91,13 +88,15 @@ def trap(self, *args, **kwargs): fn = getattr(self.__target__, method, None) if fn is None: # TODO: we could cache this - # but it is possibly a class is dynamically modified later + # but it is possible a class is dynamically modified later + # invalidating the cached result... raise TypeError(f"object of type '{type(self)}' has no {method}") return fn(*args, **kwargs) return trap +# TODO: how to verify this is the correct and complete set of magic methods? magic_methods = [ "__abs__", "__add__", diff --git a/observ/object_utils.py b/observ/object_utils.py index 2f3255b..26cd959 100644 --- a/observ/object_utils.py +++ b/observ/object_utils.py @@ -15,10 +15,13 @@ def get_class_slots(cls): def get_object_attrs(obj): """utility to collect all stateful attributes of an object""" # __slots__ from full class ancestry - attrs = list(get_class_slots(type(obj))) + attrs = get_class_slots(type(obj)) try: # all __dict__ entries - attrs.extend(vars(obj).keys()) + obj_keys = vars(obj).keys() + if obj_keys: + attrs = attrs.copy() + attrs.update(obj_keys) except TypeError: pass return attrs diff --git a/observ/proxy_db.py b/observ/proxy_db.py index 8fcae2c..9f16bae 100644 --- a/observ/proxy_db.py +++ b/observ/proxy_db.py @@ -51,12 +51,13 @@ def reference(self, proxy): obj_id = id(target) if obj_id not in self.db: - attrs = { - "dep": Dep(), - } + attrs = {} if isinstance(target, dict): + attrs["dep"] = Dep() attrs["keydep"] = {key: Dep() for key in target.keys()} - elif not isinstance(target, (list, set)): + elif isinstance(target, (list, set)): + attrs["dep"] = Dep() + else: attrs["keydep"] = {key: Dep() for key in get_object_attrs(target)} self.db[obj_id] = { "target": target, From d86a051bfc558bfba0b43a784b3ba52a7074181e Mon Sep 17 00:00:00 2001 From: Korijn van Golen Date: Mon, 8 Jan 2024 10:33:15 +0100 Subject: [PATCH 10/16] relock --- poetry.lock | 430 +++++++++++++++++++++++++--------------------------- 1 file changed, 209 insertions(+), 221 deletions(-) diff --git a/poetry.lock b/poetry.lock index 583087f..aaad77d 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,17 +1,5 @@ # This file is automatically @generated by Poetry 1.4.2 and should not be changed by hand. -[[package]] -name = "appdirs" -version = "1.4.4" -description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." -category = "dev" -optional = false -python-versions = "*" -files = [ - {file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"}, - {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"}, -] - [[package]] name = "certifi" version = "2023.11.17" @@ -215,64 +203,64 @@ files = [ [[package]] name = "coverage" -version = "7.3.2" +version = "7.4.0" description = "Code coverage measurement for Python" category = "dev" optional = false python-versions = ">=3.8" files = [ - {file = "coverage-7.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d872145f3a3231a5f20fd48500274d7df222e291d90baa2026cc5152b7ce86bf"}, - {file = "coverage-7.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:310b3bb9c91ea66d59c53fa4989f57d2436e08f18fb2f421a1b0b6b8cc7fffda"}, - {file = "coverage-7.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f47d39359e2c3779c5331fc740cf4bce6d9d680a7b4b4ead97056a0ae07cb49a"}, - {file = "coverage-7.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aa72dbaf2c2068404b9870d93436e6d23addd8bbe9295f49cbca83f6e278179c"}, - {file = "coverage-7.3.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:beaa5c1b4777f03fc63dfd2a6bd820f73f036bfb10e925fce067b00a340d0f3f"}, - {file = "coverage-7.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:dbc1b46b92186cc8074fee9d9fbb97a9dd06c6cbbef391c2f59d80eabdf0faa6"}, - {file = "coverage-7.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:315a989e861031334d7bee1f9113c8770472db2ac484e5b8c3173428360a9148"}, - {file = "coverage-7.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d1bc430677773397f64a5c88cb522ea43175ff16f8bfcc89d467d974cb2274f9"}, - {file = "coverage-7.3.2-cp310-cp310-win32.whl", hash = "sha256:a889ae02f43aa45032afe364c8ae84ad3c54828c2faa44f3bfcafecb5c96b02f"}, - {file = "coverage-7.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:c0ba320de3fb8c6ec16e0be17ee1d3d69adcda99406c43c0409cb5c41788a611"}, - {file = "coverage-7.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ac8c802fa29843a72d32ec56d0ca792ad15a302b28ca6203389afe21f8fa062c"}, - {file = "coverage-7.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:89a937174104339e3a3ffcf9f446c00e3a806c28b1841c63edb2b369310fd074"}, - {file = "coverage-7.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e267e9e2b574a176ddb983399dec325a80dbe161f1a32715c780b5d14b5f583a"}, - {file = "coverage-7.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2443cbda35df0d35dcfb9bf8f3c02c57c1d6111169e3c85fc1fcc05e0c9f39a3"}, - {file = "coverage-7.3.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4175e10cc8dda0265653e8714b3174430b07c1dca8957f4966cbd6c2b1b8065a"}, - {file = "coverage-7.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0cbf38419fb1a347aaf63481c00f0bdc86889d9fbf3f25109cf96c26b403fda1"}, - {file = "coverage-7.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:5c913b556a116b8d5f6ef834038ba983834d887d82187c8f73dec21049abd65c"}, - {file = "coverage-7.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1981f785239e4e39e6444c63a98da3a1db8e971cb9ceb50a945ba6296b43f312"}, - {file = "coverage-7.3.2-cp311-cp311-win32.whl", hash = "sha256:43668cabd5ca8258f5954f27a3aaf78757e6acf13c17604d89648ecc0cc66640"}, - {file = "coverage-7.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10c39c0452bf6e694511c901426d6b5ac005acc0f78ff265dbe36bf81f808a2"}, - {file = "coverage-7.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:4cbae1051ab791debecc4a5dcc4a1ff45fc27b91b9aee165c8a27514dd160836"}, - {file = "coverage-7.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:12d15ab5833a997716d76f2ac1e4b4d536814fc213c85ca72756c19e5a6b3d63"}, - {file = "coverage-7.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c7bba973ebee5e56fe9251300c00f1579652587a9f4a5ed8404b15a0471f216"}, - {file = "coverage-7.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fe494faa90ce6381770746077243231e0b83ff3f17069d748f645617cefe19d4"}, - {file = "coverage-7.3.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6e9589bd04d0461a417562649522575d8752904d35c12907d8c9dfeba588faf"}, - {file = "coverage-7.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d51ac2a26f71da1b57f2dc81d0e108b6ab177e7d30e774db90675467c847bbdf"}, - {file = "coverage-7.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:99b89d9f76070237975b315b3d5f4d6956ae354a4c92ac2388a5695516e47c84"}, - {file = "coverage-7.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fa28e909776dc69efb6ed975a63691bc8172b64ff357e663a1bb06ff3c9b589a"}, - {file = "coverage-7.3.2-cp312-cp312-win32.whl", hash = "sha256:289fe43bf45a575e3ab10b26d7b6f2ddb9ee2dba447499f5401cfb5ecb8196bb"}, - {file = "coverage-7.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:7dbc3ed60e8659bc59b6b304b43ff9c3ed858da2839c78b804973f613d3e92ed"}, - {file = "coverage-7.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f94b734214ea6a36fe16e96a70d941af80ff3bfd716c141300d95ebc85339738"}, - {file = "coverage-7.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:af3d828d2c1cbae52d34bdbb22fcd94d1ce715d95f1a012354a75e5913f1bda2"}, - {file = "coverage-7.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:630b13e3036e13c7adc480ca42fa7afc2a5d938081d28e20903cf7fd687872e2"}, - {file = "coverage-7.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c9eacf273e885b02a0273bb3a2170f30e2d53a6d53b72dbe02d6701b5296101c"}, - {file = "coverage-7.3.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d8f17966e861ff97305e0801134e69db33b143bbfb36436efb9cfff6ec7b2fd9"}, - {file = "coverage-7.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b4275802d16882cf9c8b3d057a0839acb07ee9379fa2749eca54efbce1535b82"}, - {file = "coverage-7.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:72c0cfa5250f483181e677ebc97133ea1ab3eb68645e494775deb6a7f6f83901"}, - {file = "coverage-7.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:cb536f0dcd14149425996821a168f6e269d7dcd2c273a8bff8201e79f5104e76"}, - {file = "coverage-7.3.2-cp38-cp38-win32.whl", hash = "sha256:307adb8bd3abe389a471e649038a71b4eb13bfd6b7dd9a129fa856f5c695cf92"}, - {file = "coverage-7.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:88ed2c30a49ea81ea3b7f172e0269c182a44c236eb394718f976239892c0a27a"}, - {file = "coverage-7.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b631c92dfe601adf8f5ebc7fc13ced6bb6e9609b19d9a8cd59fa47c4186ad1ce"}, - {file = "coverage-7.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d3d9df4051c4a7d13036524b66ecf7a7537d14c18a384043f30a303b146164e9"}, - {file = "coverage-7.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5f7363d3b6a1119ef05015959ca24a9afc0ea8a02c687fe7e2d557705375c01f"}, - {file = "coverage-7.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2f11cc3c967a09d3695d2a6f03fb3e6236622b93be7a4b5dc09166a861be6d25"}, - {file = "coverage-7.3.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:149de1d2401ae4655c436a3dced6dd153f4c3309f599c3d4bd97ab172eaf02d9"}, - {file = "coverage-7.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:3a4006916aa6fee7cd38db3bfc95aa9c54ebb4ffbfc47c677c8bba949ceba0a6"}, - {file = "coverage-7.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9028a3871280110d6e1aa2df1afd5ef003bab5fb1ef421d6dc748ae1c8ef2ebc"}, - {file = "coverage-7.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9f805d62aec8eb92bab5b61c0f07329275b6f41c97d80e847b03eb894f38d083"}, - {file = "coverage-7.3.2-cp39-cp39-win32.whl", hash = "sha256:d1c88ec1a7ff4ebca0219f5b1ef863451d828cccf889c173e1253aa84b1e07ce"}, - {file = "coverage-7.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b4767da59464bb593c07afceaddea61b154136300881844768037fd5e859353f"}, - {file = "coverage-7.3.2-pp38.pp39.pp310-none-any.whl", hash = "sha256:ae97af89f0fbf373400970c0a21eef5aa941ffeed90aee43650b81f7d7f47637"}, - {file = "coverage-7.3.2.tar.gz", hash = "sha256:be32ad29341b0170e795ca590e1c07e81fc061cb5b10c74ce7203491484404ef"}, + {file = "coverage-7.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:36b0ea8ab20d6a7564e89cb6135920bc9188fb5f1f7152e94e8300b7b189441a"}, + {file = "coverage-7.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0676cd0ba581e514b7f726495ea75aba3eb20899d824636c6f59b0ed2f88c471"}, + {file = "coverage-7.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0ca5c71a5a1765a0f8f88022c52b6b8be740e512980362f7fdbb03725a0d6b9"}, + {file = "coverage-7.4.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a7c97726520f784239f6c62506bc70e48d01ae71e9da128259d61ca5e9788516"}, + {file = "coverage-7.4.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:815ac2d0f3398a14286dc2cea223a6f338109f9ecf39a71160cd1628786bc6f5"}, + {file = "coverage-7.4.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:80b5ee39b7f0131ebec7968baa9b2309eddb35b8403d1869e08f024efd883566"}, + {file = "coverage-7.4.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:5b2ccb7548a0b65974860a78c9ffe1173cfb5877460e5a229238d985565574ae"}, + {file = "coverage-7.4.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:995ea5c48c4ebfd898eacb098164b3cc826ba273b3049e4a889658548e321b43"}, + {file = "coverage-7.4.0-cp310-cp310-win32.whl", hash = "sha256:79287fd95585ed36e83182794a57a46aeae0b64ca53929d1176db56aacc83451"}, + {file = "coverage-7.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:5b14b4f8760006bfdb6e08667af7bc2d8d9bfdb648351915315ea17645347137"}, + {file = "coverage-7.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:04387a4a6ecb330c1878907ce0dc04078ea72a869263e53c72a1ba5bbdf380ca"}, + {file = "coverage-7.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ea81d8f9691bb53f4fb4db603203029643caffc82bf998ab5b59ca05560f4c06"}, + {file = "coverage-7.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:74775198b702868ec2d058cb92720a3c5a9177296f75bd97317c787daf711505"}, + {file = "coverage-7.4.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:76f03940f9973bfaee8cfba70ac991825611b9aac047e5c80d499a44079ec0bc"}, + {file = "coverage-7.4.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:485e9f897cf4856a65a57c7f6ea3dc0d4e6c076c87311d4bc003f82cfe199d25"}, + {file = "coverage-7.4.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:6ae8c9d301207e6856865867d762a4b6fd379c714fcc0607a84b92ee63feff70"}, + {file = "coverage-7.4.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:bf477c355274a72435ceb140dc42de0dc1e1e0bf6e97195be30487d8eaaf1a09"}, + {file = "coverage-7.4.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:83c2dda2666fe32332f8e87481eed056c8b4d163fe18ecc690b02802d36a4d26"}, + {file = "coverage-7.4.0-cp311-cp311-win32.whl", hash = "sha256:697d1317e5290a313ef0d369650cfee1a114abb6021fa239ca12b4849ebbd614"}, + {file = "coverage-7.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:26776ff6c711d9d835557ee453082025d871e30b3fd6c27fcef14733f67f0590"}, + {file = "coverage-7.4.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:13eaf476ec3e883fe3e5fe3707caeb88268a06284484a3daf8250259ef1ba143"}, + {file = "coverage-7.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846f52f46e212affb5bcf131c952fb4075b55aae6b61adc9856222df89cbe3e2"}, + {file = "coverage-7.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26f66da8695719ccf90e794ed567a1549bb2644a706b41e9f6eae6816b398c4a"}, + {file = "coverage-7.4.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:164fdcc3246c69a6526a59b744b62e303039a81e42cfbbdc171c91a8cc2f9446"}, + {file = "coverage-7.4.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:316543f71025a6565677d84bc4df2114e9b6a615aa39fb165d697dba06a54af9"}, + {file = "coverage-7.4.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:bb1de682da0b824411e00a0d4da5a784ec6496b6850fdf8c865c1d68c0e318dd"}, + {file = "coverage-7.4.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:0e8d06778e8fbffccfe96331a3946237f87b1e1d359d7fbe8b06b96c95a5407a"}, + {file = "coverage-7.4.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a56de34db7b7ff77056a37aedded01b2b98b508227d2d0979d373a9b5d353daa"}, + {file = "coverage-7.4.0-cp312-cp312-win32.whl", hash = "sha256:51456e6fa099a8d9d91497202d9563a320513fcf59f33991b0661a4a6f2ad450"}, + {file = "coverage-7.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:cd3c1e4cb2ff0083758f09be0f77402e1bdf704adb7f89108007300a6da587d0"}, + {file = "coverage-7.4.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e9d1bf53c4c8de58d22e0e956a79a5b37f754ed1ffdbf1a260d9dcfa2d8a325e"}, + {file = "coverage-7.4.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:109f5985182b6b81fe33323ab4707011875198c41964f014579cf82cebf2bb85"}, + {file = "coverage-7.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3cc9d4bc55de8003663ec94c2f215d12d42ceea128da8f0f4036235a119c88ac"}, + {file = "coverage-7.4.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cc6d65b21c219ec2072c1293c505cf36e4e913a3f936d80028993dd73c7906b1"}, + {file = "coverage-7.4.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5a10a4920def78bbfff4eff8a05c51be03e42f1c3735be42d851f199144897ba"}, + {file = "coverage-7.4.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b8e99f06160602bc64da35158bb76c73522a4010f0649be44a4e167ff8555952"}, + {file = "coverage-7.4.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:7d360587e64d006402b7116623cebf9d48893329ef035278969fa3bbf75b697e"}, + {file = "coverage-7.4.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:29f3abe810930311c0b5d1a7140f6395369c3db1be68345638c33eec07535105"}, + {file = "coverage-7.4.0-cp38-cp38-win32.whl", hash = "sha256:5040148f4ec43644702e7b16ca864c5314ccb8ee0751ef617d49aa0e2d6bf4f2"}, + {file = "coverage-7.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:9864463c1c2f9cb3b5db2cf1ff475eed2f0b4285c2aaf4d357b69959941aa555"}, + {file = "coverage-7.4.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:936d38794044b26c99d3dd004d8af0035ac535b92090f7f2bb5aa9c8e2f5cd42"}, + {file = "coverage-7.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:799c8f873794a08cdf216aa5d0531c6a3747793b70c53f70e98259720a6fe2d7"}, + {file = "coverage-7.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e7defbb9737274023e2d7af02cac77043c86ce88a907c58f42b580a97d5bcca9"}, + {file = "coverage-7.4.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a1526d265743fb49363974b7aa8d5899ff64ee07df47dd8d3e37dcc0818f09ed"}, + {file = "coverage-7.4.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf635a52fc1ea401baf88843ae8708591aa4adff875e5c23220de43b1ccf575c"}, + {file = "coverage-7.4.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:756ded44f47f330666843b5781be126ab57bb57c22adbb07d83f6b519783b870"}, + {file = "coverage-7.4.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:0eb3c2f32dabe3a4aaf6441dde94f35687224dfd7eb2a7f47f3fd9428e421058"}, + {file = "coverage-7.4.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:bfd5db349d15c08311702611f3dccbef4b4e2ec148fcc636cf8739519b4a5c0f"}, + {file = "coverage-7.4.0-cp39-cp39-win32.whl", hash = "sha256:53d7d9158ee03956e0eadac38dfa1ec8068431ef8058fe6447043db1fb40d932"}, + {file = "coverage-7.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:cfd2a8b6b0d8e66e944d47cdec2f47c48fef2ba2f2dff5a9a75757f64172857e"}, + {file = "coverage-7.4.0-pp38.pp39.pp310-none-any.whl", hash = "sha256:c530833afc4707fe48524a44844493f36d8727f04dcce91fb978c414a8556cc6"}, + {file = "coverage-7.4.0.tar.gz", hash = "sha256:707c0f58cb1712b8809ece32b68996ee1e609f71bd14615bd8f87a1293cb610e"}, ] [package.dependencies] @@ -283,35 +271,35 @@ toml = ["tomli"] [[package]] name = "cryptography" -version = "41.0.5" +version = "41.0.7" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "cryptography-41.0.5-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:da6a0ff8f1016ccc7477e6339e1d50ce5f59b88905585f77193ebd5068f1e797"}, - {file = "cryptography-41.0.5-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:b948e09fe5fb18517d99994184854ebd50b57248736fd4c720ad540560174ec5"}, - {file = "cryptography-41.0.5-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d38e6031e113b7421db1de0c1b1f7739564a88f1684c6b89234fbf6c11b75147"}, - {file = "cryptography-41.0.5-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e270c04f4d9b5671ebcc792b3ba5d4488bf7c42c3c241a3748e2599776f29696"}, - {file = "cryptography-41.0.5-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ec3b055ff8f1dce8e6ef28f626e0972981475173d7973d63f271b29c8a2897da"}, - {file = "cryptography-41.0.5-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:7d208c21e47940369accfc9e85f0de7693d9a5d843c2509b3846b2db170dfd20"}, - {file = "cryptography-41.0.5-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:8254962e6ba1f4d2090c44daf50a547cd5f0bf446dc658a8e5f8156cae0d8548"}, - {file = "cryptography-41.0.5-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:a48e74dad1fb349f3dc1d449ed88e0017d792997a7ad2ec9587ed17405667e6d"}, - {file = "cryptography-41.0.5-cp37-abi3-win32.whl", hash = "sha256:d3977f0e276f6f5bf245c403156673db103283266601405376f075c849a0b936"}, - {file = "cryptography-41.0.5-cp37-abi3-win_amd64.whl", hash = "sha256:73801ac9736741f220e20435f84ecec75ed70eda90f781a148f1bad546963d81"}, - {file = "cryptography-41.0.5-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3be3ca726e1572517d2bef99a818378bbcf7d7799d5372a46c79c29eb8d166c1"}, - {file = "cryptography-41.0.5-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:e886098619d3815e0ad5790c973afeee2c0e6e04b4da90b88e6bd06e2a0b1b72"}, - {file = "cryptography-41.0.5-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:573eb7128cbca75f9157dcde974781209463ce56b5804983e11a1c462f0f4e88"}, - {file = "cryptography-41.0.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:0c327cac00f082013c7c9fb6c46b7cc9fa3c288ca702c74773968173bda421bf"}, - {file = "cryptography-41.0.5-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:227ec057cd32a41c6651701abc0328135e472ed450f47c2766f23267b792a88e"}, - {file = "cryptography-41.0.5-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:22892cc830d8b2c89ea60148227631bb96a7da0c1b722f2aac8824b1b7c0b6b8"}, - {file = "cryptography-41.0.5-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:5a70187954ba7292c7876734183e810b728b4f3965fbe571421cb2434d279179"}, - {file = "cryptography-41.0.5-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:88417bff20162f635f24f849ab182b092697922088b477a7abd6664ddd82291d"}, - {file = "cryptography-41.0.5-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c707f7afd813478e2019ae32a7c49cd932dd60ab2d2a93e796f68236b7e1fbf1"}, - {file = "cryptography-41.0.5-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:580afc7b7216deeb87a098ef0674d6ee34ab55993140838b14c9b83312b37b86"}, - {file = "cryptography-41.0.5-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:fba1e91467c65fe64a82c689dc6cf58151158993b13eb7a7f3f4b7f395636723"}, - {file = "cryptography-41.0.5-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:0d2a6a598847c46e3e321a7aef8af1436f11c27f1254933746304ff014664d84"}, - {file = "cryptography-41.0.5.tar.gz", hash = "sha256:392cb88b597247177172e02da6b7a63deeff1937fa6fec3bbf902ebd75d97ec7"}, + {file = "cryptography-41.0.7-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:3c78451b78313fa81607fa1b3f1ae0a5ddd8014c38a02d9db0616133987b9cdf"}, + {file = "cryptography-41.0.7-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:928258ba5d6f8ae644e764d0f996d61a8777559f72dfeb2eea7e2fe0ad6e782d"}, + {file = "cryptography-41.0.7-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5a1b41bc97f1ad230a41657d9155113c7521953869ae57ac39ac7f1bb471469a"}, + {file = "cryptography-41.0.7-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:841df4caa01008bad253bce2a6f7b47f86dc9f08df4b433c404def869f590a15"}, + {file = "cryptography-41.0.7-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:5429ec739a29df2e29e15d082f1d9ad683701f0ec7709ca479b3ff2708dae65a"}, + {file = "cryptography-41.0.7-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:43f2552a2378b44869fe8827aa19e69512e3245a219104438692385b0ee119d1"}, + {file = "cryptography-41.0.7-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:af03b32695b24d85a75d40e1ba39ffe7db7ffcb099fe507b39fd41a565f1b157"}, + {file = "cryptography-41.0.7-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:49f0805fc0b2ac8d4882dd52f4a3b935b210935d500b6b805f321addc8177406"}, + {file = "cryptography-41.0.7-cp37-abi3-win32.whl", hash = "sha256:f983596065a18a2183e7f79ab3fd4c475205b839e02cbc0efbbf9666c4b3083d"}, + {file = "cryptography-41.0.7-cp37-abi3-win_amd64.whl", hash = "sha256:90452ba79b8788fa380dfb587cca692976ef4e757b194b093d845e8d99f612f2"}, + {file = "cryptography-41.0.7-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:079b85658ea2f59c4f43b70f8119a52414cdb7be34da5d019a77bf96d473b960"}, + {file = "cryptography-41.0.7-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:b640981bf64a3e978a56167594a0e97db71c89a479da8e175d8bb5be5178c003"}, + {file = "cryptography-41.0.7-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:e3114da6d7f95d2dee7d3f4eec16dacff819740bbab931aff8648cb13c5ff5e7"}, + {file = "cryptography-41.0.7-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d5ec85080cce7b0513cfd233914eb8b7bbd0633f1d1703aa28d1dd5a72f678ec"}, + {file = "cryptography-41.0.7-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:7a698cb1dac82c35fcf8fe3417a3aaba97de16a01ac914b89a0889d364d2f6be"}, + {file = "cryptography-41.0.7-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:37a138589b12069efb424220bf78eac59ca68b95696fc622b6ccc1c0a197204a"}, + {file = "cryptography-41.0.7-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:68a2dec79deebc5d26d617bfdf6e8aab065a4f34934b22d3b5010df3ba36612c"}, + {file = "cryptography-41.0.7-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:09616eeaef406f99046553b8a40fbf8b1e70795a91885ba4c96a70793de5504a"}, + {file = "cryptography-41.0.7-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:48a0476626da912a44cc078f9893f292f0b3e4c739caf289268168d8f4702a39"}, + {file = "cryptography-41.0.7-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c7f3201ec47d5207841402594f1d7950879ef890c0c495052fa62f58283fde1a"}, + {file = "cryptography-41.0.7-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c5ca78485a255e03c32b513f8c2bc39fedb7f5c5f8535545bdc223a03b24f248"}, + {file = "cryptography-41.0.7-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:d6c391c021ab1f7a82da5d8d0b3cee2f4b2c455ec86c8aebbc84837a631ff309"}, + {file = "cryptography-41.0.7.tar.gz", hash = "sha256:13f93ce9bea8016c253b34afc6bd6a75993e5c40672ed5405a9c832f0d4a00bc"}, ] [package.dependencies] @@ -329,14 +317,14 @@ test-randomorder = ["pytest-randomly"] [[package]] name = "distlib" -version = "0.3.7" +version = "0.3.8" description = "Distribution utilities" category = "dev" optional = false python-versions = "*" files = [ - {file = "distlib-0.3.7-py2.py3-none-any.whl", hash = "sha256:2e24928bc811348f0feb63014e97aaae3037f2cf48712d51ae61df7fd6075057"}, - {file = "distlib-0.3.7.tar.gz", hash = "sha256:9dafe54b34a028eafd95039d5e5d4851a13734540f1331060d31c9916e7147a8"}, + {file = "distlib-0.3.8-py2.py3-none-any.whl", hash = "sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784"}, + {file = "distlib-0.3.8.tar.gz", hash = "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64"}, ] [[package]] @@ -353,14 +341,14 @@ files = [ [[package]] name = "exceptiongroup" -version = "1.1.3" +version = "1.2.0" description = "Backport of PEP 654 (exception groups)" category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "exceptiongroup-1.1.3-py3-none-any.whl", hash = "sha256:343280667a4585d195ca1cf9cef84a4e178c4b6cf2274caef9859782b567d5e3"}, - {file = "exceptiongroup-1.1.3.tar.gz", hash = "sha256:097acd85d473d75af5bb98e41b61ff7fe35efe6675e4f9370ec6ec5126d160e9"}, + {file = "exceptiongroup-1.2.0-py3-none-any.whl", hash = "sha256:4bfd3996ac73b41e9b9628b04e079f193850720ea5945fc96a08633c66912f14"}, + {file = "exceptiongroup-1.2.0.tar.gz", hash = "sha256:91f5c769735f051a4290d52edd0858999b57e5876e9f85937691bd4c9fa3ed68"}, ] [package.extras] @@ -385,14 +373,14 @@ typing = ["typing-extensions (>=4.8)"] [[package]] name = "identify" -version = "2.5.32" +version = "2.5.33" description = "File identification library for Python" category = "dev" optional = false python-versions = ">=3.8" files = [ - {file = "identify-2.5.32-py2.py3-none-any.whl", hash = "sha256:0b7656ef6cba81664b783352c73f8c24b39cf82f926f78f4550eda928e5e0545"}, - {file = "identify-2.5.32.tar.gz", hash = "sha256:5d9979348ec1a21c768ae07e0a652924538e8bce67313a73cb0f681cf08ba407"}, + {file = "identify-2.5.33-py2.py3-none-any.whl", hash = "sha256:d40ce5fcd762817627670da8a7d8d8e65f24342d14539c59488dc603bf662e34"}, + {file = "identify-2.5.33.tar.gz", hash = "sha256:161558f9fe4559e1557e1bff323e8631f6a0e4837f7497767c1782832f16b62d"}, ] [package.extras] @@ -400,33 +388,33 @@ license = ["ukkonen"] [[package]] name = "idna" -version = "3.4" +version = "3.6" description = "Internationalized Domain Names in Applications (IDNA)" category = "dev" optional = false python-versions = ">=3.5" files = [ - {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, - {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, + {file = "idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f"}, + {file = "idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca"}, ] [[package]] name = "importlib-metadata" -version = "6.8.0" +version = "7.0.1" description = "Read metadata from Python packages" category = "dev" optional = false python-versions = ">=3.8" files = [ - {file = "importlib_metadata-6.8.0-py3-none-any.whl", hash = "sha256:3ebb78df84a805d7698245025b975d9d67053cd94c79245ba4b3eb694abe68bb"}, - {file = "importlib_metadata-6.8.0.tar.gz", hash = "sha256:dbace7892d8c0c4ac1ad096662232f831d4e64f4c4545bd53016a3e9d4654743"}, + {file = "importlib_metadata-7.0.1-py3-none-any.whl", hash = "sha256:4805911c3a4ec7c3966410053e9ec6a1fecd629117df5adee56dfc9432a1081e"}, + {file = "importlib_metadata-7.0.1.tar.gz", hash = "sha256:f238736bb06590ae52ac1fab06a3a9ef1d8dce2b7a35b5ab329371d6c8f5d2cc"}, ] [package.dependencies] zipp = ">=0.5" [package.extras] -docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-lint"] perf = ["ipython"] testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)", "pytest-ruff"] @@ -552,28 +540,28 @@ files = [ [[package]] name = "nh3" -version = "0.2.14" -description = "Ammonia HTML sanitizer Python binding" +version = "0.2.15" +description = "Python bindings to the ammonia HTML sanitization library." category = "dev" optional = false python-versions = "*" files = [ - {file = "nh3-0.2.14-cp37-abi3-macosx_10_7_x86_64.whl", hash = "sha256:9be2f68fb9a40d8440cbf34cbf40758aa7f6093160bfc7fb018cce8e424f0c3a"}, - {file = "nh3-0.2.14-cp37-abi3-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:f99212a81c62b5f22f9e7c3e347aa00491114a5647e1f13bbebd79c3e5f08d75"}, - {file = "nh3-0.2.14-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7771d43222b639a4cd9e341f870cee336b9d886de1ad9bec8dddab22fe1de450"}, - {file = "nh3-0.2.14-cp37-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:525846c56c2bcd376f5eaee76063ebf33cf1e620c1498b2a40107f60cfc6054e"}, - {file = "nh3-0.2.14-cp37-abi3-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:e8986f1dd3221d1e741fda0a12eaa4a273f1d80a35e31a1ffe579e7c621d069e"}, - {file = "nh3-0.2.14-cp37-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:18415df36db9b001f71a42a3a5395db79cf23d556996090d293764436e98e8ad"}, - {file = "nh3-0.2.14-cp37-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:377aaf6a9e7c63962f367158d808c6a1344e2b4f83d071c43fbd631b75c4f0b2"}, - {file = "nh3-0.2.14-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2b0be5c792bd43d0abef8ca39dd8acb3c0611052ce466d0401d51ea0d9aa7525"}, - {file = "nh3-0.2.14-cp37-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:93a943cfd3e33bd03f77b97baa11990148687877b74193bf777956b67054dcc6"}, - {file = "nh3-0.2.14-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ac8056e937f264995a82bf0053ca898a1cb1c9efc7cd68fa07fe0060734df7e4"}, - {file = "nh3-0.2.14-cp37-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:203cac86e313cf6486704d0ec620a992c8bc164c86d3a4fd3d761dd552d839b5"}, - {file = "nh3-0.2.14-cp37-abi3-musllinux_1_2_i686.whl", hash = "sha256:5529a3bf99402c34056576d80ae5547123f1078da76aa99e8ed79e44fa67282d"}, - {file = "nh3-0.2.14-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:aed56a86daa43966dd790ba86d4b810b219f75b4bb737461b6886ce2bde38fd6"}, - {file = "nh3-0.2.14-cp37-abi3-win32.whl", hash = "sha256:116c9515937f94f0057ef50ebcbcc10600860065953ba56f14473ff706371873"}, - {file = "nh3-0.2.14-cp37-abi3-win_amd64.whl", hash = "sha256:88c753efbcdfc2644a5012938c6b9753f1c64a5723a67f0301ca43e7b85dcf0e"}, - {file = "nh3-0.2.14.tar.gz", hash = "sha256:a0c509894fd4dccdff557068e5074999ae3b75f4c5a2d6fb5415e782e25679c4"}, + {file = "nh3-0.2.15-cp37-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:9c0d415f6b7f2338f93035bba5c0d8c1b464e538bfbb1d598acd47d7969284f0"}, + {file = "nh3-0.2.15-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:6f42f99f0cf6312e470b6c09e04da31f9abaadcd3eb591d7d1a88ea931dca7f3"}, + {file = "nh3-0.2.15-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac19c0d68cd42ecd7ead91a3a032fdfff23d29302dbb1311e641a130dfefba97"}, + {file = "nh3-0.2.15-cp37-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f0d77272ce6d34db6c87b4f894f037d55183d9518f948bba236fe81e2bb4e28"}, + {file = "nh3-0.2.15-cp37-abi3-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:8d595df02413aa38586c24811237e95937ef18304e108b7e92c890a06793e3bf"}, + {file = "nh3-0.2.15-cp37-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:86e447a63ca0b16318deb62498db4f76fc60699ce0a1231262880b38b6cff911"}, + {file = "nh3-0.2.15-cp37-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3277481293b868b2715907310c7be0f1b9d10491d5adf9fce11756a97e97eddf"}, + {file = "nh3-0.2.15-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:60684857cfa8fdbb74daa867e5cad3f0c9789415aba660614fe16cd66cbb9ec7"}, + {file = "nh3-0.2.15-cp37-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3b803a5875e7234907f7d64777dfde2b93db992376f3d6d7af7f3bc347deb305"}, + {file = "nh3-0.2.15-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0d02d0ff79dfd8208ed25a39c12cbda092388fff7f1662466e27d97ad011b770"}, + {file = "nh3-0.2.15-cp37-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:f3b53ba93bb7725acab1e030bc2ecd012a817040fd7851b332f86e2f9bb98dc6"}, + {file = "nh3-0.2.15-cp37-abi3-musllinux_1_2_i686.whl", hash = "sha256:b1e97221cedaf15a54f5243f2c5894bb12ca951ae4ddfd02a9d4ea9df9e1a29d"}, + {file = "nh3-0.2.15-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a5167a6403d19c515217b6bcaaa9be420974a6ac30e0da9e84d4fc67a5d474c5"}, + {file = "nh3-0.2.15-cp37-abi3-win32.whl", hash = "sha256:427fecbb1031db085eaac9931362adf4a796428ef0163070c484b5a768e71601"}, + {file = "nh3-0.2.15-cp37-abi3-win_amd64.whl", hash = "sha256:bc2d086fb540d0fa52ce35afaded4ea526b8fc4d3339f783db55c95de40ef02e"}, + {file = "nh3-0.2.15.tar.gz", hash = "sha256:d1e30ff2d8d58fb2a14961f7aac1bbb1c51f9bdd7da727be35c63826060b0bf3"}, ] [[package]] @@ -630,6 +618,22 @@ files = [ [package.extras] testing = ["pytest", "pytest-cov"] +[[package]] +name = "platformdirs" +version = "4.1.0" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +category = "dev" +optional = false +python-versions = ">=3.8" +files = [ + {file = "platformdirs-4.1.0-py3-none-any.whl", hash = "sha256:11c8f37bcca40db96d8144522d925583bdb7a31f7b0e37e3ed4318400a8e2380"}, + {file = "platformdirs-4.1.0.tar.gz", hash = "sha256:906d548203468492d432bcb294d4bc2fff751bf84971fbb2c10918cc206ee420"}, +] + +[package.extras] +docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.1)", "sphinx-autodoc-typehints (>=1.24)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4)", "pytest-cov (>=4.1)", "pytest-mock (>=3.11.1)"] + [[package]] name = "pluggy" version = "1.3.0" @@ -648,14 +652,14 @@ testing = ["pytest", "pytest-benchmark"] [[package]] name = "pre-commit" -version = "2.20.0" +version = "3.6.0" description = "A framework for managing and maintaining multi-language pre-commit hooks." category = "dev" optional = false -python-versions = ">=3.7" +python-versions = ">=3.9" files = [ - {file = "pre_commit-2.20.0-py2.py3-none-any.whl", hash = "sha256:51a5ba7c480ae8072ecdb6933df22d2f812dc897d5fe848778116129a681aac7"}, - {file = "pre_commit-2.20.0.tar.gz", hash = "sha256:a978dac7bc9ec0bcee55c18a277d553b0f419d259dadb4b9418ff2d00eb43959"}, + {file = "pre_commit-3.6.0-py2.py3-none-any.whl", hash = "sha256:c255039ef399049a5544b6ce13d135caba8f2c28c3b4033277a788f434308376"}, + {file = "pre_commit-3.6.0.tar.gz", hash = "sha256:d30bad9abf165f7785c15a21a1f46da7d0677cb00ee7ff4c579fd38922efe15d"}, ] [package.dependencies] @@ -663,8 +667,7 @@ cfgv = ">=2.0.0" identify = ">=1.0.0" nodeenv = ">=0.11.1" pyyaml = ">=5.1" -toml = "*" -virtualenv = ">=20.0.8" +virtualenv = ">=20.10.0" [[package]] name = "py-cpuinfo" @@ -692,14 +695,14 @@ files = [ [[package]] name = "pygments" -version = "2.17.1" +version = "2.17.2" description = "Pygments is a syntax highlighting package written in Python." category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "pygments-2.17.1-py3-none-any.whl", hash = "sha256:1b37f1b1e1bff2af52ecaf28cc601e2ef7077000b227a0675da25aef85784bc4"}, - {file = "pygments-2.17.1.tar.gz", hash = "sha256:e45a0e74bf9c530f564ca81b8952343be986a29f6afe7f5ad95c5f06b7bdf5e8"}, + {file = "pygments-2.17.2-py3-none-any.whl", hash = "sha256:b27c2826c47d0f3219f29554824c30c5e8945175d888647acd804ddd04af846c"}, + {file = "pygments-2.17.2.tar.gz", hash = "sha256:da46cec9fd2de5be3a8a784f434e4c4ab670b4ff54d605c4c2717e9d49c4c367"}, ] [package.extras] @@ -708,68 +711,68 @@ windows-terminal = ["colorama (>=0.4.6)"] [[package]] name = "pyside6" -version = "6.6.0" +version = "6.6.1" description = "Python bindings for the Qt cross-platform application and UI framework" category = "dev" optional = false python-versions = "<3.13,>=3.8" files = [ - {file = "PySide6-6.6.0-cp38-abi3-macosx_11_0_universal2.whl", hash = "sha256:8103f14ed46a05e81acccbfc8388e3321e392fe54f3aa4a13336bd2ed8af5cd4"}, - {file = "PySide6-6.6.0-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:d487eab0f9bfc5c9141b474093e16207ff48cd9335e6465a01deb8dff0693fbc"}, - {file = "PySide6-6.6.0-cp38-abi3-manylinux_2_31_aarch64.whl", hash = "sha256:f40917b7a0c5c7f0c7faaa87e66ffa3b58d9d1d3d977d30fc07e193b82dd6749"}, - {file = "PySide6-6.6.0-cp38-abi3-win_amd64.whl", hash = "sha256:d41dfcfa32c89502cdaa20206b7e2aba199072f94b424bc6a4d6f50b10f4eb5a"}, + {file = "PySide6-6.6.1-cp38-abi3-macosx_11_0_universal2.whl", hash = "sha256:3c348948fe3957b18164c9c7b8942fe065bdb39648b326f212bc114326679fa9"}, + {file = "PySide6-6.6.1-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:0a67587c088cb80e90d4ce3023b02466ea858c93a6dc9c4e062b13314e03d464"}, + {file = "PySide6-6.6.1-cp38-abi3-manylinux_2_31_aarch64.whl", hash = "sha256:ed3822150f0d7a06b68bf4ceebe287515b5e8309bb256e9b49ae405afd062b18"}, + {file = "PySide6-6.6.1-cp38-abi3-win_amd64.whl", hash = "sha256:3593d605175e83e6952cf3b428ecc9c146af97effb36de921ecf3da2752de082"}, ] [package.dependencies] -PySide6-Addons = "6.6.0" -PySide6-Essentials = "6.6.0" -shiboken6 = "6.6.0" +PySide6-Addons = "6.6.1" +PySide6-Essentials = "6.6.1" +shiboken6 = "6.6.1" [[package]] name = "pyside6-addons" -version = "6.6.0" +version = "6.6.1" description = "Python bindings for the Qt cross-platform application and UI framework (Addons)" category = "dev" optional = false python-versions = "<3.13,>=3.8" files = [ - {file = "PySide6_Addons-6.6.0-cp38-abi3-macosx_11_0_universal2.whl", hash = "sha256:82fe1bb6a1aabf5ab3d8632072dc908aa37fc75b4b46e520258c441bd6b103fb"}, - {file = "PySide6_Addons-6.6.0-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:5c56e963b841aeaacbc9ca8ca34df45308818dbd6fc59faa2b5a00a299e9892b"}, - {file = "PySide6_Addons-6.6.0-cp38-abi3-manylinux_2_31_aarch64.whl", hash = "sha256:e145514a7c37ee3a6ad0ddd693f805ccff6fbd9640a24fb2ef6b7f852882d3e9"}, - {file = "PySide6_Addons-6.6.0-cp38-abi3-win_amd64.whl", hash = "sha256:414864f7cfbdf8ac4c5a745566a515f18b6018d5730bf5489cc2716b5e1fcd9b"}, + {file = "PySide6_Addons-6.6.1-cp38-abi3-macosx_11_0_universal2.whl", hash = "sha256:7cb7af1b050c40f7ac891b0e61c758c1923863173932f5b92dc47bdfb4158b42"}, + {file = "PySide6_Addons-6.6.1-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a0982da4033319667f9df5ed6fa8eff300a88216aec103a1fff6751a172b19a0"}, + {file = "PySide6_Addons-6.6.1-cp38-abi3-manylinux_2_31_aarch64.whl", hash = "sha256:5a63a8a943724ce5acd2df72e5ab04982b6906963278cbabb216656b9a26ee18"}, + {file = "PySide6_Addons-6.6.1-cp38-abi3-win_amd64.whl", hash = "sha256:a223575c81e9a13173136c044c3447e25f6d656b462b4d71fc3c6bd9c935a709"}, ] [package.dependencies] -PySide6-Essentials = "6.6.0" -shiboken6 = "6.6.0" +PySide6-Essentials = "6.6.1" +shiboken6 = "6.6.1" [[package]] name = "pyside6-essentials" -version = "6.6.0" +version = "6.6.1" description = "Python bindings for the Qt cross-platform application and UI framework (Essentials)" category = "dev" optional = false python-versions = "<3.13,>=3.8" files = [ - {file = "PySide6_Essentials-6.6.0-cp38-abi3-macosx_11_0_universal2.whl", hash = "sha256:26d27e3e3a18acbc770a69a30774fd345414fe4932fcb89d2cfc4fc130c3eb33"}, - {file = "PySide6_Essentials-6.6.0-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:60284641619f964e1cb4d53cf3169d7a385e0378b74edb75610918d2aea1c4e5"}, - {file = "PySide6_Essentials-6.6.0-cp38-abi3-manylinux_2_31_aarch64.whl", hash = "sha256:37d5d9c6ee696a17890f07e7ce1f88f890174bd4aca29f47922ecc959ab28352"}, - {file = "PySide6_Essentials-6.6.0-cp38-abi3-win_amd64.whl", hash = "sha256:97bbe33f584a09cef6a3fbc1295fb0fd7acaac345bc0f5601bf45d41c91683ac"}, + {file = "PySide6_Essentials-6.6.1-cp38-abi3-macosx_11_0_universal2.whl", hash = "sha256:0c8917b15236956957178a8c9854641b12b11dad79ba0caf26147119164c30cf"}, + {file = "PySide6_Essentials-6.6.1-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:c7185616083eab6f42eaed598d97d49fac4f60ae2e7415194140d54f58c2b42c"}, + {file = "PySide6_Essentials-6.6.1-cp38-abi3-manylinux_2_31_aarch64.whl", hash = "sha256:a383c3d60298392cfb621ec1a0cf24b4799321e6c5bbafc021d4cc8076ea1315"}, + {file = "PySide6_Essentials-6.6.1-cp38-abi3-win_amd64.whl", hash = "sha256:13da926e9e9ee3e26e3f66883a9d5e43726ddee70cdabddca02a07aa1ccf9484"}, ] [package.dependencies] -shiboken6 = "6.6.0" +shiboken6 = "6.6.1" [[package]] name = "pytest" -version = "7.4.3" +version = "7.4.4" description = "pytest: simple powerful testing with Python" category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "pytest-7.4.3-py3-none-any.whl", hash = "sha256:0d009c083ea859a71b76adf7c1d502e4bc170b80a8ef002da5806527b9591fac"}, - {file = "pytest-7.4.3.tar.gz", hash = "sha256:d989d136982de4e3b29dabcc838ad581c64e8ed52c11fbe86ddebd9da0818cd5"}, + {file = "pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8"}, + {file = "pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280"}, ] [package.dependencies] @@ -825,14 +828,14 @@ testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtuale [[package]] name = "pytest-qt" -version = "4.2.0" +version = "4.3.1" description = "pytest support for PyQt and PySide applications" category = "dev" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "pytest-qt-4.2.0.tar.gz", hash = "sha256:00a17b586dd530b6d7a9399923a40489ca4a9a309719011175f55dc6b5dc8f41"}, - {file = "pytest_qt-4.2.0-py2.py3-none-any.whl", hash = "sha256:a7659960a1ab2af8fc944655a157ff45d714b80ed7a6af96a4b5bb99ecf40a22"}, + {file = "pytest-qt-4.3.1.tar.gz", hash = "sha256:0256041af78082dfc08e22ccf860aaa878d8029557f03f4bfc2007fc291b1e61"}, + {file = "pytest_qt-4.3.1-py3-none-any.whl", hash = "sha256:e1bd043a4e460f184657bc3a0eabf289ad5e88a55a2abf0436d248049a331653"}, ] [package.dependencies] @@ -910,6 +913,7 @@ files = [ {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, + {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"}, {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, @@ -917,8 +921,15 @@ files = [ {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, + {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}, {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, + {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, + {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, + {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, + {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}, {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, @@ -935,6 +946,7 @@ files = [ {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, + {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"}, {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, @@ -942,6 +954,7 @@ files = [ {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, + {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"}, {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, @@ -1040,29 +1053,29 @@ jupyter = ["ipywidgets (>=7.5.1,<9)"] [[package]] name = "ruff" -version = "0.1.6" +version = "0.1.11" description = "An extremely fast Python linter and code formatter, written in Rust." category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "ruff-0.1.6-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:88b8cdf6abf98130991cbc9f6438f35f6e8d41a02622cc5ee130a02a0ed28703"}, - {file = "ruff-0.1.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:5c549ed437680b6105a1299d2cd30e4964211606eeb48a0ff7a93ef70b902248"}, - {file = "ruff-0.1.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1cf5f701062e294f2167e66d11b092bba7af6a057668ed618a9253e1e90cfd76"}, - {file = "ruff-0.1.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:05991ee20d4ac4bb78385360c684e4b417edd971030ab12a4fbd075ff535050e"}, - {file = "ruff-0.1.6-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:87455a0c1f739b3c069e2f4c43b66479a54dea0276dd5d4d67b091265f6fd1dc"}, - {file = "ruff-0.1.6-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:683aa5bdda5a48cb8266fcde8eea2a6af4e5700a392c56ea5fb5f0d4bfdc0240"}, - {file = "ruff-0.1.6-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:137852105586dcbf80c1717facb6781555c4e99f520c9c827bd414fac67ddfb6"}, - {file = "ruff-0.1.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd98138a98d48a1c36c394fd6b84cd943ac92a08278aa8ac8c0fdefcf7138f35"}, - {file = "ruff-0.1.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a0cd909d25f227ac5c36d4e7e681577275fb74ba3b11d288aff7ec47e3ae745"}, - {file = "ruff-0.1.6-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e8fd1c62a47aa88a02707b5dd20c5ff20d035d634aa74826b42a1da77861b5ff"}, - {file = "ruff-0.1.6-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:fd89b45d374935829134a082617954120d7a1470a9f0ec0e7f3ead983edc48cc"}, - {file = "ruff-0.1.6-py3-none-musllinux_1_2_i686.whl", hash = "sha256:491262006e92f825b145cd1e52948073c56560243b55fb3b4ecb142f6f0e9543"}, - {file = "ruff-0.1.6-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:ea284789861b8b5ca9d5443591a92a397ac183d4351882ab52f6296b4fdd5462"}, - {file = "ruff-0.1.6-py3-none-win32.whl", hash = "sha256:1610e14750826dfc207ccbcdd7331b6bd285607d4181df9c1c6ae26646d6848a"}, - {file = "ruff-0.1.6-py3-none-win_amd64.whl", hash = "sha256:4558b3e178145491e9bc3b2ee3c4b42f19d19384eaa5c59d10acf6e8f8b57e33"}, - {file = "ruff-0.1.6-py3-none-win_arm64.whl", hash = "sha256:03910e81df0d8db0e30050725a5802441c2022ea3ae4fe0609b76081731accbc"}, - {file = "ruff-0.1.6.tar.gz", hash = "sha256:1b09f29b16c6ead5ea6b097ef2764b42372aebe363722f1605ecbcd2b9207184"}, + {file = "ruff-0.1.11-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:a7f772696b4cdc0a3b2e527fc3c7ccc41cdcb98f5c80fdd4f2b8c50eb1458196"}, + {file = "ruff-0.1.11-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:934832f6ed9b34a7d5feea58972635c2039c7a3b434fe5ba2ce015064cb6e955"}, + {file = "ruff-0.1.11-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ea0d3e950e394c4b332bcdd112aa566010a9f9c95814844a7468325290aabfd9"}, + {file = "ruff-0.1.11-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9bd4025b9c5b429a48280785a2b71d479798a69f5c2919e7d274c5f4b32c3607"}, + {file = "ruff-0.1.11-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e1ad00662305dcb1e987f5ec214d31f7d6a062cae3e74c1cbccef15afd96611d"}, + {file = "ruff-0.1.11-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:4b077ce83f47dd6bea1991af08b140e8b8339f0ba8cb9b7a484c30ebab18a23f"}, + {file = "ruff-0.1.11-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c4a88efecec23c37b11076fe676e15c6cdb1271a38f2b415e381e87fe4517f18"}, + {file = "ruff-0.1.11-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5b25093dad3b055667730a9b491129c42d45e11cdb7043b702e97125bcec48a1"}, + {file = "ruff-0.1.11-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:231d8fb11b2cc7c0366a326a66dafc6ad449d7fcdbc268497ee47e1334f66f77"}, + {file = "ruff-0.1.11-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:09c415716884950080921dd6237767e52e227e397e2008e2bed410117679975b"}, + {file = "ruff-0.1.11-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:0f58948c6d212a6b8d41cd59e349751018797ce1727f961c2fa755ad6208ba45"}, + {file = "ruff-0.1.11-py3-none-musllinux_1_2_i686.whl", hash = "sha256:190a566c8f766c37074d99640cd9ca3da11d8deae2deae7c9505e68a4a30f740"}, + {file = "ruff-0.1.11-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:6464289bd67b2344d2a5d9158d5eb81025258f169e69a46b741b396ffb0cda95"}, + {file = "ruff-0.1.11-py3-none-win32.whl", hash = "sha256:9b8f397902f92bc2e70fb6bebfa2139008dc72ae5177e66c383fa5426cb0bf2c"}, + {file = "ruff-0.1.11-py3-none-win_amd64.whl", hash = "sha256:eb85ee287b11f901037a6683b2374bb0ec82928c5cbc984f575d0437979c521a"}, + {file = "ruff-0.1.11-py3-none-win_arm64.whl", hash = "sha256:97ce4d752f964ba559c7023a86e5f8e97f026d511e48013987623915431c7ea9"}, + {file = "ruff-0.1.11.tar.gz", hash = "sha256:f9d4d88cb6eeb4dfe20f9f0519bd2eaba8119bde87c3d5065c541dbae2b5a2cb"}, ] [[package]] @@ -1083,57 +1096,33 @@ jeepney = ">=0.6" [[package]] name = "setuptools" -version = "68.2.2" +version = "69.0.3" description = "Easily download, build, install, upgrade, and uninstall Python packages" category = "dev" optional = false python-versions = ">=3.8" files = [ - {file = "setuptools-68.2.2-py3-none-any.whl", hash = "sha256:b454a35605876da60632df1a60f736524eb73cc47bbc9f3f1ef1b644de74fd2a"}, - {file = "setuptools-68.2.2.tar.gz", hash = "sha256:4ac1475276d2f1c48684874089fefcd83bd7162ddaafb81fac866ba0db282a87"}, + {file = "setuptools-69.0.3-py3-none-any.whl", hash = "sha256:385eb4edd9c9d5c17540511303e39a147ce2fc04bc55289c322b9e5904fe2c05"}, + {file = "setuptools-69.0.3.tar.gz", hash = "sha256:be1af57fc409f93647f2e8e4573a142ed38724b8cdd389706a867bb4efcf1e78"}, ] [package.extras] -docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.1)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] [[package]] name = "shiboken6" -version = "6.6.0" +version = "6.6.1" description = "Python/C++ bindings helper module" category = "dev" optional = false python-versions = "<3.13,>=3.8" files = [ - {file = "shiboken6-6.6.0-cp38-abi3-macosx_11_0_universal2.whl", hash = "sha256:bf23c43e53ffe6097853666ced05358c6640c378e992902a36c4cf77efb0223b"}, - {file = "shiboken6-6.6.0-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:456b89fb4b323e0c5002d92e4d346b48bb4e709db801208df8a0d6b4f5efc33d"}, - {file = "shiboken6-6.6.0-cp38-abi3-manylinux_2_31_aarch64.whl", hash = "sha256:fb8c683206c3365f9240fff7f908485384893b02efb089b69ff816b4390baa9a"}, - {file = "shiboken6-6.6.0-cp38-abi3-win_amd64.whl", hash = "sha256:e62b2610b84f0ff7ed0181a4c535849cdc0654127097b5ef561cf0a33078f245"}, -] - -[[package]] -name = "six" -version = "1.16.0" -description = "Python 2 and 3 compatibility utilities" -category = "dev" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" -files = [ - {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, - {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, -] - -[[package]] -name = "toml" -version = "0.10.2" -description = "Python Library for Tom's Obvious, Minimal Language" -category = "dev" -optional = false -python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" -files = [ - {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, - {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, + {file = "shiboken6-6.6.1-cp38-abi3-macosx_11_0_universal2.whl", hash = "sha256:d756fd1fa945b787e8eef142f2eb571da0b4c4dc2f2eec1a7c12a474a2cf84e4"}, + {file = "shiboken6-6.6.1-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:fb102e4bc210006f0cdd0ce38e1aaaaf792bd871f02a2b3f01d07922c5cf4c59"}, + {file = "shiboken6-6.6.1-cp38-abi3-manylinux_2_31_aarch64.whl", hash = "sha256:a605960e72af5eef915991cee7eef4cc72f5cabe63b9ae1a955ceb3d3b0a00b9"}, + {file = "shiboken6-6.6.1-cp38-abi3-win_amd64.whl", hash = "sha256:072c35c4fe46ec13b364d9dc47b055bb2277ee3aeaab18c23650280ec362f62a"}, ] [[package]] @@ -1190,25 +1179,24 @@ zstd = ["zstandard (>=0.18.0)"] [[package]] name = "virtualenv" -version = "20.4.7" +version = "20.25.0" description = "Virtual Python Environment builder" category = "dev" optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" +python-versions = ">=3.7" files = [ - {file = "virtualenv-20.4.7-py2.py3-none-any.whl", hash = "sha256:2b0126166ea7c9c3661f5b8e06773d28f83322de7a3ff7d06f0aed18c9de6a76"}, - {file = "virtualenv-20.4.7.tar.gz", hash = "sha256:14fdf849f80dbb29a4eb6caa9875d476ee2a5cf76a5f5415fa2f1606010ab467"}, + {file = "virtualenv-20.25.0-py3-none-any.whl", hash = "sha256:4238949c5ffe6876362d9c0180fc6c3a824a7b12b80604eeb8085f2ed7460de3"}, + {file = "virtualenv-20.25.0.tar.gz", hash = "sha256:bf51c0d9c7dd63ea8e44086fa1e4fb1093a31e963b86959257378aef020e1f1b"}, ] [package.dependencies] -appdirs = ">=1.4.3,<2" -distlib = ">=0.3.1,<1" -filelock = ">=3.0.0,<4" -six = ">=1.9.0,<2" +distlib = ">=0.3.7,<1" +filelock = ">=3.12.2,<4" +platformdirs = ">=3.9.1,<5" [package.extras] -docs = ["proselint (>=0.10.2)", "sphinx (>=3)", "sphinx-argparse (>=0.2.5)", "sphinx-rtd-theme (>=0.4.3)", "towncrier (>=19.9.0rc1)"] -testing = ["coverage (>=4)", "coverage-enable-subprocess (>=1)", "flaky (>=3)", "packaging (>=20.0)", "pytest (>=4)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.1)", "pytest-mock (>=2)", "pytest-randomly (>=1)", "pytest-timeout (>=1)", "xonsh (>=0.9.16)"] +docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] +test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"] [[package]] name = "zipp" From 71bbc4572f1eee825c42656eb30ae8cc4c10f0a5 Mon Sep 17 00:00:00 2001 From: Korijn van Golen Date: Mon, 8 Jan 2024 11:28:02 +0100 Subject: [PATCH 11/16] workaround issue in pyside 6.6.1 --- tests/test_qt.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/test_qt.py b/tests/test_qt.py index ea700d7..9f4131e 100644 --- a/tests/test_qt.py +++ b/tests/test_qt.py @@ -28,6 +28,20 @@ def qtasyncio(): scheduler.register_request_flush(old_callback) +@pytest.fixture +def qapp(qapp_args, qapp_cls, pytestconfig): + # workaround for https://bugreports.qt.io/browse/PYSIDE-2575 + from pytestqt.qt_compat import qt_api + + app = qt_api.QtWidgets.QApplication.instance() + if app is None: + _qapp_instance = qapp_cls(qapp_args) + name = pytestconfig.getini("qt_qapp_name") + _qapp_instance.setApplicationName(name) + return _qapp_instance + return app + + @pytest.mark.skipif(not has_qt, reason=qt_missing_reason) def test_scheduler_pyside_asyncio(qtasyncio, qapp): """ From a59da2c2eb9e3cd66eb8f7e270144010fcfb4fc1 Mon Sep 17 00:00:00 2001 From: Korijn van Golen Date: Mon, 8 Jan 2024 11:28:16 +0100 Subject: [PATCH 12/16] bump version --- observ/__init__.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/observ/__init__.py b/observ/__init__.py index 7cd2bad..9a11aac 100644 --- a/observ/__init__.py +++ b/observ/__init__.py @@ -1,4 +1,4 @@ -__version__ = "0.14.1" +__version__ = "0.15.0" from .init import init, loop_factory diff --git a/pyproject.toml b/pyproject.toml index df944d9..ea38373 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "observ" -version = "0.14.1" +version = "0.15.0" description = "Reactive state management for Python" authors = [ "Korijn van Golen ", From 9d1f13b3a547dba89d9416cae7aa1018f3ffce0f Mon Sep 17 00:00:00 2001 From: Korijn van Golen Date: Mon, 8 Jan 2024 14:01:55 +0100 Subject: [PATCH 13/16] numpy handling --- observ/object_proxy.py | 13 +++++++---- observ/watcher.py | 5 ++-- poetry.lock | 48 +++++++++++++++++++++++++++++++++++++- pyproject.toml | 7 ++++++ tests/test_object_proxy.py | 17 ++++++++++++++ tests/test_qt.py | 3 +-- 6 files changed, 83 insertions(+), 10 deletions(-) diff --git a/observ/object_proxy.py b/observ/object_proxy.py index dcad734..83de537 100644 --- a/observ/object_proxy.py +++ b/observ/object_proxy.py @@ -97,6 +97,8 @@ def trap(self, *args, **kwargs): # TODO: how to verify this is the correct and complete set of magic methods? +# scraped from https://docs.python.org/3/reference/datamodel.html +# as of py3.12 magic_methods = [ "__abs__", "__add__", @@ -243,11 +245,12 @@ def __init__(self, target, shallow=False, **kwargs): def type_test(target): - # exclude builtin objects and objects for which we have better proxies available - return ( - not isinstance(target, (list, set, dict, tuple)) - and type(target).__module__ != object.__module__ - ) + # exclude builtin objects + # exclude objects for which we have better proxies available + # exclude ndarrays + return not isinstance(target, (list, set, dict, tuple)) and type( + target + ).__module__ not in (object.__module__, "numpy") TYPE_LOOKUP[type_test] = (ObjectProxy, ReadonlyObjectProxy) diff --git a/observ/watcher.py b/observ/watcher.py index df09961..bbc12ac 100644 --- a/observ/watcher.py +++ b/observ/watcher.py @@ -89,9 +89,10 @@ def traverse(obj, seen=None): return val_iter = iter(obj) else: - val_iter = [getattr(obj, attr) for attr in get_object_attrs(obj)] - if not val_iter: + obj_attrs = get_object_attrs(obj) + if not obj_attrs: return + val_iter = (getattr(obj, attr) for attr in obj_attrs) # track which objects we have already seen to support(!) full traversal # of datastructures with cycles diff --git a/poetry.lock b/poetry.lock index aaad77d..6f714bb 100644 --- a/poetry.lock +++ b/poetry.lock @@ -579,6 +579,52 @@ files = [ [package.dependencies] setuptools = "*" +[[package]] +name = "numpy" +version = "1.26.3" +description = "Fundamental package for array computing in Python" +category = "dev" +optional = false +python-versions = ">=3.9" +files = [ + {file = "numpy-1.26.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:806dd64230dbbfaca8a27faa64e2f414bf1c6622ab78cc4264f7f5f028fee3bf"}, + {file = "numpy-1.26.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02f98011ba4ab17f46f80f7f8f1c291ee7d855fcef0a5a98db80767a468c85cd"}, + {file = "numpy-1.26.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6d45b3ec2faed4baca41c76617fcdcfa4f684ff7a151ce6fc78ad3b6e85af0a6"}, + {file = "numpy-1.26.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bdd2b45bf079d9ad90377048e2747a0c82351989a2165821f0c96831b4a2a54b"}, + {file = "numpy-1.26.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:211ddd1e94817ed2d175b60b6374120244a4dd2287f4ece45d49228b4d529178"}, + {file = "numpy-1.26.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b1240f767f69d7c4c8a29adde2310b871153df9b26b5cb2b54a561ac85146485"}, + {file = "numpy-1.26.3-cp310-cp310-win32.whl", hash = "sha256:21a9484e75ad018974a2fdaa216524d64ed4212e418e0a551a2d83403b0531d3"}, + {file = "numpy-1.26.3-cp310-cp310-win_amd64.whl", hash = "sha256:9e1591f6ae98bcfac2a4bbf9221c0b92ab49762228f38287f6eeb5f3f55905ce"}, + {file = "numpy-1.26.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b831295e5472954104ecb46cd98c08b98b49c69fdb7040483aff799a755a7374"}, + {file = "numpy-1.26.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9e87562b91f68dd8b1c39149d0323b42e0082db7ddb8e934ab4c292094d575d6"}, + {file = "numpy-1.26.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c66d6fec467e8c0f975818c1796d25c53521124b7cfb760114be0abad53a0a2"}, + {file = "numpy-1.26.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f25e2811a9c932e43943a2615e65fc487a0b6b49218899e62e426e7f0a57eeda"}, + {file = "numpy-1.26.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:af36e0aa45e25c9f57bf684b1175e59ea05d9a7d3e8e87b7ae1a1da246f2767e"}, + {file = "numpy-1.26.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:51c7f1b344f302067b02e0f5b5d2daa9ed4a721cf49f070280ac202738ea7f00"}, + {file = "numpy-1.26.3-cp311-cp311-win32.whl", hash = "sha256:7ca4f24341df071877849eb2034948459ce3a07915c2734f1abb4018d9c49d7b"}, + {file = "numpy-1.26.3-cp311-cp311-win_amd64.whl", hash = "sha256:39763aee6dfdd4878032361b30b2b12593fb445ddb66bbac802e2113eb8a6ac4"}, + {file = "numpy-1.26.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:a7081fd19a6d573e1a05e600c82a1c421011db7935ed0d5c483e9dd96b99cf13"}, + {file = "numpy-1.26.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:12c70ac274b32bc00c7f61b515126c9205323703abb99cd41836e8125ea0043e"}, + {file = "numpy-1.26.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7f784e13e598e9594750b2ef6729bcd5a47f6cfe4a12cca13def35e06d8163e3"}, + {file = "numpy-1.26.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5f24750ef94d56ce6e33e4019a8a4d68cfdb1ef661a52cdaee628a56d2437419"}, + {file = "numpy-1.26.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:77810ef29e0fb1d289d225cabb9ee6cf4d11978a00bb99f7f8ec2132a84e0166"}, + {file = "numpy-1.26.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8ed07a90f5450d99dad60d3799f9c03c6566709bd53b497eb9ccad9a55867f36"}, + {file = "numpy-1.26.3-cp312-cp312-win32.whl", hash = "sha256:f73497e8c38295aaa4741bdfa4fda1a5aedda5473074369eca10626835445511"}, + {file = "numpy-1.26.3-cp312-cp312-win_amd64.whl", hash = "sha256:da4b0c6c699a0ad73c810736303f7fbae483bcb012e38d7eb06a5e3b432c981b"}, + {file = "numpy-1.26.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1666f634cb3c80ccbd77ec97bc17337718f56d6658acf5d3b906ca03e90ce87f"}, + {file = "numpy-1.26.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:18c3319a7d39b2c6a9e3bb75aab2304ab79a811ac0168a671a62e6346c29b03f"}, + {file = "numpy-1.26.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b7e807d6888da0db6e7e75838444d62495e2b588b99e90dd80c3459594e857b"}, + {file = "numpy-1.26.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b4d362e17bcb0011738c2d83e0a65ea8ce627057b2fdda37678f4374a382a137"}, + {file = "numpy-1.26.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b8c275f0ae90069496068c714387b4a0eba5d531aace269559ff2b43655edd58"}, + {file = "numpy-1.26.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:cc0743f0302b94f397a4a65a660d4cd24267439eb16493fb3caad2e4389bccbb"}, + {file = "numpy-1.26.3-cp39-cp39-win32.whl", hash = "sha256:9bc6d1a7f8cedd519c4b7b1156d98e051b726bf160715b769106661d567b3f03"}, + {file = "numpy-1.26.3-cp39-cp39-win_amd64.whl", hash = "sha256:867e3644e208c8922a3be26fc6bbf112a035f50f0a86497f98f228c50c607bb2"}, + {file = "numpy-1.26.3-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:3c67423b3703f8fbd90f5adaa37f85b5794d3366948efe9a5190a5f3a83fc34e"}, + {file = "numpy-1.26.3-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46f47ee566d98849323f01b349d58f2557f02167ee301e5e28809a8c0e27a2d0"}, + {file = "numpy-1.26.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a8474703bffc65ca15853d5fd4d06b18138ae90c17c8d12169968e998e448bb5"}, + {file = "numpy-1.26.3.tar.gz", hash = "sha256:697df43e2b6310ecc9d95f05d5ef20eacc09c7c4ecc9da3f235d39e71b7da1e4"}, +] + [[package]] name = "packaging" version = "23.2" @@ -1217,4 +1263,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p [metadata] lock-version = "2.0" python-versions = ">=3.9" -content-hash = "c73ff10353a6247b7acdd0e2566ac3fce78755d79bcbd3bf3ae4c694398a5fff" +content-hash = "b5c350a3e45093a757d0c55ef889afffcad01b1ddcb6042c174a19684e01db8d" diff --git a/pyproject.toml b/pyproject.toml index ea38373..bd07d43 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,6 +32,13 @@ PySide6 = { version = ">=6.6", python = "<3.13" } pytest-qt = "*" pytest-xvfb = "*" +[tool.poetry.group.numpy] +optional = true + +[tool.poetry.group.numpy.dependencies] +numpy = "*" + + [tool.ruff.lint] select = [ "E4", diff --git a/tests/test_object_proxy.py b/tests/test_object_proxy.py index 0f851f6..48078ee 100644 --- a/tests/test_object_proxy.py +++ b/tests/test_object_proxy.py @@ -1,10 +1,19 @@ import pytest +from observ import reactive from observ.object_proxy import ObjectProxy, ReadonlyObjectProxy from observ.object_utils import get_class_slots, get_object_attrs from observ.proxy import proxy from observ.traps import ReadonlyError +try: + import numpy as np + + has_numpy = True +except ImportError: + has_numpy = False +numpy_missing_reason = "Numpy is not installed" + class A: __slots__ = ("bar",) @@ -81,3 +90,11 @@ def test_readonly_proxy(): delattr(another_proxied, "baz") assert not hasattr(proxied, "baz") + + +@pytest.mark.skipif(not has_numpy, reason=numpy_missing_reason) +def test_usage_numpy(): + arr = np.eye(4) + proxy = reactive(arr) + # not supported + assert not isinstance(proxy, ObjectProxy) diff --git a/tests/test_qt.py b/tests/test_qt.py index 9f4131e..29328dd 100644 --- a/tests/test_qt.py +++ b/tests/test_qt.py @@ -7,6 +7,7 @@ try: from PySide6 import QtAsyncio, QtWidgets + from pytestqt.qt_compat import qt_api has_qt = True except ImportError: @@ -31,8 +32,6 @@ def qtasyncio(): @pytest.fixture def qapp(qapp_args, qapp_cls, pytestconfig): # workaround for https://bugreports.qt.io/browse/PYSIDE-2575 - from pytestqt.qt_compat import qt_api - app = qt_api.QtWidgets.QApplication.instance() if app is None: _qapp_instance = qapp_cls(qapp_args) From e1c2ef0cd464613d920e5da88adb60063374d6a4 Mon Sep 17 00:00:00 2001 From: Korijn van Golen Date: Mon, 8 Jan 2024 14:03:17 +0100 Subject: [PATCH 14/16] remove some todos --- observ/object_proxy.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/observ/object_proxy.py b/observ/object_proxy.py index 83de537..886c70c 100644 --- a/observ/object_proxy.py +++ b/observ/object_proxy.py @@ -51,7 +51,6 @@ def __setattr__(self, name, value): # the set attr is not stateful (e.g. someone # is attaching a bound method) # so no need to track this modification - # TODO: we could cache this? return retval new_value = getattr(target, name, None) @@ -87,8 +86,8 @@ def passthrough(method): def trap(self, *args, **kwargs): fn = getattr(self.__target__, method, None) if fn is None: - # TODO: we could cache this - # but it is possible a class is dynamically modified later + # we don't cache this + # since it is possible a class is dynamically modified later # invalidating the cached result... raise TypeError(f"object of type '{type(self)}' has no {method}") return fn(*args, **kwargs) From a47c957d8f0b62d2385bcc0c35d99d43cb0f4634 Mon Sep 17 00:00:00 2001 From: Korijn van Golen Date: Mon, 8 Jan 2024 14:08:33 +0100 Subject: [PATCH 15/16] document to_raw limitations in unit test --- observ/proxy.py | 2 -- tests/test_object_proxy.py | 26 +++++++++++++++++++++++++- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/observ/proxy.py b/observ/proxy.py index 55bf3e6..f7eb938 100644 --- a/observ/proxy.py +++ b/observ/proxy.py @@ -122,6 +122,4 @@ def to_raw(target: Proxy[T] | T) -> T: if isinstance(target, set): return cast(T, {to_raw(t) for t in target}) - # TODO: I don't think we can implement to_raw for objects - return target diff --git a/tests/test_object_proxy.py b/tests/test_object_proxy.py index 48078ee..b45ee08 100644 --- a/tests/test_object_proxy.py +++ b/tests/test_object_proxy.py @@ -1,6 +1,6 @@ import pytest -from observ import reactive +from observ import reactive, to_raw from observ.object_proxy import ObjectProxy, ReadonlyObjectProxy from observ.object_utils import get_class_slots, get_object_attrs from observ.proxy import proxy @@ -98,3 +98,27 @@ def test_usage_numpy(): proxy = reactive(arr) # not supported assert not isinstance(proxy, ObjectProxy) + + +def test_toraw(): + """Demonstrate the supported and unsupported to_raw code paths""" + obj = B(bar=5) + proxy = ObjectProxy(obj) + also_obj = to_raw(proxy) + assert obj is also_obj and obj is not proxy + + tree = B(bar=obj) + tree_proxy = ObjectProxy(tree) + also_tree = to_raw(tree_proxy) + assert tree is also_tree and tree is not tree_proxy + assert also_tree.bar is obj + + tree = B(bar=proxy) + tree_proxy = ObjectProxy(tree) + also_tree = to_raw(tree_proxy) + assert tree is also_tree and tree is not tree_proxy + + # since to_raw cannot copy/instantiate custom objects + # any proxies assigned to object attributes will + # not be processed recursively by to_raw + assert also_tree.bar is not obj From 5cf13febbd884a6f1d74e0432c2704acd4cf359e Mon Sep 17 00:00:00 2001 From: Korijn van Golen Date: Tue, 9 Jan 2024 13:57:16 +0100 Subject: [PATCH 16/16] add test for attribute and type errors --- tests/test_object_proxy.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/tests/test_object_proxy.py b/tests/test_object_proxy.py index b45ee08..14fae65 100644 --- a/tests/test_object_proxy.py +++ b/tests/test_object_proxy.py @@ -40,6 +40,8 @@ class D: def __init__(self, bar=5): self.bar = bar + i_am_none = None + def test_get_class_slots(): assert get_class_slots(C) == {"bar", "quux"} @@ -122,3 +124,24 @@ def test_toraw(): # any proxies assigned to object attributes will # not be processed recursively by to_raw assert also_tree.bar is not obj + + +def test_typeerror(): + obj = D() + proxy = reactive(obj) + assert isinstance(proxy, ObjectProxy) + + assert obj.i_am_none is None + assert proxy.i_am_none is None + + with pytest.raises(AttributeError): + obj.doesnt_exist() + + with pytest.raises(AttributeError): + proxy.doesnt_exist() + + with pytest.raises(TypeError): + iter(obj) + + with pytest.raises(TypeError): + iter(proxy)