Source code for arcade.gui.property

from __future__ import annotations

import sys
import traceback
from typing import Any, Callable, Dict, Generic, List, Optional, TypeVar, cast
from weakref import WeakKeyDictionary, ref

from typing_extensions import Self, overload, override

P = TypeVar("P")


class _Obs(Generic[P]):
    """
    Internal holder for Property value and change listeners
    """

    __slots__ = ("value", "listeners")

    def __init__(self, value: P):
        self.value = value
        # This will keep any added listener even if it is not referenced anymore
        # and would be garbage collected
        self.listeners: set[Callable[[Any, P], Any] | Callable[[], Any]] = set()


[docs] class Property(Generic[P]): """An observable property which triggers observers when changed. .. code-block:: python def log_change(instance, value): print("Something changed") class MyObject: name = Property() my_obj = MyObject() bind(my_obj, "name", log_change) unbind(my_obj, "name", log_change) my_obj.name = "Hans" # > Something changed Properties provide a less verbose way to implement the observer pattern in comparison to using the `property` decorator. Args: default: Default value which is returned, if no value set before default_factory: A callable which returns the default value. Will be called with the property and the instance """ __slots__ = ("name", "default_factory", "obs") name: str """Attribute name of the property""" default_factory: Callable[[Any, Any], P] """Default factory to create the initial value""" obs: WeakKeyDictionary[Any, _Obs] """Weak dictionary to hold the value and listeners""" def __init__( self, default: Optional[P] = None, default_factory: Optional[Callable[[Any, Any], P]] = None, ): if default_factory is None: default_factory = lambda prop, instance: cast(P, default) self.default_factory = default_factory self.obs: WeakKeyDictionary[Any, _Obs] = WeakKeyDictionary() def _get_obs(self, instance) -> _Obs: obs = self.obs.get(instance) if obs is None: obs = _Obs(self.default_factory(self, instance)) self.obs[instance] = obs return obs
[docs] def get(self, instance) -> P: """Get value for owner instance""" obs = self._get_obs(instance) return obs.value
[docs] def set(self, instance, value): """Set value for owner instance""" obs = self._get_obs(instance) if obs.value != value: obs.value = value self.dispatch(instance, value)
[docs] def dispatch(self, instance, value): """Notifies every listener, which subscribed to the change event. Args: instance: Property instance value: new value to set """ obs = self._get_obs(instance) for listener in obs.listeners: try: try: # FIXME if listener() raises an error, the invalid call will be # also shown as an exception listener(instance, value) # type: ignore except TypeError: # If the listener does not accept arguments, we call it without it listener() # type: ignore except Exception: print( f"Change listener for {instance}.{self.name} = {value} raised an exception!", file=sys.stderr, ) traceback.print_exc()
[docs] def bind(self, instance, callback): """Binds a function to the change event of the property. A reference to the function will be kept. Args: instance: The instance to bind the callback to. callback: The callback to bind. """ obs = self._get_obs(instance) # Instance methods are bound methods, which can not be referenced by normal `ref()` # if listeners would be a WeakSet, we would have to add listeners as WeakMethod # ourselves into `WeakSet.data`. obs.listeners.add(callback)
[docs] def unbind(self, instance, callback): """Unbinds a function from the change event of the property. Args: instance: The target instance. callback: The callback to unbind. """ obs = self._get_obs(instance) obs.listeners.remove(callback)
def __set_name__(self, owner, name): self.name = name @overload def __get__(self, instance: None, instance_type) -> Self: ... @overload def __get__(self, instance: Any, instance_type) -> P: ... def __get__(self, instance: Any | None, instance_type) -> Self | P: if instance is None: return self return self.get(instance) def __set__(self, instance, value: P): self.set(instance, value)
[docs] def bind(instance, property: str, callback): """Bind a function to the change event of the property. A reference to the function will be kept, so that it will be still invoked even if it would normally have been garbage collected: .. code-block:: python def log_change(instance, value): print(f"Value of {instance} changed to {value}") class MyObject: name = Property() my_obj = MyObject() bind(my_obj, "name", log_change) my_obj.name = "Hans" # > Value of <__main__.MyObject ...> changed to Hans Args: instance: Instance owning the property property: Name of the property callback: Function to call Returns: None """ t = type(instance) prop = getattr(t, property) if isinstance(prop, Property): prop.bind(instance, callback)
[docs] def unbind(instance, property: str, callback): """Unbinds a function from the change event of the property. .. code-block:: python def log_change(instance, value): print("Something changed") class MyObject: name = Property() my_obj = MyObject() bind(my_obj, "name", log_change) unbind(my_obj, "name", log_change) my_obj.name = "Hans" # > Something changed Args: instance: Instance owning the property property: Name of the property callback: Function to unbind Returns: None """ t = type(instance) prop = getattr(t, property) if isinstance(prop, Property): prop.unbind(instance, callback)
class _ObservableDict(dict): """Internal class to observe changes inside a native python dict.""" __slots__ = ("prop", "obj") def __init__(self, prop: Property, instance, *args): self.prop: Property = prop self.obj = ref(instance) super().__init__(*args) def dispatch(self): self.prop.dispatch(self.obj(), self) @override def __setitem__(self, key, value): dict.__setitem__(self, key, value) self.dispatch() @override def __delitem__(self, key): dict.__delitem__(self, key) self.dispatch() @override def clear(self): dict.clear(self) self.dispatch() @override def pop(self, *args): result = dict.pop(self, *args) self.dispatch() return result @override def popitem(self): result = dict.popitem(self) self.dispatch() return result @override def setdefault(self, *args): dict.setdefault(self, *args) self.dispatch() @override def update(self, *args): dict.update(self, *args) self.dispatch() K = TypeVar("K") V = TypeVar("V")
[docs] class DictProperty(Property[Dict[K, V]], Generic[K, V]): """Property that represents a dict. Only dict are allowed. Any other classes are forbidden. """ def __init__(self): super().__init__(default_factory=_ObservableDict)
[docs] @override def set(self, instance, value: dict): """Set value for owner instance, wraps the dict into an observable dict.""" value = _ObservableDict(self, instance, value) super().set(instance, value)
class _ObservableList(list): """Internal class to observe changes inside a native python list. Args: prop: Property instance instance: Instance owning the property *args: List of arguments to pass to the list """ __slots__ = ("prop", "obj") def __init__(self, prop: Property, instance, *args): self.prop: Property = prop self.obj = ref(instance) super().__init__(*args) def dispatch(self): """Dispatches the change event.""" self.prop.dispatch(self.obj(), self) @override def __setitem__(self, key, value): list.__setitem__(self, key, value) self.dispatch() @override def __delitem__(self, key): list.__delitem__(self, key) self.dispatch() @override def __iadd__(self, *args): list.__iadd__(self, *args) self.dispatch() return self @override def __imul__(self, *args): list.__imul__(self, *args) self.dispatch() return self @override def append(self, *args): """Proxy for list.append() which dispatches the change event.""" list.append(self, *args) self.dispatch() @override def clear(self): """Proxy for list.clear() which dispatches the change event.""" list.clear(self) self.dispatch() @override def remove(self, *args): """Proxy for list.remove() which dispatches the change event.""" list.remove(self, *args) self.dispatch() @override def insert(self, *args): """Proxy for list.insert() which dispatches the change event.""" list.insert(self, *args) self.dispatch() @override def pop(self, *args): """Proxy for list.pop() which dispatches the change""" result = list.pop(self, *args) self.dispatch() return result @override def extend(self, *args): """Proxy for list.extend() which dispatches the change event.""" list.extend(self, *args) self.dispatch() @override def sort(self, **kwargs): """Proxy for list.sort() which dispatches the change event.""" list.sort(self, **kwargs) self.dispatch() @override def reverse(self): """Proxy for list.reverse() which dispatches the change event.""" list.reverse(self) self.dispatch()
[docs] class ListProperty(Property[List[P]], Generic[P]): """Property that represents a list. Only list are allowed. Any other classes are forbidden. """ def __init__(self): super().__init__(default_factory=_ObservableList)
[docs] @override def set(self, instance, value: list): """Set value for owner instance, wraps the list into an observable list.""" value = _ObservableList(self, instance, value) super().set(instance, value)