Skip to content

Commit 9d8af75

Browse files
authored
Merge pull request #269 from mpsonntag/propTest
Property tests and tweaks
2 parents c16f989 + b2791c6 commit 9d8af75

3 files changed

Lines changed: 320 additions & 125 deletions

File tree

odml/property.py

Lines changed: 95 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,9 @@ def __init__(self, name, value=None, parent=None, unit=None,
2222
dependency=None, dependency_value=None, dtype=None,
2323
value_origin=None, id=None):
2424
"""
25-
Create a new Property with a single value. The method will try to infer
26-
the value's dtype from the type of the value if not explicitly stated.
27-
Example for a property with
25+
Create a new Property. If a value without an explicitly stated dtype
26+
has been provided, the method will try to infer the value's dtype.
27+
Example:
2828
>>> p = Property("property1", "a string")
2929
>>> p.dtype
3030
>>> str
@@ -34,21 +34,25 @@ def __init__(self, name, value=None, parent=None, unit=None,
3434
>>> p = Property("prop", [2, 3, 4])
3535
>>> p.dtype
3636
>>> int
37-
:param name: The name of the property
38-
:param value: Some data value, this may be a list of homogeneous values
37+
:param name: The name of the property.
38+
:param value: Some data value, it can be a single value or
39+
a list of homogeneous values.
3940
:param unit: The unit of the stored data.
40-
:param uncertainty: the uncertainty (e.g. the standard deviation)
41+
:param uncertainty: The uncertainty (e.g. the standard deviation)
4142
associated with a measure value.
4243
:param reference: A reference (e.g. an URL) to an external definition
4344
of the value.
4445
:param definition: The definition of the property.
4546
:param dependency: Another property this property depends on.
4647
:param dependency_value: Dependency on a certain value.
47-
:param dtype: the data type of the values stored in the property,
48-
if dtype is not given, the type is deduced from the values
48+
:param dtype: The data type of the values stored in the property,
49+
if dtype is not given, the type is deduced from the values.
50+
Check odml.DType for supported data types.
4951
:param value_origin: Reference where the value originated from e.g. a file name.
52+
:param id: UUID string as specified in RFC 4122. If no id is provided,
53+
an id will be generated and assigned. An id has to be unique
54+
within an odML Document.
5055
"""
51-
# TODO validate arguments
5256
try:
5357
if id is not None:
5458
self._id = str(uuid.UUID(id))
@@ -84,7 +88,7 @@ def id(self):
8488

8589
def new_id(self, id=None):
8690
"""
87-
new_id sets the id of the current object to a RFC 4122 compliant UUID.
91+
new_id sets the id of the current object to an RFC 4122 compliant UUID.
8892
If an id was provided, it is assigned if it is RFC 4122 UUID format compliant.
8993
If no id was provided, a new UUID is generated and assigned.
9094
:param id: UUID string as specified in RFC 4122.
@@ -108,19 +112,17 @@ def __repr__(self):
108112
@property
109113
def dtype(self):
110114
"""
111-
The data type of the value
115+
The data type of the value. Check odml.DType for supported data types.
112116
"""
113117
return self._dtype
114118

115119
@dtype.setter
116120
def dtype(self, new_type):
117121
"""
118122
If the data type of a property value is changed, it is tried
119-
to convert the value to the new type.
120-
If this doesn't work, the change is refused.
121-
122-
This behaviour can be overridden by directly accessing the *_dtype*
123-
attribute and adjusting the *data* attribute manually.
123+
to convert existing values to the new type. If this doesn't work,
124+
the change is refused. The dtype can always be changed, if
125+
a Property does not contain values.
124126
"""
125127
# check if this is a valid type
126128
if not dtypes.valid_type(new_type):
@@ -139,7 +141,7 @@ def dtype(self, new_type):
139141
@property
140142
def parent(self):
141143
"""
142-
The section containing this property
144+
The section containing this property.
143145
"""
144146
return self._parent
145147

@@ -170,29 +172,30 @@ def _validate_parent(new_parent):
170172
@property
171173
def value(self):
172174
"""
173-
Returns the value(s) stored in this property. Method always returns a list that
174-
is a copy (!) of the stored value. Changing this list will NOT change the property.
175-
For manipulation of the stored values use the append, extend, and direct access methods
176-
(using brackets).
175+
Returns the value(s) stored in this property. Method always returns a list
176+
that is a copy (!) of the stored value. Changing this list will NOT change
177+
the property.
178+
For manipulation of the stored values use the append, extend, and direct
179+
access methods (using brackets).
177180
178181
For example:
179-
>> p = odml.Property("prop", value=[1, 2, 3])
180-
>> print(p.value)
182+
>>> p = odml.Property("prop", value=[1, 2, 3])
183+
>>> print(p.value)
181184
[1, 2, 3]
182-
>> p.value.append(4)
183-
>> print(p.value)
185+
>>> p.value.append(4)
186+
>>> print(p.value)
184187
[1, 2, 3]
185188
186189
Individual values can be accessed and manipulated like this:
187190
>>> print(p[0])
188191
[1]
189-
>> p[0] = 4
190-
>> print(p[0])
192+
>>> p[0] = 4
193+
>>> print(p[0])
191194
[4]
192195
193196
The values can be iterated e.g. with a loop:
194-
>> for v in p.value:
195-
print(v)
197+
>>> for v in p.value:
198+
>>> print(v)
196199
4
197200
2
198201
3
@@ -201,18 +204,18 @@ def value(self):
201204

