diff --git a/odml/property.py b/odml/property.py index 74e31f7e..894296d2 100644 --- a/odml/property.py +++ b/odml/property.py @@ -22,9 +22,9 @@ def __init__(self, name, value=None, parent=None, unit=None, dependency=None, dependency_value=None, dtype=None, value_origin=None, id=None): """ - Create a new Property with a single value. The method will try to infer - the value's dtype from the type of the value if not explicitly stated. - Example for a property with + Create a new Property. If a value without an explicitly stated dtype + has been provided, the method will try to infer the value's dtype. + Example: >>> p = Property("property1", "a string") >>> p.dtype >>> str @@ -34,21 +34,25 @@ def __init__(self, name, value=None, parent=None, unit=None, >>> p = Property("prop", [2, 3, 4]) >>> p.dtype >>> int - :param name: The name of the property - :param value: Some data value, this may be a list of homogeneous values + :param name: The name of the property. + :param value: Some data value, it can be a single value or + a list of homogeneous values. :param unit: The unit of the stored data. - :param uncertainty: the uncertainty (e.g. the standard deviation) + :param uncertainty: The uncertainty (e.g. the standard deviation) associated with a measure value. :param reference: A reference (e.g. an URL) to an external definition of the value. :param definition: The definition of the property. :param dependency: Another property this property depends on. :param dependency_value: Dependency on a certain value. - :param dtype: the data type of the values stored in the property, - if dtype is not given, the type is deduced from the values + :param dtype: The data type of the values stored in the property, + if dtype is not given, the type is deduced from the values. + Check odml.DType for supported data types. :param value_origin: Reference where the value originated from e.g. a file name. + :param id: UUID string as specified in RFC 4122. If no id is provided, + an id will be generated and assigned. An id has to be unique + within an odML Document. """ - # TODO validate arguments try: if id is not None: self._id = str(uuid.UUID(id)) @@ -84,7 +88,7 @@ def id(self): def new_id(self, id=None): """ - new_id sets the id of the current object to a RFC 4122 compliant UUID. + new_id sets the id of the current object to an RFC 4122 compliant UUID. If an id was provided, it is assigned if it is RFC 4122 UUID format compliant. If no id was provided, a new UUID is generated and assigned. :param id: UUID string as specified in RFC 4122. @@ -108,7 +112,7 @@ def __repr__(self): @property def dtype(self): """ - The data type of the value + The data type of the value. Check odml.DType for supported data types. """ return self._dtype @@ -116,11 +120,9 @@ def dtype(self): def dtype(self, new_type): """ If the data type of a property value is changed, it is tried - to convert the value to the new type. - If this doesn't work, the change is refused. - - This behaviour can be overridden by directly accessing the *_dtype* - attribute and adjusting the *data* attribute manually. + to convert existing values to the new type. If this doesn't work, + the change is refused. The dtype can always be changed, if + a Property does not contain values. """ # check if this is a valid type if not dtypes.valid_type(new_type): @@ -139,7 +141,7 @@ def dtype(self, new_type): @property def parent(self): """ - The section containing this property + The section containing this property. """ return self._parent @@ -170,29 +172,30 @@ def _validate_parent(new_parent): @property def value(self): """ - Returns the value(s) stored in this property. Method always returns a list that - is a copy (!) of the stored value. Changing this list will NOT change the property. - For manipulation of the stored values use the append, extend, and direct access methods - (using brackets). + Returns the value(s) stored in this property. Method always returns a list + that is a copy (!) of the stored value. Changing this list will NOT change + the property. + For manipulation of the stored values use the append, extend, and direct + access methods (using brackets). For example: - >> p = odml.Property("prop", value=[1, 2, 3]) - >> print(p.value) + >>> p = odml.Property("prop", value=[1, 2, 3]) + >>> print(p.value) [1, 2, 3] - >> p.value.append(4) - >> print(p.value) + >>> p.value.append(4) + >>> print(p.value) [1, 2, 3] Individual values can be accessed and manipulated like this: >>> print(p[0]) [1] - >> p[0] = 4 - >> print(p[0]) + >>> p[0] = 4 + >>> print(p[0]) [4] The values can be iterated e.g. with a loop: - >> for v in p.value: - print(v) + >>> for v in p.value: + >>> print(v) 4 2 3 @@ -201,18 +204,18 @@ def value(self): def value_str(self, index=0): """ - Used to access typed data of the value as a string. - Use data to access the raw type, i.e.: + Used to access typed data of the value at a specific + index position as a string. """ return dtypes.set(self._value[index], self._dtype) def _validate_values(self, values): """ - Method ensures that the passed value(s) can be cast to the - same dtype, i.e. that associated with this property or the - inferred dtype of the first entry of the values list. + Method ensures that the passed value(s) can be cast to the + same dtype, i.e. that are associated with this property or the + inferred dtype of the first entry of the values list. - :param values an iterable that contains the values + :param values: an iterable that contains the values. """ for v in values: try: @@ -227,7 +230,7 @@ def _convert_value_input(self, new_value): If new_value is a string, it will convert it to a list of strings if the new_value contains embracing brackets. - returns list of new_value + :return: list of new_value """ if isinstance(new_value, str): if new_value[0] == "[" and new_value[-1] == "]": @@ -241,21 +244,22 @@ def _convert_value_input(self, new_value): elif not isinstance(new_value, list): new_value = [new_value] else: - raise ValueError("odml.Property._convert_value_input: unsupported data type for values: %s" % type(new_value)) + raise ValueError("odml.Property._convert_value_input: " + "unsupported data type for values: %s" % type(new_value)) return new_value @value.setter def value(self, new_value): """ - Set the value of the property discarding any previous information. Method will try to convert the passed value to the dtype of - the property and raise an ValueError, if not possible + the property and raise an ValueError if not possible. - :param new_value a single value or list of values. + :param new_value: a single value or list of values. """ # Make sure boolean value 'False' gets through as well... - if new_value is None or (isinstance(new_value, (list, tuple, str)) and len(new_value) == 0): + if new_value is None or \ + (isinstance(new_value, (list, tuple, str)) and len(new_value) == 0): self._value = [] return @@ -285,6 +289,8 @@ def uncertainty(self): @uncertainty.setter def uncertainty(self, new_value): + if new_value == "": + new_value = None self._uncertainty = new_value @property @@ -339,9 +345,9 @@ def dependency_value(self, new_value): def remove(self, value): """ - Remove a value from this property and unset its parent. - Raises a TypeError if this would cause the property not to hold any - value at all. This can be circumvented by using the *_values* property. + Remove a value from this property. Only the first encountered + occurrence of the passed in value is removed from the properties + list of values. """ if value in self._value: self._value.remove(value) @@ -358,6 +364,7 @@ def get_path(self): def clone(self): """ Clone this object to copy it independently to another document. + The id of the cloned object will be set to a different uuid. """ obj = super(BaseProperty, self).clone() obj._parent = None @@ -367,23 +374,23 @@ def clone(self): return obj def merge(self, other, strict=True): - """Merges the property 'other' into self, if possible. Information - will be synchronized. Method will raise an ValueError when the + """ + Merges the property 'other' into self, if possible. Information + will be synchronized. Method will raise a ValueError when the information in this property and the passed property are in conflict. - :param other a Property - :param strict Bool value to indicate whether types should be - implicitly converted even when information may be lost. Default is True, i.e. no conversion, and error will be raised if types do not match. - + :param other: an odML Property. + :param strict: Bool value to indicate whether types should be implicitly converted + even when information may be lost. Default is True, i.e. no conversion, + and a ValueError will be raised if types do not match. """ - assert(isinstance(other, (BaseProperty))) + assert(isinstance(other, BaseProperty)) if strict and self.dtype != other.dtype: raise ValueError("odml.Property.merge: src and dest dtypes do not match!") if self.unit is not None and other.unit is not None and self.unit != other.unit: - raise ValueError("odml.Property.merge: src and dest units (%s, %s) do not match!" - % (other.unit, self.unit)) + raise ValueError("odml.Property.merge: src and dest units (%s, %s) do not match!" % (other.unit, self.unit)) if self.definition is not None and other.definition is not None: self_def = ''.join(map(str.strip, self.definition.split())).lower() @@ -422,14 +429,14 @@ def merge(self, other, strict=True): def unmerge(self, other): """ - Stub that doesn't do anything for this class + Stub that doesn't do anything for this class. """ pass def get_merged_equivalent(self): """ - Return the merged object (i.e. if the section is linked to another one, - return the corresponding property of the linked section) or None + Return the merged object (i.e. if the parent section is linked to another one, + return the corresponding property of the linked section) or None. """ if self.parent is None or self.parent._merged is None: return None @@ -466,17 +473,18 @@ def __setitem__(self, key, item): def extend(self, obj, strict=True): """ - Extend the list of values stored in this property by the passed values. Method will - raise an ValueError, if values cannot be converted to the current dtype. One can also pass - another Property to append all values stored in that one. In this case units must match! + Extend the list of values stored in this property by the passed values. Method + will raise a ValueError, if values cannot be converted to the current dtype. + One can also pass another Property to append all values stored in that one. + In this case units must match! - :param obj single value, list of values or Property - :param strict a Bool that controls whether dtypes must match. Default is True. + :param obj: single value, list of values or a Property. + :param strict: a Bool that controls whether dtypes must match. Default is True. """ if isinstance(obj, BaseProperty): - if (obj.unit != self.unit): - raise ValueError("odml.Property.append: src and dest units (%s, %s) do not match!" - % (obj.unit, self.unit)) + if obj.unit != self.unit: + raise ValueError("odml.Property.extend: src and dest units (%s, %s) " + "do not match!" % (obj.unit, self.unit)) self.extend(obj.value) return @@ -486,29 +494,41 @@ def extend(self, obj, strict=True): new_value = self._convert_value_input(obj) if len(new_value) > 0 and strict and dtypes.infer_dtype(new_value[0]) != self.dtype: - raise ValueError("odml.Property.extend: passed value data type does not match dtype!"); + raise ValueError("odml.Property.extend: " + "passed value data type does not match dtype!") if not self._validate_values(new_value): - raise ValueError("odml.Property.append: passed value(s) cannot be converted to " - "data type \'%s\'!" % self._dtype) + raise ValueError("odml.Property.extend: passed value(s) cannot be converted " + "to data type \'%s\'!" % self._dtype) self._value.extend([dtypes.get(v, self.dtype) for v in new_value]) def append(self, obj, strict=True): """ - Append a single value to the list of stored values. Method will raise an ValueError if - the passed value cannot be converted to the current dtype. + Append a single value to the list of stored values. Method will raise + a ValueError if the passed value cannot be converted to the current dtype. - :param obj the additional value. - :param strict a Bool that controls whether dtypes must match. Default is True. + :param obj: the additional value. + :param strict: a Bool that controls whether dtypes must match. Default is True. """ + # Ignore empty values before nasty stuff happens, but make sure + # 0 and False get through. + if obj in [None, "", [], {}]: + return + + if not self.value: + self.value = obj + return + new_value = self._convert_value_input(obj) if len(new_value) > 1: raise ValueError("odml.property.append: Use extend to add a list of values!") + if len(new_value) > 0 and strict and dtypes.infer_dtype(new_value[0]) != self.dtype: - raise ValueError("odml.Property.extend: passed value data type does not match dtype!"); + raise ValueError("odml.Property.append: " + "passed value data type does not match dtype!") if not self._validate_values(new_value): - raise ValueError("odml.Property.append: passed value(s) cannot be converted to " - "data type \'%s\'!" % self._dtype) - self._value.append(dtypes.get(new_value[0], self.dtype)) + raise ValueError("odml.Property.append: passed value(s) cannot be converted " + "to data type \'%s\'!" % self._dtype) + self._value.append(dtypes.get(new_value[0], self.dtype)) diff --git a/test/test_property.py b/test/test_property.py index d0dc6737..f0aa9769 100644 --- a/test/test_property.py +++ b/test/test_property.py @@ -10,26 +10,67 @@ class TestProperty(unittest.TestCase): def setUp(self): pass + def test_simple_attributes(self): + p_name = "propertyName" + p_origin = "from over there" + p_unit = "pears" + p_uncertainty = "+-12" + p_ref = "4 8 15 16 23" + p_def = "an odml test property" + p_dep = "yes" + p_dep_val = "42" + + prop = Property(name=p_name, value_origin=p_origin, unit=p_unit, + uncertainty=p_uncertainty, reference=p_ref, definition=p_def, + dependency=p_dep, dependency_value=p_dep_val) + + self.assertEqual(prop.name, p_name) + self.assertEqual(prop.value_origin, p_origin) + self.assertEqual(prop.unit, p_unit) + self.assertEqual(prop.uncertainty, p_uncertainty) + self.assertEqual(prop.reference, p_ref) + self.assertEqual(prop.definition, p_def) + self.assertEqual(prop.dependency, p_dep) + self.assertEqual(prop.dependency_value, p_dep_val) + + # Test setting attributes + prop.name = "%s_edit" % p_name + self.assertEqual(prop.name, "%s_edit" % p_name) + prop.value_origin = "%s_edit" % p_origin + self.assertEqual(prop.value_origin, "%s_edit" % p_origin) + prop.unit = "%s_edit" % p_unit + self.assertEqual(prop.unit, "%s_edit" % p_unit) + prop.uncertainty = "%s_edit" % p_uncertainty + self.assertEqual(prop.uncertainty, "%s_edit" % p_uncertainty) + prop.reference = "%s_edit" % p_ref + self.assertEqual(prop.reference, "%s_edit" % p_ref) + prop.definition = "%s_edit" % p_def + self.assertEqual(prop.definition, "%s_edit" % p_def) + prop.dependency = "%s_edit" % p_dep + self.assertEqual(prop.dependency, "%s_edit" % p_dep) + prop.dependency_value = "%s_edit" % p_dep_val + self.assertEqual(prop.dependency_value, "%s_edit" % p_dep_val) + + # Test setting attributes to None when '' is passed. + prop.value_origin = "" + self.assertIsNone(prop.value_origin) + prop.unit = "" + self.assertIsNone(prop.unit) + prop.uncertainty = "" + self.assertIsNone(prop.uncertainty) + prop.reference = "" + self.assertIsNone(prop.reference) + prop.definition = "" + self.assertIsNone(prop.definition) + prop.dependency = "" + self.assertIsNone(prop.dependency) + prop.dependency_value = "" + self.assertIsNone(prop.dependency_value) + def test_value(self): p = Property("property", 100) self.assertEqual(p.value[0], 100) - self.assertEqual(type(p.value), list) - - p.append(10) - self.assertEqual(len(p), 2) - self.assertRaises(ValueError, p.append, [1, 2, 3]) - - p.extend([20, 30, '40']) - self.assertEqual(len(p), 5) - with self.assertRaises(ValueError): - p.append('invalid') - with self.assertRaises(ValueError): - p.extend(('5', 6, 7)) - - p2 = Property("property 2", 3) - self.assertRaises(ValueError, p.append, p2) - p.extend(p2) - self.assertEqual(len(p), 6) + self.assertIsInstance(p.value, list) p.value = None self.assertEqual(len(p), 0) @@ -46,42 +87,178 @@ def test_value(self): p.value = () self.assertEqual(len(p), 0) - p3 = Property("test", value=2, unit="Hz") - p4 = Property("test", value=5.5, unit="s") + p.value.append(5) + self.assertEqual(len(p.value), 0) + + p2 = Property("test", {"name": "Marie", "name": "Johanna"}) + self.assertEqual(len(p2), 1) + + # Test tuple dtype value. + t = Property(name="Location", value='(39.12; 67.19)', dtype='2-tuple') + tuple_value = t.value[0] # As the formed tuple is a list of list + self.assertEqual(tuple_value[0], '39.12') + self.assertEqual(tuple_value[1], '67.19') + # Test invalid tuple length with self.assertRaises(ValueError): - p3.append(p4) + _ = Property(name="Public-Key", value='(5689; 1254; 687)', dtype='2-tuple') - p.value.append(5) - self.assertEqual(len(p.value), 0) - self.assertRaises(ValueError, p.append, 5.5) + def test_value_append(self): + # Test append w/o Property value or dtype + prop = Property(name="append") + prop.append(1) + self.assertEqual(prop.dtype, DType.int) + self.assertEqual(prop.value, [1]) + + # Test append with Property dtype. + prop = Property(name="append", dtype="int") + prop.append(3) + self.assertEqual(prop.value, [3]) + + # Test append with Property value + prop = Property(name="append", value=[1, 2]) + prop.append(3) + self.assertEqual(prop.value, [1, 2, 3]) + + # Test append with Property list value + prop = Property(name="append", value=[1, 2]) + prop.append([3]) + self.assertEqual(prop.value, [1, 2, 3]) + + # Test append of empty values, make sure 0 and False are properly handled + prop = Property(name="append") + prop.append(None) + prop.append("") + prop.append([]) + prop.append({}) + self.assertEqual(prop.value, []) + + prop.append(0) + self.assertEqual(prop.value, [0]) + + prop.value = None + prop.dtype = None + prop.append(False) + self.assertEqual(prop.value, [False]) + + prop = Property(name="append", value=[1, 2]) + prop.append(None) + prop.append("") + prop.append([]) + prop.append({}) + self.assertEqual(prop.value, [1, 2]) - p.append(5.5, strict=False) - self.assertEqual(len(p), 1) + prop.append(0) + self.assertEqual(prop.value, [1, 2, 0]) - self.assertRaises(ValueError, p.extend, [3.14, 6.28]) - p.extend([3.14, 6.28], strict=False) - self.assertEqual(len(p), 3) + # Test fail append with multiple values + prop = Property(name="append", value=[1, 2, 3]) + with self.assertRaises(ValueError): + prop.append([4, 5]) + self.assertEqual(prop.value, [1, 2, 3]) + + # Test fail append with mismatching dtype + prop = Property(name="append", value=[1, 2], dtype="int") + with self.assertRaises(ValueError): + prop.append([3.14]) + with self.assertRaises(ValueError): + prop.append([True]) + with self.assertRaises(ValueError): + prop.append(["5.927"]) + self.assertEqual(prop.value, [1, 2]) + + # Test strict flag + prop.append(3.14, strict=False) + prop.append(True, strict=False) + prop.append("5.927", strict=False) + self.assertEqual(prop.value, [1, 2, 3, 1, 5]) + + # Make sure non-convertible values still raise an error + with self.assertRaises(ValueError): + prop.append("invalid") + self.assertEqual(prop.value, [1, 2, 3, 1, 5]) p5 = Property("test", value="a string") p5.append("Freude") self.assertEqual(len(p5), 2) self.assertRaises(ValueError, p5.append, "[a, b, c]") - p5.extend("[a, b, c]") - self.assertEqual(len(p5), 5) - p6 = Property("test", {"name": "Marie", "name": "Johanna"}) - self.assertEqual(len(p6), 1) + def test_value_extend(self): + prop = Property(name="extend") - # Test tuple dtype value. - t = Property(name="Location", value='(39.12; 67.19)', dtype='2-tuple') - tuple_value = t.value[0] # As the formed tuple is a list of list - self.assertEqual(tuple_value[0], '39.12') - self.assertEqual(tuple_value[1], '67.19') + # Test extend w/o Property value or dtype. + val = [1, 2, 3] + prop.extend(val) + self.assertEqual(prop.dtype, DType.int) + self.assertEqual(prop.value, val) - # Test invalid tuple length + # Extend with single value. + prop.extend(4) + self.assertEqual(prop.value, [1, 2, 3, 4]) + + # Extend with list value. + prop.extend([5, 6]) + self.assertEqual(prop.value, [1, 2, 3, 4, 5, 6]) + + # Test extend w/o Property value + prop = Property(name="extend", dtype="float") + prop.extend([1.0, 2.0, 3.0]) + self.assertEqual(prop.value, [1.0, 2.0, 3.0]) + + # Test extend with Property value + prop = Property(name="extend", value=10) + prop.extend([20, 30, '40']) + self.assertEqual(prop.value, [10, 20, 30, 40]) + + # Test extend fail with mismatching dtype with self.assertRaises(ValueError): - _ = Property(name="Public-Key", value='(5689; 1254; 687)', dtype='2-tuple') + prop.extend(['5', 6, 7]) + with self.assertRaises(ValueError): + prop.extend([5, 6, 'a']) + + # Test extend via Property + prop = Property(name="extend", value=["a", "b"]) + ext_prop = Property(name="value extend", value="c") + prop.extend(ext_prop) + self.assertEqual(prop.value, ["a", "b", "c"]) + + ext_prop.value = ["d", "e"] + prop.extend(ext_prop) + self.assertEqual(prop.value, ["a", "b", "c", "d", "e"]) + + ext_prop = Property(name="value extend", value=[1, 2 ,3]) + with self.assertRaises(ValueError): + prop.extend(ext_prop) + self.assertEqual(prop.value, ["a", "b", "c", "d", "e"]) + + # Test extend via Property unit check + prop = Property(name="extend", value=[1, 2], unit="mV") + ext_prop = Property(name="extend", value=[3, 4], unit="mV") + prop.extend(ext_prop) + self.assertEqual(prop.value, [1, 2, 3, 4]) + + ext_prop.unit = "kV" + with self.assertRaises(ValueError): + prop.extend(ext_prop) + self.assertEqual(prop.value, [1, 2, 3, 4]) + + ext_prop.unit = "" + with self.assertRaises(ValueError): + prop.extend(ext_prop) + self.assertEqual(prop.value, [1, 2, 3, 4]) + + # Test strict flag + prop = Property(name="extend", value=[1, 2], dtype="int") + with self.assertRaises(ValueError): + prop.extend([3.14, True, "5.927"]) + self.assertEqual(prop.value, [1, 2]) + + prop.extend([3.14, True, "5.927"], strict=False) + self.assertEqual(prop.value, [1, 2, 3, 1, 5]) + + # Make sure non-convertible values still raise an error + with self.assertRaises(ValueError): + prop.extend([6, "some text"]) def test_get_set_value(self): values = [1, 2, 3, 4, 5] @@ -150,9 +327,6 @@ def test_str_to_int_convert(self): assert(p.dtype == 'string') assert(p.value == ['7', '20', '1 Dog', 'Seven']) - def test_name(self): - pass - def test_parent(self): p = Property("property_section", parent=Section("S")) self.assertIsInstance(p.parent, BaseSection) @@ -206,6 +380,12 @@ def test_dtype(self): with self.assertRaises(AttributeError): prop.dtype = "x-tuple" + # Test not setting None when a property contains values. + prop.value = [1, 2, 3] + self.assertIsNotNone(prop.dtype) + prop.dtype = None + self.assertIsNotNone(prop.dtype) + def test_get_path(self): doc = Document() sec = Section(name="parent", parent=doc) @@ -218,14 +398,6 @@ def test_get_path(self): prop.parent = sec self.assertEqual("/%s:%s" % (sec.name, prop.name), prop.get_path()) - def test_value_origin(self): - p = Property("P") - self.assertEqual(p.value_origin, None) - p = Property("P", value_origin="V") - self.assertEqual(p.value_origin, "V") - p.value_origin = "" - self.assertEqual(p.value_origin, None) - def test_id(self): p = Property(name="P") self.assertIsNotNone(p.id) diff --git a/test/test_property_integration.py b/test/test_property_integration.py index 479883f7..cf30d591 100644 --- a/test/test_property_integration.py +++ b/test/test_property_integration.py @@ -106,6 +106,7 @@ def test_simple_attributes(self): self.assertEqual(jprop.unit, p_unit) self.assertEqual(jprop.uncertainty, p_uncertainty) self.assertEqual(jprop.reference, p_ref) + self.assertEqual(jprop.definition, p_def) self.assertEqual(jprop.dependency, p_dep) self.assertEqual(jprop.dependency_value, p_dep_val) @@ -116,6 +117,7 @@ def test_simple_attributes(self): self.assertEqual(xprop.unit, p_unit) self.assertEqual(xprop.uncertainty, p_uncertainty) self.assertEqual(xprop.reference, p_ref) + self.assertEqual(xprop.definition, p_def) self.assertEqual(xprop.dependency, p_dep) self.assertEqual(xprop.dependency_value, p_dep_val) @@ -126,5 +128,6 @@ def test_simple_attributes(self): self.assertEqual(yprop.unit, p_unit) self.assertEqual(yprop.uncertainty, p_uncertainty) self.assertEqual(yprop.reference, p_ref) + self.assertEqual(yprop.definition, p_def) self.assertEqual(yprop.dependency, p_dep) self.assertEqual(yprop.dependency_value, p_dep_val)