Skip to content

Commit 40d088a

Browse files
authored
Merge pull request #275 from mpsonntag/refMerge
Refactor Property and Section merge
2 parents a66ecdf + aeafcc1 commit 40d088a

5 files changed

Lines changed: 590 additions & 75 deletions

File tree

odml/base.py

Lines changed: 52 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -84,34 +84,18 @@ def __hash__(self):
8484
return id(self)
8585

8686

87-
class SafeList(list):
87+
class SmartList(list):
8888

89-
def index(self, obj):
89+
def __init__(self, content_type):
9090
"""
91-
Find obj in list
92-
93-
Be sure to use "is" based comparison (instead of __eq__)
91+
Only values of the instance *content_type* can be added to the SmartList.
9492
"""
95-
for i, e in enumerate(self):
96-
if e is obj:
97-
return i
98-
raise ValueError("remove: %s not in list" % repr(obj))
99-
100-
def remove(self, obj):
101-
"""
102-
Remove an element from this list.
103-
104-
Be sure to use "is" based comparison (instead of __eq__)
105-
"""
106-
del self[self.index(obj)]
107-
108-
109-
class SmartList(SafeList):
93+
self._content_type = content_type
94+
super(SmartList, self).__init__()
11095

11196
def __getitem__(self, key):
11297
"""
113-
Provides element index also by searching for an element with a given
114-
name
98+
Provides element index also by searching for an element with a given name.
11599
"""
116100
# Try normal list index first (for integers)
117101
if isinstance(key, int):
@@ -125,31 +109,68 @@ def __getitem__(self, key):
125109
# and fail eventually
126110
raise KeyError(key)
127111

112+
def __setitem__(self, key, value):
113+
"""
114+
Replaces item at list[*key*] with *value*.
115+
:param key: index position
116+
:param value: object that replaces item at *key* position.
117+
value has to be of the same content type as the list.
118+
In this context usually a Section or a Property.
119+
"""
120+
if not isinstance(value, self._content_type):
121+
raise ValueError("List only supports elements of type '%s'" %
122+
self._content_type)
123+
124+
# If required remove new object from its old parents child-list
125+
if hasattr(value, "_parent") and (value._parent and value in value._parent):
126+
value._parent.remove(value)
127+
128+
# If required move parent reference from replaced to new object
129+
# and set parent reference on replaced object None.
130+
if hasattr(self[key], "_parent"):
131+
value._parent = self[key]._parent
132+
self[key]._parent = None
133+
134+
super(SmartList, self).__setitem__(key, value)
135+
128136
def __contains__(self, key):
129137
for obj in self:
130138
if (hasattr(obj, "name") and obj.name == key) or key == obj:
131139
return True
132140

141+
def index(self, obj):
142+
"""
143+
Find obj in list
144+
"""
145+
for i, e in enumerate(self):
146+
if e is obj:
147+
return i
148+
raise ValueError("remove: %s not in list" % repr(obj))
149+
150+
def remove(self, obj):
151+
"""
152+
Remove an element from this list.
153+
"""
154+
del self[self.index(obj)]
155+
133156
def append(self, *obj_tuple):
134-
from odml.section import BaseSection
135-
from odml.doc import BaseDocument
136157
for obj in obj_tuple:
137158
if obj.name in self:
138159
raise KeyError(
139160
"Object with the same name already exists! " + str(obj))
140161

141-
if (not isinstance(obj, BaseSection)) & \
142-
isinstance(self, BaseDocument):
143-
raise KeyError("Object " + str(obj) + " is not a Section.")
162+
if not isinstance(obj, self._content_type):
163+
raise ValueError("List only supports elements of type '%s'" %
164+
self._content_type)
144165

145166
super(SmartList, self).append(obj)
146167

147168

148169
@allow_inherit_docstring
149170
class sectionable(baseobject):
150-
151171
def __init__(self):
152-
self._sections = SmartList()
172+
from odml.section import Section
173+
self._sections = SmartList(Section)
153174
self._repository = None
154175

