Author: reinhard Date: 2009-10-12 17:17:24 -0500 (Mon, 12 Oct 2009) New Revision: 9963
Modified: trunk/gnue-common/src/base/tree.py Log: Added methods to create a deep copy of a node (including all children), and to merge two trees. Added some module documentation. Modified: trunk/gnue-common/src/base/tree.py =================================================================== --- trunk/gnue-common/src/base/tree.py 2009-10-12 20:32:31 UTC (rev 9962) +++ trunk/gnue-common/src/base/tree.py 2009-10-12 22:17:24 UTC (rev 9963) @@ -24,7 +24,28 @@ """ Classes representing a node in a tree structure. -TODO: This module is work in progress. +Object trees in GNU Enterprise +============================== + +The classes in this module facilitate the creation of an in-memory object tree +made of objects of different classes. An XML tree from a form or report +definition file is typically represented as such a tree. + +The simplest variant for such an object is the L{Node} class which just +maintains a link to its parent and its children, which also have to be +descendants of that class. + +The L{NamedNode} adds a __node_name__ attribute which can be searched for in a +tree, for example set through a C{name=} attribute in an XML element. + +The L{AttribNode} introduces I{attributes}, key/value pairs where the possible +keys are predefined, and for each key, the allowed values are also predefined. +Attributes mirror the XML attributes of a form/report definition file. + +For the sake of intuitive usability, both child nodes and node attributes can +be accessed as object attributes. For example, if the node "foo" has a child +"bar", and that child has an attribute "baz", this attribute can be accessed +with C{foo.bar.baz}. """ from gnue.common.base import errors, i18n @@ -33,7 +54,8 @@ __all__ = ['Node', 'NamedNode', 'AttribNode', 'ChildNotAllowedError', 'DuplicateSingletonError', 'CircularReferenceError', 'DuplicateChildNameError', - 'DuplicateDescendantNameError', 'NodeDictNotAvailableError'] + 'DuplicateDescendantNameError', 'NodeDictNotAvailableError', + 'AttribRequiredError'] # ============================================================================= @@ -57,7 +79,8 @@ the parent. Instances of this class support searching L{ancestors - <__find_ancestor_of_class__>} and L{descendants + <__find_ancestor_of_class__>}, L{direct children + <__find_child_of_class__>}, and L{descendants <__find_descendant_of_class__>} of a given class. @cvar _allowed_children_: Dictionary with valid child node classes, where @@ -221,6 +244,30 @@ # ------------------------------------------------------------------------- + # Find first child of a given class + # ------------------------------------------------------------------------- + + def __find_child_of_class__(self, child_class): + """ + Return the first child with the given class. + + If none of the children is of the given class, None is returned. + + @param child_class: Node class to search for. + @type child_class: type + + @return: Child node of the given class. + @rtype: Node or None + """ + + checktype(child_class, type) + + for child in self.__children__: + if isinstance(child, child_class): + return child + + + # ------------------------------------------------------------------------- # Find first descendant of a given class # ------------------------------------------------------------------------- @@ -239,7 +286,7 @@ @param descendant_class: Node class to search for. @type descendant_class: type - @return: Descendant node of the given node type. + @return: Descendant node of the given class. @rtype: Node or None """ @@ -271,7 +318,7 @@ # Must test with isinstance to also match descendant classes. if isinstance(self, allowed_class): if info.get('singleton', False): - if parent.__find_descendant_of_class__(allowed_class): + if parent.__find_child_of_class__(allowed_class): raise DuplicateSingletonError(parent, allowed_class) break @@ -761,14 +808,14 @@ @type children: list """ - NamedNode.__init__(self, parent=parent, children=children) + NamedNode.__init__(self, children=children) #: Attributes self.__attribs = {} # Set name attribute first so the object already has a nice string - # representation in case setting any of the other attributes causes an - # exception. + # representation in case setting any of the other attributes causes + # an exception. if name is not None: self.name = name @@ -782,7 +829,11 @@ if self.__get_attrib__(attrib) is None: raise AttribRequiredError(self, attrib) + # Set parent only now, in case an exception happens before we aren't + # linked in the tree. + self.__parent__ = parent + # ------------------------------------------------------------------------- # Access attribs as object attributes # ------------------------------------------------------------------------- @@ -818,6 +869,24 @@ # Attribute access # ------------------------------------------------------------------------- + def __get_attrib_list(self): + + return self.__attribs.keys() + + # ------------------------------------------------------------------------- + + __attrib_list__ = property(__get_attrib_list, None, None, + """ + A list of the names of the attributes which have been explicitly + set. (readonly) + + Attributes still containing the default value are not listed. + + @type: [Node] + """) + + # ------------------------------------------------------------------------- + def __get_attrib__(self, name): """ Get an attribute value. @@ -869,7 +938,7 @@ target_type = unicode # Typecast if necessary. - if not isinstance(value, target_type): + if not isinstance(value, target_type) and value is not None: value = target_type(value) # Check if value is allowed. @@ -892,9 +961,72 @@ # Merge two nodes with all their children # ------------------------------------------------------------------------- - # TODO: Steal from GParserHelpers + def __copy__(self): + """ + Create a deep copy of the object tree. + """ + # Copy the node itself. + result = self.__class__(**self.__attribs) + # Copy children. + for child in self.__children__: + child.__copy__().__parent__ = result + + return result + + + # ------------------------------------------------------------------------- + # Merge two nodes with all their children + # ------------------------------------------------------------------------- + + def __merge__(self, other): + """ + Merge another object tree into this tree. + + All attributes and child nodes from the other object are merged into this + object. If any child node exists in both objects with the same name, + the merge is done recursively. + + @param other: L{ParserObj} tree to be merged into this object tree + """ + + # First copy all explicitly set attributes from the other object. + # We access other.__attribs directly to avoid conversion from string to + # object and back for object references. + for (name, value) in other.__attribs.items(): + if name not in self.__attribs: + self.__set_attrib__(name, value) + + # Now copy/merge all children. + for child in other.__children__: + if child.name is None: + # Unnamed children: check if singleton. + for allowed_class, info in self._allowed_children_.items(): + if isinstance(self, allowed_class): + my_child = self.__find_child_of_class__(allowed_class) + if my_child and info.get('singleton', False): + # If singleton and already there: merge. + my_child.__merge__(child) + else: + # Otherwise: copy. + child.__copy__().__parent__ = self + break + else: + # Not found in self._allowed_children_: Copy it, this will + # create a meaningful exception. + child.__copy__().__parent__ = self + else: + # Named children: check if already there. + my_child = self.__get_child__(child.name) + if my_child: + # If already there: merge. + my_child.__merge__(child) + else: + # Otherwise: copy. + child.__copy__().__parent__ = self + + # ============================================================================= # Exceptions # ============================================================================= @@ -1271,8 +1403,7 @@ rank="Captain"), Member( name="Leonard McCoy", - rank="Medic", - supervisor="James T. Kirk"), + rank="Medic"), Member( name="Nyota Uhura", rank="Communication Officer")]) @@ -1307,7 +1438,30 @@ print "Spock's supervisor is", repr(crew.Spock.supervisor) print "Uhura's supervisor is", repr(uhura.supervisor) + # Test __copy__ method. + new_crew = crew.__copy__() + print "Copy of the crew:", new_crew.__children__ + print "IDs of the original:", [id(c) for c in crew.__children__] + print "IDs of the copy:", [id(c) for c in new_crew.__children__] + # Test __merge__ method. + merge_crew = Crew( + name="U.S.S. Enterprise", + children=[ + Member( + name="Scotty", + rank="Chief Technican", + supervisor="James T. Kirk"), + Member( + name="Leonard McCoy", + rank="Medic", + supervisor="James T. Kirk")]) + crew.__merge__(merge_crew) + print "Merged crew:", crew.__children__ + print "McCoy's supervisor is now", + print repr(crew.__get_child__("Leonard McCoy").supervisor) + + # ----------------------------------------------------------------------------- # Run tests # ----------------------------------------------------------------------------- _______________________________________________ commit-gnue mailing list commit-gnue@gnu.org http://lists.gnu.org/mailman/listinfo/commit-gnue