diff --git a/pydispatch/dispatch.py b/pydispatch/dispatch.py index 84174e1..9c942ec 100644 --- a/pydispatch/dispatch.py +++ b/pydispatch/dispatch.py @@ -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 diff --git a/pydispatch/properties.py b/pydispatch/properties.py index 603c049..152d94f 100644 --- a/pydispatch/properties.py +++ b/pydispatch/properties.py @@ -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 @@ -56,29 +57,36 @@ 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 @@ -86,6 +94,7 @@ def __get__(self, obj, objcls=None): 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: @@ -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 @@ -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: + 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): + 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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -202,12 +269,14 @@ 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: @@ -215,6 +284,7 @@ def _get_copy_or_none(self): if not self.copy_on_change: return None return self._deepcopy() + def _deepcopy(self): o = self.copy() if isinstance(self, list): @@ -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 @@ -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__() @@ -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) @@ -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 @@ -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__() @@ -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 @@ -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) + + diff --git a/tests/test_properties.py b/tests/test_properties.py index 739806e..b782b07 100644 --- a/tests/test_properties.py +++ b/tests/test_properties.py @@ -1,10 +1,15 @@ - def test_properties(listener): - from pydispatch import Dispatcher, Property + from pydispatch import Dispatcher, Property, AliasProperty class A(Dispatcher): test_prop = Property('default') name = Property('') something = Property() + + def get_name_something(self): + return '{} {}'.format(self.name, self.something) + + name_something = AliasProperty(get_name_something, bind=('name', 'something')) + def __init__(self, name): self.name = name self.something = 'stuff' @@ -12,6 +17,7 @@ def __init__(self, name): a = A('foo') assert a.something == 'stuff' assert a.name == 'foo' + assert a.name_something == 'foo stuff' a.bind(test_prop=listener.on_prop) assert 'test_prop' in a._Dispatcher__property_events @@ -23,11 +29,12 @@ def __init__(self, name): a.test_prop = 'b' assert listener.property_events == ['a', 'b'] + def test_container_properties(listener): from pydispatch import Dispatcher, ListProperty, DictProperty class A(Dispatcher): - test_dict = DictProperty({'defaultkey':'defaultval'}) + test_dict = DictProperty({'defaultkey': 'defaultval'}) test_list = ListProperty(['defaultitem']) a = A() @@ -36,17 +43,17 @@ class A(Dispatcher): assert a.test_dict['defaultkey'] == 'defaultval' assert a.test_list[0] == 'defaultitem' - a.test_dict = {'changed':True} + a.test_dict = {'changed': True} a.test_list = ['changed'] assert a.test_dict['changed'] is True and len(a.test_dict) == 1 assert a.test_list[0] == 'changed' and len(a.test_list) == 1 - assert listener.property_events == [{'changed':True}, ['changed']] + assert listener.property_events == [{'changed': True}, ['changed']] listener.property_events = [] listener.property_event_kwargs = [] - a.test_dict['nested_dict'] = {'foo':'bar'} + a.test_dict['nested_dict'] = {'foo': 'bar'} a.test_dict['nested_dict']['foo'] = 'baz' a.test_dict['nested_dict']['nested_list'] = [0] a.test_dict['nested_dict']['nested_list'].append(1) @@ -57,12 +64,12 @@ class A(Dispatcher): listener.property_events = [] - a.test_list.append({'nested_dict':{'foo':'bar'}}) + a.test_list.append({'nested_dict': {'foo': 'bar'}}) d = a.test_list[-1] d['nested_dict']['foo'] = 'baz' assert len(listener.property_events) == 2 - assert a.test_list[-1] == {'nested_dict':{'foo':'baz'}} + assert a.test_list[-1] == {'nested_dict': {'foo': 'baz'}} listener.property_events = [] del a.test_list[:] @@ -76,6 +83,7 @@ class A(Dispatcher): a.test_list.append(True) assert len(listener.property_events) == 3 + def test_list_property_ops(listener): from pydispatch import Dispatcher, ListProperty @@ -119,11 +127,12 @@ class A(Dispatcher): assert a.test_list == ['a', 'b', 'c', 'd', 'e', 'f', 'g'] assert len(listener.property_events) == 2 + def test_dict_property_ops(listener): from pydispatch import Dispatcher, DictProperty class A(Dispatcher): - test_dict = DictProperty({'a':1, 'b':2, 'c':3, 'd':4}) + test_dict = DictProperty({'a': 1, 'b': 2, 'c': 3, 'd': 4}) a = A() a.bind(test_dict=listener.on_prop) @@ -139,9 +148,9 @@ class A(Dispatcher): listener.property_events = [] listener.property_event_kwargs = [] - a.test_dict.update({'c':3, 'e':5, 'f':6, 'g':7}) + a.test_dict.update({'c': 3, 'e': 5, 'f': 6, 'g': 7}) assert len(listener.property_events) == 1 - assert a.test_dict == {'c':3, 'd':4, 'e':5, 'f':6, 'g':7} + assert a.test_dict == {'c': 3, 'd': 4, 'e': 5, 'f': 6, 'g': 7} assert sorted(listener.property_event_kwargs[0]['keys']) == ['e', 'f', 'g'] listener.property_events = [] @@ -154,6 +163,7 @@ class A(Dispatcher): assert len(listener.property_events) == 1 assert a.test_dict['foo'] == 'bar' + def test_empty_defaults(listener): from pydispatch import Dispatcher, ListProperty, DictProperty from pydispatch.properties import ObservableList, ObservableDict @@ -176,10 +186,12 @@ class A(Dispatcher): assert len(listener.property_events) == 2 + def test_unbind(listener): from pydispatch import Dispatcher, Property class A(Dispatcher): test_prop = Property() + a = A() a.bind(test_prop=listener.on_prop) @@ -205,12 +217,14 @@ class A(Dispatcher): a.test_prop = 4 assert len(listener.property_events) == 0 + def test_removal(): from pydispatch import Dispatcher, Property class Listener(object): def __init__(self): self.property_events = [] + def on_prop(self, obj, value, **kwargs): self.property_events.append(value) @@ -235,6 +249,7 @@ class A(Dispatcher): assert len(prop._Property__weakrefs) == 0 assert len(prop._Property__storage) == 0 + def test_self_binding(): from pydispatch import Dispatcher, Property, ListProperty, DictProperty @@ -242,6 +257,7 @@ class A(Dispatcher): test_prop = Property() test_dict = DictProperty() test_list = ListProperty() + def __init__(self): self.received = [] self.bind( @@ -249,10 +265,13 @@ def __init__(self): test_dict=self.on_test_dict, test_list=self.on_test_list, ) + def on_test_prop(self, *args, **kwargs): self.received.append('test_prop') + def on_test_dict(self, *args, **kwargs): self.received.append('test_dict') + def on_test_list(self, *args, **kwargs): self.received.append('test_list') @@ -264,6 +283,7 @@ def on_test_list(self, *args, **kwargs): assert a.received == ['test_prop', 'test_dict', 'test_list'] + def test_emission_lock(listener): from pydispatch import Dispatcher, Property, ListProperty, DictProperty @@ -279,7 +299,7 @@ class A(Dispatcher): a.test_prop = 'foo' a.test_list = [-1] * 4 - a.test_dict = {'a':0, 'b':1, 'c':2, 'd':3} + a.test_dict = {'a': 0, 'b': 1, 'c': 2, 'd': 3} assert len(listener.property_events) == 3 listener.property_events = [] @@ -320,7 +340,7 @@ class A(Dispatcher): assert listener.property_event_kwargs[1]['property'].name == 'test_list' assert listener.property_events[1][0] == 'a' assert listener.property_event_kwargs[2]['property'].name == 'test_dict' - assert listener.property_events[2] == {k:i for k in a.test_dict.keys()} + assert listener.property_events[2] == {k: i for k in a.test_dict.keys()} listener.property_events = [] listener.property_event_kwargs = [] @@ -341,6 +361,7 @@ class A(Dispatcher): assert listener.property_event_kwargs[2]['property'].name == 'test_prop' assert listener.property_events[2] == i + def test_copy_on_change(listener): from pydispatch import Dispatcher, ListProperty, DictProperty @@ -360,22 +381,22 @@ class A(Dispatcher): assert listener.property_event_kwargs[0]['old'] == {} a.test_dict['foo'] = None - assert listener.property_event_kwargs[1]['old'] == {'foo':'bar'} + assert listener.property_event_kwargs[1]['old'] == {'foo': 'bar'} - a.test_dict['nested_dict'] = {'a':1} - assert listener.property_event_kwargs[2]['old'] == {'foo':None} + a.test_dict['nested_dict'] = {'a': 1} + assert listener.property_event_kwargs[2]['old'] == {'foo': None} a.test_dict['nested_dict']['b'] = 2 - assert listener.property_event_kwargs[3]['old'] == {'foo':None, 'nested_dict':{'a':1}} + assert listener.property_event_kwargs[3]['old'] == {'foo': None, 'nested_dict': {'a': 1}} a.test_dict['nested_list'] = ['a', 'b'] assert listener.property_event_kwargs[4]['old'] == { - 'foo':None, 'nested_dict':{'a':1, 'b':2} + 'foo': None, 'nested_dict': {'a': 1, 'b': 2} } a.test_dict['nested_list'].append('c') assert listener.property_event_kwargs[5]['old'] == { - 'foo':None, 'nested_dict':{'a':1, 'b':2}, 'nested_list':['a', 'b'] + 'foo': None, 'nested_dict': {'a': 1, 'b': 2}, 'nested_list': ['a', 'b'] } listener.property_event_kwargs = [] @@ -393,10 +414,10 @@ class A(Dispatcher): assert listener.property_event_kwargs[3]['old'] == [{}, 'b', 'c'] a.test_list[1] = [0] - assert listener.property_event_kwargs[4]['old'] == [{'foo':'bar'}, 'b', 'c'] + assert listener.property_event_kwargs[4]['old'] == [{'foo': 'bar'}, 'b', 'c'] a.test_list[1].append(1) - assert listener.property_event_kwargs[5]['old'] == [{'foo':'bar'}, [0], 'c'] + assert listener.property_event_kwargs[5]['old'] == [{'foo': 'bar'}, [0], 'c'] listener.property_event_kwargs = []