202205
def value_str(self, index=0):
203206
"""
204-
Used to access typed data of the value as a string.
205-
Use data to access the raw type, i.e.:
207+
Used to access typed data of the value at a specific
208+
index position as a string.
206209
"""
207210
return dtypes.set(self._value[index], self._dtype)
208211

209212
def _validate_values(self, values):
210213
"""
211-
Method ensures that the passed value(s) can be cast to the
212-
same dtype, i.e. that associated with this property or the
213-
inferred dtype of the first entry of the values list.
214+
Method ensures that the passed value(s) can be cast to the
215+
same dtype, i.e. that are associated with this property or the
216+
inferred dtype of the first entry of the values list.
214217
215-
:param values an iterable that contains the values
218+
:param values: an iterable that contains the values.
216219
"""
217220
for v in values:
218221
try:
@@ -227,7 +230,7 @@ def _convert_value_input(self, new_value):
227230
If new_value is a string, it will convert it to a list of
228231
strings if the new_value contains embracing brackets.
229232
230-
returns list of new_value
233+
:return: list of new_value
231234
"""
232235
if isinstance(new_value, str):
233236
if new_value[0] == "[" and new_value[-1] == "]":
@@ -241,21 +244,22 @@ def _convert_value_input(self, new_value):
241244
elif not isinstance(new_value, list):
242245
new_value = [new_value]
243246
else:
244-
raise ValueError("odml.Property._convert_value_input: unsupported data type for values: %s" % type(new_value))
247+
raise ValueError("odml.Property._convert_value_input: "
248+
"unsupported data type for values: %s" % type(new_value))
245249
return new_value
246250

247251
@value.setter
248252
def value(self, new_value):
249253
"""
250-
251254
Set the value of the property discarding any previous information.
252255
Method will try to convert the passed value to the dtype of
253-
the property and raise an ValueError, if not possible
256+
the property and raise an ValueError if not possible.
254257
255-
:param new_value a single value or list of values.
258+
:param new_value: a single value or list of values.
256259
"""
257260
# Make sure boolean value 'False' gets through as well...
258-
if new_value is None or (isinstance(new_value, (list, tuple, str)) and len(new_value) == 0):
261+
if new_value is None or \
262+
(isinstance(new_value, (list, tuple, str)) and len(new_value) == 0):
259263
self._value = []
260264
return
261265

@@ -285,6 +289,8 @@ def uncertainty(self):
285289

286290
@uncertainty.setter
287291
def uncertainty(self, new_value):
292+
if new_value == "":
293+
new_value = None
288294
self._uncertainty = new_value
289295

290296
@property
@@ -339,9 +345,9 @@ def dependency_value(self, new_value):
339345

340346
def remove(self, value):
341347
"""
342-
Remove a value from this property and unset its parent.
343-
Raises a TypeError if this would cause the property not to hold any
344-
value at all. This can be circumvented by using the *_values* property.
348+
Remove a value from this property. Only the first encountered
349+
occurrence of the passed in value is removed from the properties
350+
list of values.
345351
"""
346352
if value in self._value:
347353
self._value.remove(value)
@@ -358,6 +364,7 @@ def get_path(self):
358364
def clone(self):
359365
"""
360366
Clone this object to copy it independently to another document.
367+
The id of the cloned object will be set to a different uuid.
361368
"""
362369
obj = super(BaseProperty, self).clone()
363370
obj._parent = None
@@ -367,23 +374,23 @@ def clone(self):
367374
return obj
368375

369376
def merge(self, other, strict=True):
370-
"""Merges the property 'other' into self, if possible. Information
371-
will be synchronized. Method will raise an ValueError when the
377+
"""
378+
Merges the property 'other' into self, if possible. Information
379+
will be synchronized. Method will raise a ValueError when the
372380
information in this property and the passed property are in
373381
conflict.
374382
375-
:param other a Property
376-
:param strict Bool value to indicate whether types should be
377-
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.
378-
383+
:param other: an odML Property.
384+
:param strict: Bool value to indicate whether types should be implicitly converted
385+
even when information may be lost. Default is True, i.e. no conversion,
386+
and a ValueError will be raised if types do not match.
379387
"""
380-
assert(isinstance(other, (BaseProperty)))
388+
assert(isinstance(other, BaseProperty))
381389
if strict and self.dtype != other.dtype:
382390
raise ValueError("odml.Property.merge: src and dest dtypes do not match!")
383391

