Skip to content

Added AliasProperty and tests #10

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 2 additions & 9 deletions pydispatch/dispatch.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,19 +83,12 @@ class Foo(Dispatcher):
__initialized_subclasses = set()
__skip_initialized = True
def __new__(cls, *args, **kwargs):
def iter_bases(_cls):
if _cls is not object:
yield _cls
for b in _cls.__bases__:
for _cls_ in iter_bases(b):
yield _cls_
skip_initialized = Dispatcher._Dispatcher__skip_initialized
if not skip_initialized or cls not in Dispatcher._Dispatcher__initialized_subclasses:
props = {}
events = set()
for _cls in iter_bases(cls):
for attr in dir(_cls):
prop = getattr(_cls, attr)
for _cls in cls.__mro__:
for attr, prop in _cls.__dict__.items():
if attr not in props and isinstance(prop, Property):
props[attr] = prop
prop.name = attr
Expand Down
93 changes: 92 additions & 1 deletion pydispatch/properties.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@ def on_foo_value(self, instance, value, **kwargs):

PY2 = sys.version_info < (3,)

__all__ = ['Property', 'ListProperty', 'DictProperty']
__all__ = ['Property', 'AliasProperty', 'ListProperty', 'DictProperty']


class Property(object):
"""Defined on the class level to create an observable attribute
Expand All @@ -56,36 +57,44 @@ class Property(object):
:class:`~pydispatch.dispatch.Dispatcher` instance.

"""

def __init__(self, default=None):
self._name = ''
self.default = default
self.__storage = {}
self.__weakrefs = InformativeWVDict(del_callback=self._on_weakref_fin)

@property
def name(self):
return self._name

@name.setter
def name(self, value):
if self._name != '':
return
self._name = value

def _add_instance(self, obj, default=None):
if default is None:
default = self.default
self.__storage[id(obj)] = self.default
self.__weakrefs[id(obj)] = obj

def _del_instance(self, obj):
del self.__storage[id(obj)]

def _on_weakref_fin(self, obj_id):
if obj_id in self.__storage:
del self.__storage[obj_id]

def __get__(self, obj, objcls=None):
if obj is None:
return self
obj_id = id(obj)
if obj_id not in self.__storage:
self._add_instance(obj)
return self.__storage[obj_id]

def __set__(self, obj, value):
obj_id = id(obj)
if obj_id not in self.__storage:
Expand All @@ -95,6 +104,7 @@ def __set__(self, obj, value):
return
self.__storage[obj_id] = value
self._on_change(obj, current, value)

def _on_change(self, obj, old, value, **kwargs):
"""Called internally to emit changes from the instance object

Expand All @@ -114,11 +124,58 @@ def _on_change(self, obj, old, value, **kwargs):
"""
kwargs['property'] = self
obj.emit(self.name, obj, value, old=old, **kwargs)

def __repr__(self):
return '<{}: {}>'.format(self.__class__, self)

def __str__(self):
return self.name


class AliasProperty(Property):
"""Property with a getter method and optional setter method. Behaves similar to Pythons builtin properties.

Args:
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add a description for bind here?

Copy link
Author

@leonwilly leonwilly Dec 12, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes

getter : method used to provide the property value
setter (Optional): method used to set the property value. If this method returns False change events will
not be emitted.
"""

def __init__(self, getter, setter=None, bind=None):
super().__init__()
self.__getter = getter
self.__setter = setter
self.__bindings = dict((prop, self._on_change) for prop in bind) if bind is not None else {}

def _on_change(self, obj, *args, **kwargs):
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm wondering if it would be better to use a separate callback for bound properties instead of overriding _on_change.

Seems like there could be a lot of added method calls with all of the extra bindings. It would also be cleaner without having to inspect the source Property, calls to super(), etc.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree I originally added an _trigger_change callback. But I didn't want to add to much bloat. I wanted to keep additions limited.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think an additional method specific to AliasProperty would be just fine. Doesn't really add bloat and would make intent much clearer.

Maybe something like _on_bound_prop_change? I'm not the best at naming things, but you get the idea.

property = kwargs.get('property', None)
if property is None:
return super()._on_change(obj, *args, **kwargs)
old = super().__get__(obj)
value = self.__get__(obj)
if old != value:
super()._on_change(obj, old, value)

def _add_instance(self, obj, default=None):
super()._add_instance(obj, default)
obj.bind(**self.__bindings)

def __get__(self, obj, objcls=None):
if obj is None:
return self
value = self._Property__storage[id(obj)] = self.__getter(obj)
return value

def __set__(self, obj, value):
current = self.__getter(obj)
if current == value:
return
if self.__setter is None:
raise AttributeError("can't set attribute")
if self.__setter(obj, value) is None:
super().__set__(obj, value)


class ListProperty(Property):
"""Property with a :class:`list` type value

Expand All @@ -134,18 +191,22 @@ class ListProperty(Property):
Changes to the contents of the list are able to be observed through
:class:`ObservableList`.
"""

def __init__(self, default=None, copy_on_change=False):
if default is None:
default = []
self.copy_on_change = copy_on_change
super(ListProperty, self).__init__(default)

def _add_instance(self, obj):
default = self.default[:]
default = ObservableList(default, obj=obj, property=self)
super(ListProperty, self)._add_instance(obj, default)

def __set__(self, obj, value):
value = ObservableList(value, obj=obj, property=self)
super(ListProperty, self).__set__(obj, value)

