Skip to content

Commit

Permalink
Merge pull request #152 from 3ptscience/dev
Browse files Browse the repository at this point in the history
v0.3.3 Deployment - copy and equal
  • Loading branch information
fwkoch committed Mar 23, 2017
2 parents d52e2d4 + 0474512 commit 33a7fba
Show file tree
Hide file tree
Showing 13 changed files with 179 additions and 45 deletions.
2 changes: 1 addition & 1 deletion .bumpversion.cfg
@@ -1,4 +1,4 @@
[bumpversion]
current_version = 0.3.2
current_version = 0.3.3
files = properties/__init__.py setup.py docs/conf.py

4 changes: 2 additions & 2 deletions docs/conf.py
Expand Up @@ -66,9 +66,9 @@
# built documents.
#
# The short X.Y version.
version = u'0.3.2'
version = u'0.3.3'
# The full version, including alpha/beta/rc tags.
release = u'0.3.2'
release = u'0.3.3'

# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
Expand Down
16 changes: 14 additions & 2 deletions docs/content/hasproperties.rst
Expand Up @@ -4,11 +4,23 @@ HasProperties
=============

.. autoclass:: properties.HasProperties
:members: validate, serialize, deserialize, equal
:members: validate, serialize, deserialize

.. autoclass:: properties.base.PropertyMetaclass

.. rubric:: HasProperties Features
|
|
.. rubric:: Functions that act on HasProperties instances:

.. autofunction:: properties.copy

.. autofunction:: properties.equal

|
|
.. rubric:: HasProperties features:

* :ref:`documentation` - Classes are auto-documented with
a sphinx-style docstring.
Expand Down
10 changes: 9 additions & 1 deletion properties/__init__.py
Expand Up @@ -21,6 +21,8 @@ class Profile(properties.HasProperties):
Set,
Tuple,
Union,
copy,
equal,
)

from .basic import (
Expand Down Expand Up @@ -80,7 +82,13 @@ class Profile(properties.HasProperties):
undefined,
)

__version__ = '0.3.2'
__version__ = '0.3.3'
__author__ = '3point Science'
__license__ = 'MIT'
__copyright__ = 'Copyright 2017 3point Science,'

try:
del absolute_import, division, print_function, unicode_literals
except NameError:
# Error cleaning namespace
pass
77 changes: 67 additions & 10 deletions properties/base/base.py
Expand Up @@ -267,14 +267,20 @@ def _notify(self, change):
listener.func(self, change)