155176
@property
@@ -518,9 +539,10 @@ def clone(self, children=True):
518539
Clone this object recursively allowing to copy it independently
519540
to another document
520541
"""
542+
from odml.section import Section
521543
obj = super(sectionable, self).clone(children)
522544
obj._parent = None
523-
obj._sections = SmartList()
545+
obj._sections = SmartList(Section)
524546
if children:
525547
for s in self._sections:
526548
obj.append(s.clone())

odml/property.py

Lines changed: 63 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -373,45 +373,83 @@ def clone(self):
373373

374374
return obj
375375

376-
def merge(self, other, strict=True):
376+
def merge_check(self, source, strict=True):
377377
"""
378-
Merges the property 'other' into self, if possible. Information
379-
will be synchronized. Method will raise a ValueError when the
380-
information in this property and the passed property are in
381-
conflict.
378+
Checks whether a source Property can be merged with self as destination and
379+
raises a ValueError if the values of source and destination are not compatible.
380+
With parameter *strict=True* a ValueError is also raised, if any of the
381+
attributes unit, definition, uncertainty, reference or value_origin and dtype
382+
differ in source and destination.
382383
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.
384+
:param source: an odML Property.
385+
:param strict: If True, the attributes dtype, unit, uncertainty, definition,
386+
reference and value_origin of source and destination
387+
must be identical.
387388
"""
388-
assert(isinstance(other, BaseProperty))
389-
if strict and self.dtype != other.dtype:
389+
if not isinstance(source, BaseProperty):
390+
raise ValueError("odml.Property.merge: odML Property required.")
391+
392+
# Catch unmerge-able values at this point to avoid
393+
# failing Section tree merges which cannot easily be rolled back.
394+
new_value = self._convert_value_input(source.value)
395+
if not self._validate_values(new_value):
396+
raise ValueError("odml.Property.merge: passed value(s) cannot "
397+
"be converted to data type '%s'!" % self._dtype)
398+
if not strict:
399+
return
400+
401+
if (self.dtype is not None and source.dtype is not None and
402+
self.dtype != source.dtype):
390403
raise ValueError("odml.Property.merge: src and dest dtypes do not match!")
391404

392-
if self.unit is not None and other.unit is not None and self.unit != other.unit:
393-
raise ValueError("odml.Property.merge: src and dest units (%s, %s) do not match!" % (other.unit, self.unit))
405+
if self.unit is not None and source.unit is not None and self.unit != source.unit:
406+
raise ValueError("odml.Property.merge: "
407+
"src and dest units (%s, %s) do not match!" %
408+
(source.unit, self.unit))
394409

395-
if self.definition is not None and other.definition is not None:
410+
if (self.uncertainty is not None and source.uncertainty is not None and
411+
self.uncertainty != source.uncertainty):
412+
raise ValueError("odml.Property.merge: "
413+
"src and dest uncertainty both set and do not match!")
414+
415+
if self.definition is not None and source.definition is not None:
396416
self_def = ''.join(map(str.strip, self.definition.split())).lower()
397-
other_def = ''.join(map(str.strip, other.definition.split())).lower()
417+
other_def = ''.join(map(str.strip, source.definition.split())).lower()
398418
if self_def != other_def:
399-
raise ValueError("odml.Property.merge: src and dest definitions do not match!")
400-
401-
if self.uncertainty is not None and other.uncertainty is not None:
402-
raise ValueError("odml.Property.merge: src and dest uncertainty both set and do not match!")
419+
raise ValueError("odml.Property.merge: "
420+
"src and dest definitions do not match!")
403421

404-
if self.reference is not None and other.reference is not None:
422+
if self.reference is not None and source.reference is not None:
405423
self_ref = ''.join(map(str.strip, self.reference.lower().split()))
406-
other_ref = ''.join(map(str.strip, other.reference.lower().split()))
424+
other_ref = ''.join(map(str.strip, source.reference.lower().split()))
407425
if self_ref != other_ref:
408-
raise ValueError("odml.Property.merge: src and dest references are in conflict!")
426+
raise ValueError("odml.Property.merge: "
427+
"src and dest references are in conflict!")
409428

410-
if self.value_origin is not None and other.value_origin is not None:
429+
if self.value_origin is not None and source.value_origin is not None:
411430
self_ori = ''.join(map(str.strip, self.value_origin.lower().split()))
412-
other_ori = ''.join(map(str.strip, other.value_origin.lower().split()))
431+
other_ori = ''.join(map(str.strip, source.value_origin.lower().split()))
413432
if self_ori != other_ori:
414-
raise ValueError("odml.Property.merge: src and dest value_origin are in conflict!")
433+
raise ValueError("odml.Property.merge: "
434+
"src and dest value_origin are in conflict!")
435+
436+
def merge(self, other, strict=True):
437+
"""
438+
Merges the Property 'other' into self, if possible. Information
439+
will be synchronized. By default the method will raise a ValueError when the
440+
information in this property and the passed property are in conflict.
441+
442+
:param other: an odML Property.
443+
:param strict: Bool value to indicate whether types should be implicitly converted
444+
even when information may be lost. Default is True, i.e. no conversion,
445+
and a ValueError will be raised if types or other attributes do not match.
446+
If a conflict arises with strict=False, the attribute value of self will
447+
be kept, while the attribute value of other will be lost.
448+
"""
449+
if not isinstance(other, BaseProperty):
450+
raise TypeError("odml.Property.merge: odml Property required.")
451+
452+
self.merge_check(other, strict)
415453

416454
if self.value_origin is None and other.value_origin is not None:
417455
self.value_origin = other.value_origin

0 commit comments

Comments
 (0)