def __get__(self, obj, objcls=None):
if obj is None:
return self
Expand All @@ -155,6 +216,7 @@ def __get__(self, obj, objcls=None):
self._Property__storage[id(obj)] = value
return value


class DictProperty(Property):
"""Property with a :class:`dict` type value

Expand All @@ -170,18 +232,22 @@ class DictProperty(Property):
Changes to the contents of the dict are able to be observed through
:class:`ObservableDict`.
"""

def __init__(self, default=None, copy_on_change=False):
if default is None:
default = {}
self.copy_on_change = copy_on_change
super(DictProperty, self).__init__(default)

def _add_instance(self, obj):
default = self.default.copy()
default = ObservableDict(default, obj=obj, property=self)
super(DictProperty, self)._add_instance(obj, default)

def __set__(self, obj, value):
value = ObservableDict(value, obj=obj, property=self)
super(DictProperty, self).__set__(obj, value)

def __get__(self, obj, objcls=None):
if obj is None:
return self
Expand All @@ -191,6 +257,7 @@ def __get__(self, obj, objcls=None):
self._Property__storage[id(obj)] = value
return value


class Observable(object):
"""Mixin used by :class:`ObservableList` and :class:`ObservableDict`
to emit changes and build other observables
Expand All @@ -202,19 +269,22 @@ class Observable(object):
copied and replaced by another :class:`ObservableDict`. This allows nested
containers to be observed and their changes to be tracked.
"""

def _build_observable(self, item):
if isinstance(item, list):
item = ObservableList(item, parent=self)
elif isinstance(item, dict):
item = ObservableDict(item, parent=self)
return item

def _get_copy_or_none(self):
p = self.parent_observable
if p is not None:
return p._get_copy_or_none()
if not self.copy_on_change:
return None
return self._deepcopy()

def _deepcopy(self):
o = self.copy()
if isinstance(self, list):
Expand All @@ -225,6 +295,7 @@ def _deepcopy(self):
if isinstance(item, Observable):
o[key] = item._deepcopy()
return o

def _emit_change(self, **kwargs):
if not self._init_complete:
return
Expand All @@ -235,12 +306,14 @@ def _emit_change(self, **kwargs):
return
self.property._on_change(self.obj, old, self, **kwargs)


class ObservableList(list, Observable):
"""A :class:`list` subclass that tracks changes to its contents

Note:
This class is for internal use and not intended to be used directly
"""

def __init__(self, initlist=None, **kwargs):
self._init_complete = False
super(ObservableList, self).__init__()
Expand All @@ -254,20 +327,24 @@ def __init__(self, initlist=None, **kwargs):
if initlist is not None:
self.extend(initlist)
self._init_complete = True

def __setitem__(self, key, item):
old = self._get_copy_or_none()
item = self._build_observable(item)
super(ObservableList, self).__setitem__(key, item)
self._emit_change(keys=[key], old=old)

def __delitem__(self, key):
old = self._get_copy_or_none()
super(ObservableList, self).__delitem__(key)
self._emit_change(old=old)

if PY2:
def __setslice__(self, *args):
old = self._get_copy_or_none()
super(ObservableList, self).__setslice__(*args)
self._emit_change(old=old)

def __delslice__(self, *args):
old = self._get_copy_or_none()
super(ObservableList, self).__delslice__(*args)
Expand All @@ -280,15 +357,18 @@ def clear(self):
if not hasattr(list, 'copy'):
def copy(self):
return self[:]

def __iadd__(self, other):
other = self._build_observable(other)
self.extend(other)
return self

def append(self, item):
old = self._get_copy_or_none()
item = self._build_observable(item)
super(ObservableList, self).append(item)
self._emit_change(old=old)

def extend(self, other):
old = self._get_copy_or_none()
init = self._init_complete
Expand All @@ -298,17 +378,20 @@ def extend(self, other):
if init:
self._init_complete = True
self._emit_change(old=old)

def remove(self, *args):
old = self._get_copy_or_none()
super(ObservableList, self).remove(*args)
self._emit_change(old=old)


class ObservableDict(dict, Observable):
"""A :class:`dict` subclass that tracks changes to its contents

Note:
This class is for internal use and not intended to be used directly
"""

def __init__(self, initdict=None, **kwargs):
self._init_complete = False
super(ObservableDict, self).__init__()
Expand All @@ -322,15 +405,18 @@ def __init__(self, initdict=None, **kwargs):
if initdict is not None:
self.update(initdict)
self._init_complete = True

def __setitem__(self, key, item):
old = self._get_copy_or_none()
item = self._build_observable(item)
super(ObservableDict, self).__setitem__(key, item)
self._emit_change(keys=[key], old=old)

def __delitem__(self, key):
old = self._get_copy_or_none()
super(ObservableDict, self).__delitem__(key)
self._emit_change(old=old)

def update(self, other):
old = self._get_copy_or_none()
init = self._init_complete
Expand All @@ -344,15 +430,20 @@ def update(self, other):
if init:
self._init_complete = True
self._emit_change(keys=list(keys), old=old)

def clear(self):
old = self._get_copy_or_none()
super(ObservableDict, self).clear()
self._emit_change(old=old)

def pop(self, *args):
old = self._get_copy_or_none()
super(ObservableDict, self).pop(*args)
self._emit_change(old=old)

def setdefault(self, *args):
old = self._get_copy_or_none()
super(ObservableDict, self).setdefault(*args)
self._emit_change(old=old)


Loading