def _set(self, name, value):
prev = self._get(name)
prev = self._backend.get(name, utils.undefined)
change = dict(name=name, previous=prev, value=value, mode='validate')
self._notify(change)
if change['value'] is utils.undefined:
self._backend.pop(name, None)
else:
self._backend[name] = change['value']
if not self._props[name].equal(prev, change['value']):
if prev is utils.undefined and change['value'] is utils.undefined:
pass
elif(
prev is utils.undefined or
change['value'] is utils.undefined or
not self._props[name].equal(prev, change['value'])
):
change.update(name=name, previous=prev, mode='observe_change')
self._notify(change)
change.update(name=name, previous=prev, mode='observe_set')
Expand Down Expand Up @@ -436,12 +442,63 @@ def equal(self, other):
Equivalence is determined by checking if all Property values on
two instances are equal, using :code:`Property.equal`.
"""
if self is other:
return True
if not isinstance(other, self.__class__):
return False
for prop in itervalues(self._props):
if prop.equal(getattr(self, prop.name), getattr(other, prop.name)):
continue
return False
warn('HasProperties.equal has been depricated in favor of '
'properties.equal and will be removed in the next release',
FutureWarning)
return equal(self, other)


@utils.stop_recursion_with(False)
def equal(value_a, value_b):
"""Determine if two **HasProperties** instances are equivalent
Equivalence is determined by checking if (1) the two instances are
the same class and (2) all Property values on two instances are
equal, using :code:`Property.equal`. If the two values are the same
HasProperties instance (eg. :code:`value_a is value_b`) this method
returns True. Finally, if either value is not a HasProperties
instance, equality is simply checked with ==.
.. note::
HasProperties objects with recursive self-references will not
evaluate to equal, even if their property values and structure
are equivalent.
"""
if (
not isinstance(value_a, HasProperties) or
not isinstance(value_b, HasProperties)
):
return value_a == value_b
if value_a is value_b:
return True
if value_a.__class__ is not value_b.__class__:
return False
for prop in itervalues(value_a._props):
prop_a = getattr(value_a, prop.name)
prop_b = getattr(value_b, prop.name)
if prop_a is None and prop_b is None:
continue
if prop_a is None or prop_b is None:
return False
if prop.equal(getattr(value_a, prop.name),
getattr(value_b, prop.name)):
continue
return False
return True


def copy(value, **kwargs):
"""Return a copy of a **HasProperties** instance
A copy is produced by serializing the HasProperties instance then
deserializing it to a new instance. Therefore, if any properties
cannot be serialized/deserialized, :code:`copy` will fail. Any
keyword arguments will be passed through to both :code:`serialize`
and :code:`deserialize`.
"""

if not isinstance(value, HasProperties):
raise ValueError('properties.copy may only be used to copy'
'HasProperties instances')
return value.__class__.deserialize(value.serialize(**kwargs), **kwargs)
8 changes: 3 additions & 5 deletions properties/base/instance.py
Expand Up @@ -7,7 +7,7 @@
import json
from six import PY2

from .base import HasProperties
from .base import HasProperties, equal
from .. import basic
from .. import utils

Expand Down Expand Up @@ -143,9 +143,7 @@ def deserialize(self, value, **kwargs):
return self.from_json(value, **kwargs)

def equal(self, value_a, value_b):
if isinstance(value_a, HasProperties):
return value_a.equal(value_b)
return value_a is value_b
return equal(value_a, value_b)

@staticmethod
def to_json(value, **kwargs):
Expand All @@ -165,7 +163,7 @@ def to_json(value, **kwargs):
def from_json(value, **kwargs):
"""Instance properties cannot statically convert from JSON"""
raise TypeError("Instance properties cannot statically convert "
"values from JSON. 'eserialize' must be used on an "
"values from JSON. 'deserialize' must be used on an "
"instance of Instance Property instead, and if the "
"instance_class is not a HasProperties subclass a "
"custom deserializer must be registered")
Expand Down
15 changes: 12 additions & 3 deletions properties/basic.py
Expand Up @@ -247,8 +247,17 @@ def assert_valid(self, instance, value=None):
return True

def equal(self, value_a, value_b): #pylint: disable=no-self-use
"""Check if two valid Property values are equal"""
return value_a == value_b
"""Check if two valid Property values are equal
.. note::
This method assumes that :code:`None` and
:code:`properties.undefined` are never passed in as values
"""
equal = value_a == value_b
if hasattr(equal, '__iter__'):
return all(equal)
return equal

def get_property(self):
"""Establishes access of GettableProperty values"""
Expand Down Expand Up @@ -511,7 +520,7 @@ def del_func(self):
return getattr(self, '_del_func', None)

def get_property(self):
"""Establishes the dynamic behaviour of Property values"""
"""Establishes the dynamic behavior of Property values"""
scope = self

def fget(self):
Expand Down
12 changes: 8 additions & 4 deletions properties/handlers.py
Expand Up @@ -207,8 +207,10 @@ def callback_function(self, change):
This dictionary contains:
* 'name' - the name of the changed Property
* 'previous' - the value of the Property prior to change
* 'value' - the new value of the Property
* 'previous' - the value of the Property prior to change (this will be
:code:`properties.undefined` if the value was not previously set)
* 'value' - the new value of the Property (this will be
:code:`properties.undefined` if the value is deleted)
* 'mode' - the mode of the change; for observers, this is either
'observe_set' or 'observe_change'
Expand Down Expand Up @@ -274,8 +276,10 @@ def callback_function(self, change):
This dictionary contains:
* 'name' - the name of the changed Property
* 'previous' - the value of the Property prior to change
* 'value' - the new value of the Property
* 'previous' - the value of the Property prior to change (this will be
:code:`properties.undefined` if the value was not previously set)
* 'value' - the new value of the Property (this will be
:code:`properties.undefined` if the value is deleted)
* 'mode' - the mode of the change; for validators, this is 'validate'
**Mode 2:**
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Expand Up @@ -29,7 +29,7 @@
EXTRAS.update({'full': sum(EXTRAS.values(), [])})
setup(
name='properties',
version='0.3.2',
version='0.3.3',
packages=find_packages(exclude=('tests',)),
install_requires=['six'],
extras_require=EXTRAS,
Expand Down
45 changes: 43 additions & 2 deletions tests/test_basic.py
Expand Up @@ -77,8 +77,15 @@ class PropOpts(properties.HasProperties):

assert PropOpts(myprop=5).validate()

assert PropOpts().equal(PropOpts())
assert not PropOpts(myprop=5).equal(PropOpts())
with warnings.catch_warnings(record=True) as w:
assert PropOpts().equal(PropOpts())
assert len(w) == 1
assert issubclass(w[0].category, FutureWarning)

assert properties.equal(PropOpts(), PropOpts())
assert properties.equal(PropOpts(myprop=5), PropOpts(myprop=5))
assert not properties.equal(PropOpts(myprop=5), PropOpts())
assert not properties.equal(PropOpts(myprop=5), PropOpts(myprop=6))

with self.assertRaises(AttributeError):
class BadDocOrder(properties.HasProperties):
Expand Down Expand Up @@ -108,6 +115,13 @@ class WithDocOrder(properties.HasProperties):
class NoMoreDocOrder(WithDocOrder):
_doc_order = None

assert properties.Property('').equal(5, 5)
assert not properties.Property('').equal(5, 'hi')
assert properties.Property('').equal(np.array([1., 2.]),
np.array([1., 2.]))
assert not properties.Property('').equal(np.array([1., 2.]),
np.array([3., 4.]))

def test_bool(self):

class BoolOpts(properties.HasProperties):
Expand Down Expand Up @@ -641,6 +655,33 @@ class MyHasProps(properties.HasProperties):

assert myp.my_int is None

def test_copy(self):

class HasProps2(properties.HasProperties):
my_list = properties.List('my list', properties.Bool(''))
five = properties.GettableProperty('five', default=5)
my_array = properties.Vector3Array('my array')

class HasProps1(properties.HasProperties):
my_hp2 = properties.Instance('my HasProps2', HasProps2)
my_union = properties.Union(
'string or int',
(properties.String(''), properties.Integer(''))
)

hp1 = HasProps1(
my_hp2=HasProps2(
my_list=[True, True, False],
my_array=[[1., 2., 3.], [4., 5., 6.]],
),
my_union=10,
)
hp1_copy = properties.copy(hp1)
assert properties.equal(hp1, hp1_copy)
assert hp1 is not hp1_copy
assert hp1.my_hp2 is not hp1_copy.my_hp2
assert hp1.my_hp2.my_list is not hp1_copy.my_hp2.my_list
assert hp1.my_hp2.my_array is not hp1_copy.my_hp2.my_array

if __name__ == '__main__':
unittest.main()
23 changes: 10 additions & 13 deletions tests/test_container.py
Expand Up @@ -464,22 +464,19 @@ class HasColorSet(properties.HasProperties):
class HasIntA(properties.HasProperties):
a = properties.Integer('int a', required=True)

hia_json = properties.Set.to_json({HasIntA(a=5), HasIntA(a=10)})
assert (
properties.Set.to_json(
{HasIntA(a=5), HasIntA(a=10)}
) == [{'__class__': 'HasIntA', 'a': 5},
{'__class__': 'HasIntA', 'a': 10}] or
properties.Set.to_json(
{HasIntA(a=5), HasIntA(a=10)}
) == [{'__class__': 'HasIntA', 'a': 10},
{'__class__': 'HasIntA', 'a': 5}]
hia_json == [{'__class__': 'HasIntA', 'a': 5},
{'__class__': 'HasIntA', 'a': 10}] or
hia_json == [{'__class__': 'HasIntA', 'a': 10},
{'__class__': 'HasIntA', 'a': 5}]
)

assert li.serialize(include_class=False) == {
'ccc': [[255, 0, 0], [0, 255, 0]]
} or li.serialize(include_class=False) == {
'ccc': [[0, 255, 0], [255, 0, 0]]
}
li_ser = li.serialize(include_class=False)
assert (
li_ser == {'ccc': [[255, 0, 0], [0, 255, 0]]} or
li_ser == {'ccc': [[0, 255, 0], [255, 0, 0]]}
)

class HasIntASet(properties.HasProperties):
myset = properties.Set('set of HasIntA', HasIntA,
Expand Down
8 changes: 8 additions & 0 deletions tests/test_math.py
Expand Up @@ -83,6 +83,14 @@ class ArrayOpts(properties.HasProperties):
np.array([1., 2., 3.]))
assert not properties.Array('').equal(np.array([1., 2., 3.]),
[1., 2., 3.])
assert properties.Array('').equal(np.array([1., 2., np.nan, np.nan]),
np.array([1., 2., np.nan, np.nan]))
assert properties.Array('').equal(np.array([1., 2., np.inf, np.nan]),
np.array([1., 2., np.inf, np.nan]))
assert not properties.Array('').equal(
np.array([1., 2., np.nan, np.nan]),
np.array([1., 2., np.inf, np.nan])
)

def test_vector2(self):

Expand Down
2 changes: 1 addition & 1 deletion tests/test_serialization.py
Expand Up @@ -104,7 +104,7 @@ class UidModel(properties.HasProperties):

um1 = UidModel()
um2 = UidModel.deserialize(um1.serialize())
assert um1.equal(um2)
assert properties.equal(um1, um2)

def test_none_serial(self):

Expand Down

0 comments on commit 33a7fba

Please sign in to comment.