384392
if self.unit is not None and other.unit is not None and self.unit != other.unit:
385-
raise ValueError("odml.Property.merge: src and dest units (%s, %s) do not match!"
386-
% (other.unit, self.unit))
393+
raise ValueError("odml.Property.merge: src and dest units (%s, %s) do not match!" % (other.unit, self.unit))
387394

388395
if self.definition is not None and other.definition is not None:
389396
self_def = ''.join(map(str.strip, self.definition.split())).lower()
@@ -422,14 +429,14 @@ def merge(self, other, strict=True):
422429

423430
def unmerge(self, other):
424431
"""
425-
Stub that doesn't do anything for this class
432+
Stub that doesn't do anything for this class.
426433
"""
427434
pass
428435

429436
def get_merged_equivalent(self):
430437
"""
431-
Return the merged object (i.e. if the section is linked to another one,
432-
return the corresponding property of the linked section) or None
438+
Return the merged object (i.e. if the parent section is linked to another one,
439+
return the corresponding property of the linked section) or None.
433440
"""
434441
if self.parent is None or self.parent._merged is None:
435442
return None
@@ -466,17 +473,18 @@ def __setitem__(self, key, item):
466473

467474
def extend(self, obj, strict=True):
468475
"""
469-
Extend the list of values stored in this property by the passed values. Method will
470-
raise an ValueError, if values cannot be converted to the current dtype. One can also pass
471-
another Property to append all values stored in that one. In this case units must match!
476+
Extend the list of values stored in this property by the passed values. Method
477+
will raise a ValueError, if values cannot be converted to the current dtype.
478+
One can also pass another Property to append all values stored in that one.
479+
In this case units must match!
472480
473-
:param obj single value, list of values or Property
474-
:param strict a Bool that controls whether dtypes must match. Default is True.
481+
:param obj: single value, list of values or a Property.
482+
:param strict: a Bool that controls whether dtypes must match. Default is True.
475483
"""
476484
if isinstance(obj, BaseProperty):
477-
if (obj.unit != self.unit):
478-
raise ValueError("odml.Property.append: src and dest units (%s, %s) do not match!"
479-
% (obj.unit, self.unit))
485+
if obj.unit != self.unit:
486+
raise ValueError("odml.Property.extend: src and dest units (%s, %s) "
487+
"do not match!" % (obj.unit, self.unit))
480488
self.extend(obj.value)
481489
return
482490

@@ -486,29 +494,41 @@ def extend(self, obj, strict=True):
486494

487495
new_value = self._convert_value_input(obj)
488496
if len(new_value) > 0 and strict and dtypes.infer_dtype(new_value[0]) != self.dtype:
489-
raise ValueError("odml.Property.extend: passed value data type does not match dtype!");
497+
raise ValueError("odml.Property.extend: "
498+
"passed value data type does not match dtype!")
490499

491500
if not self._validate_values(new_value):
492-
raise ValueError("odml.Property.append: passed value(s) cannot be converted to "
493-
"data type \'%s\'!" % self._dtype)
501+
raise ValueError("odml.Property.extend: passed value(s) cannot be converted "
502+
"to data type \'%s\'!" % self._dtype)
494503
self._value.extend([dtypes.get(v, self.dtype) for v in new_value])
495504

496505
def append(self, obj, strict=True):
497506
"""
498-
Append a single value to the list of stored values. Method will raise an ValueError if
499-
the passed value cannot be converted to the current dtype.
507+
Append a single value to the list of stored values. Method will raise
508+
a ValueError if the passed value cannot be converted to the current dtype.
500509
501-
:param obj the additional value.
502-
:param strict a Bool that controls whether dtypes must match. Default is True.
510+
:param obj: the additional value.
511+
:param strict: a Bool that controls whether dtypes must match. Default is True.
503512
"""
513+
# Ignore empty values before nasty stuff happens, but make sure
514+
# 0 and False get through.
515+
if obj in [None, "", [], {}]:
516+
return
517+
518+
if not self.value:
519+
self.value = obj
520+
return
521+
504522
new_value = self._convert_value_input(obj)
505523
if len(new_value) > 1:
506524
raise ValueError("odml.property.append: Use extend to add a list of values!")
525+
507526
if len(new_value) > 0 and strict and dtypes.infer_dtype(new_value[0]) != self.dtype:
508-
raise ValueError("odml.Property.extend: passed value data type does not match dtype!");
527+
raise ValueError("odml.Property.append: "
528+
"passed value data type does not match dtype!")
509529

510530
if not self._validate_values(new_value):
511-
raise ValueError("odml.Property.append: passed value(s) cannot be converted to "
512-
"data type \'%s\'!" % self._dtype)
513-
self._value.append(dtypes.get(new_value[0], self.dtype))
531+
raise ValueError("odml.Property.append: passed value(s) cannot be converted "
532+
"to data type \'%s\'!" % self._dtype)
514533

534+
self._value.append(dtypes.get(new_value[0], self.dtype))

0 commit comments

Comments
 (0)