source: trunk/python/Rappture/library.py @ 6

Last change on this file since 6 was 6, checked in by mmc, 16 years ago

Fixed the Tcl library to mirror the API developed for XML
libraries on the Python side. The Tcl Rappture::library
now has methods like "children", "element", "put", etc.
One difference: On the Tcl side, the default -flavor for
element/children is "component", since that works better
in Tcl code. In Python, the default is flavor=object.

Also fixed the Tcl install script to install not just
the tcl/scripts library, but also the ../gui and ../lib
directories.

File size: 15.3 KB
Line 
1# ----------------------------------------------------------------------
2#  COMPONENT: library - provides access to the XML library
3#
4#  These routines make it easy to load the XML description for a
5#  series of tool parameters and browse through the results.
6# ======================================================================
7#  AUTHOR:  Michael McLennan, Purdue University
8#  Copyright (c) 2005  Purdue Research Foundation, West Lafayette, IN
9# ======================================================================
10from xml.dom import minidom
11import re, string
12
13class library:
14    """
15    A Rappture library object represents an entire XML document or
16    part of an XML tree.  It simplifies the interface to the XML
17    package, giving the caller an easier mechanism for finding,
18    querying, and setting values in the tree.
19    """
20
21    re_xml = re.compile('<\?[Xx][Mm][Ll]')
22    re_dots = re.compile('(\([^\)]*)\.([^\)]*\))')
23    re_pathElem = re.compile('^(([a-zA-Z_]+#?)([0-9]*))?(\(([^\)]+)\))?$')
24    re_pathCreateElem = re.compile('^([^\(]+)\(([^\)]+)\)$')
25
26    # ------------------------------------------------------------------
27    def __init__(self, elem):
28        if isinstance(elem, minidom.Node):
29            # if the input is a node, then wrap up this node
30            self.doc = elem.ownerDocument
31            self.node = elem
32        elif self.re_xml.match(elem):
33            # if the input is a string, then parse the string
34            self.doc = minidom.parseString(elem)
35            self.node = self.doc.documentElement
36        else:
37            # if the input is a file, then parse the file
38            self.doc = minidom.parse(elem)
39            self.node = self.doc.documentElement
40
41    # ------------------------------------------------------------------
42    def element(self, path="", flavor="object"):
43        """
44        Clients use this to query a particular element within the
45        entire data structure.  The path is a string of the form
46        "structure.box(source).corner".  This example represents
47        the tag <corner> within a tag <box id="source"> within a
48        a tag <structure>, which must be found at the top level
49        within this document.
50
51        By default, this method returns an object representing the
52        DOM node referenced by the path.  This is changed by setting
53        the "flavor" argument to "id" (for name of the tail element),
54        to "type" (for the type of the tail element), to "component"
55        (for the component name "type(id)"), or to "object"
56        for the default (an object representing the tail element).
57        """
58
59        node = self._find(path)
60        if not node:
61            return None
62
63        if flavor == 'object':
64            return library(node)
65        elif flavor == 'component':
66            return self._node2comp(node)
67        elif flavor == 'id':
68            return self._node2name(node)
69        elif flavor == 'type':
70            return node.tagName
71
72        raise ValueError, "bad flavor '%s': should be object, id, type" % flavor
73
74    # ------------------------------------------------------------------
75    def children(self, path="", flavor="object", type=None):
76        """
77        Clients use this to query the children of a particular element
78        within the entire data structure.  This is just like the
79        element() method, but it returns the children of the element
80        instead of the element itself.  If the optional type argument
81        is specified, then the return list is restricted to children
82        of the specified type.
83
84        By default, this method returns a list of objects representing
85        the children.  This is changed by setting the "flavor" argument
86        to "id" (for tail names of all children), to "type" (for the
87        types of all children), to "component" (for the path component
88        names of all children), or to "object" for the default (a list
89        child objects).
90        """
91
92        node = self._find(path)
93        if not node:
94            return None
95
96        nlist = [n for n in node.childNodes if not n.nodeName.startswith('#')]
97
98        if type:
99            nlist = [n for n in nlist if n.nodeName == type]
100
101        if flavor == 'object':
102            return [library(n) for n in nlist]
103        elif flavor == 'component':
104            return [self._node2comp(n) for n in nlist]
105        elif flavor == 'id':
106            return [self._node2name(n) for n in nlist]
107        elif flavor == 'type':
108            return [n.tagName for n in nlist]
109
110    # ------------------------------------------------------------------
111    def get(self, path=""):
112        """
113        Clients use this to query the value of a node.  If the path
114        is not specified, it returns the value associated with the
115        root node.  Otherwise, it returns the value for the element
116        specified by the path.
117        """
118
119        node = self._find(path)
120        if not node:
121            return None
122
123        clist = node.childNodes
124        rval = None
125        for node in [n for n in clist if isinstance(n,minidom.Text)]:
126            if rval == None:
127                rval = node.nodeValue
128            else:
129                rval += node.nodeValue
130        if rval:
131            rval = rval.strip()
132        return rval
133
134    # ------------------------------------------------------------------
135    def put(self, path="", value="", id=None, append=0):
136        """
137        Clients use this to set the value of a node.  If the path
138        is not specified, it sets the value for the root node.
139        Otherwise, it sets the value for the element specified
140        by the path.  If the value is a string, then it is treated
141        as the text within the tag at the tail of the path.  If it
142        is a DOM node or a library, then it is inserted into the
143        tree at the specified path.
144
145        If the optional id is specified, then it sets the identifier
146        for the tag at the tail of the path.  If the optional append
147        flag is specified, then the value is appended to the current
148        value.  Otherwise, the value replaces the current value.
149        """
150
151        node = self._find(path,create=1)
152
153        if append:
154            nlist = [n for n in node.childNodes
155                       if not n.nodeType == n.TEXT_NODE]
156        else:
157            nlist = node.childNodes
158
159        for n in nlist:
160            node.removeChild(n)
161
162        if value:
163            # if there's a value, then add it to the node
164            if isinstance(value, library):
165                node.appendChild(value.node)
166            elif isinstance(value, minidom.Node):
167                node.appendChild(value)
168            elif value and value != '':
169                n = self.doc.createTextNode(value)
170                node.appendChild(n)
171
172        # if there's an id, then set it on the node
173        if id:
174            node.setAttribute("id",id)
175
176    # ------------------------------------------------------------------
177    def remove(self, path=""):
178        """
179        Clients use this to remove the specified node.  Removes the
180        node from the tree and returns it as an element, so you can
181        put it somewhere else if you like.
182        """
183
184        node = self._find(path)
185        if not node:
186            return None
187
188        rval = library('<?xml version="1.0"?>' + node.toxml())
189        node.parentNode.removeChild(node)
190
191        return rval
192
193    # ------------------------------------------------------------------
194    def xml(self):
195        """
196        Clients use this to query the XML representation for this
197        object.
198        """
199        return self.doc.toxml()
200
201    # ------------------------------------------------------------------
202    def _node2name(self, node):
203        """
204        Used internally to create a name for the specified node.
205        If the node doesn't have a specific name ("id" attribute)
206        then a name of the form "type123" is constructed.
207        """
208
209        try:
210            # node has a name? then return it
211            name = node.getAttribute("id")
212        except AttributeError:
213            name = ''
214
215        if name == '':
216            # try to build a name like "type123"
217            type = node.tagName
218            siblings = node.parentNode.getElementsByTagName(type)
219            index = siblings.index(node)
220            if index == 0:
221                name = type
222            else:
223                name = "%s%d" % (type,index)
224
225        return name
226
227    # ------------------------------------------------------------------
228    def _node2comp(self, node):
229        """
230        Used internally to create a path component name for the
231        specified node.  A path component name has the form "type(id)"
232        or just "type##" if the node doesn't have a name.  This name
233        can be used in a path to uniquely address the component.
234        """
235
236        try:
237            # node has a name? then return it
238            name = node.getAttribute("id")
239            if name != '':
240                name = "%s(%s)" % (node.tagName, name)
241        except AttributeError:
242            name = ''
243
244        if name == '':
245            # try to build a name like "type123"
246            type = node.tagName
247            siblings = node.parentNode.getElementsByTagName(type)
248            index = siblings.index(node)
249            if index == 0:
250                name = type
251            else:
252                name = "%s%d" % (type,index)
253
254        return name
255
256    # ------------------------------------------------------------------
257    def _find(self, path, create=0):
258        """
259        Used internally to find a particular element within the
260        root node according to the path, which is a string of
261        the form "typeNN(id).typeNN(id). ...", where each
262        "type" is a tag <type>; if the optional NN is specified,
263        it indicates an index for the <type> tag within its parent;
264        if the optional (id) part is included, it indicates a
265        tag of the form <type id="id">.
266
267        By default, it looks for an element along the path and
268        returns None if not found.  If the create flag is set,
269        it creates various elements along the path as it goes.
270        This is useful for "put" operations.
271
272        If you include "#" instead of a specific number, a node
273        will be created automatically with a new number.  For example,
274        the path "foo.bar#" called the first time will create "foo.bar",
275        the second time "foo.bar1", the third time "foo.bar2" and
276        so forth.
277
278        Returns an object representing the element indicated by
279        the path, or None if the path is not found.
280        """
281
282        if path == "":
283            return self.node
284
285        path = self._path2list(path)
286
287        #
288        # Follow the given path and look for all of the parts.
289        #
290        node = lastnode = self.node
291        for part in path:
292            #
293            # Pick apart "type123(id)" for this part.
294            #
295            match = self.re_pathElem.search(part)
296            if not match:
297                raise ValueError, "bad path component '%s': should have the form 'type123(id)'" % part
298
299            (dummy, type, index, dummy, name) = match.groups()
300
301            if not name:
302                #
303                # If the name is like "type2", then look for elements with
304                # the type name and return the one with the given index.
305                # If the name is like "type", then assume the index is 0.
306                #
307                if not index or index == "":
308                    index = 0
309                else:
310                    index = int(index)
311
312                nlist = node.getElementsByTagName(type)
313                if index < len(nlist):
314                    node = nlist[index]
315                else:
316                    node = None
317            else:
318                #
319                # If the name is like "type(id)", then look for elements
320                # that match the type and see if one has the requested name.
321                # if the name is like "(id)", then look for any elements
322                # with the requested name.
323                #
324                if type:
325                    nlist = node.getElementsByTagName(type)
326                else:
327                    nlist = node.childNodes
328
329                node = None
330                for n in nlist:
331                    try:
332                        tag = n.getAttribute("id")
333                    except AttributeError:
334                        tag = ""
335
336                    if tag == name:
337                        node = n
338                        break
339
340            if not node:
341                if not create:
342                    return None
343
344                #
345                # If the "create" flag is set, then create a node
346                # with the specified "type(id)" and continue on.
347                # If the type is "type#", then create a node with
348                # an automatic number.
349                #
350                match = self.re_pathCreateElem.search(part)
351                if match:
352                    (type, name) = match.groups()
353                else:
354                    type = part
355                    name = ""
356
357                if type.endswith('#'):
358                    type = type.rstrip('#')
359                    node = self.doc.createElement(type)
360
361                    # find the last node of same type and append there
362                    pos = None
363                    for n in lastnode.childNodes:
364                        if n.nodeName == type:
365                            pos = n
366
367                    if pos:
368                        pos = pos.nextSibling
369
370                    if pos:
371                        lastnode.insertBefore(node,pos)
372                    else:
373                        lastnode.appendChild(node)
374                else:
375                    node = self.doc.createElement(type)
376                    lastnode.appendChild(node)
377
378                if name:
379                    node.setAttribute("id",name)
380
381            lastnode = node
382
383        # last node is the desired element
384        return node
385
386    # ------------------------------------------------------------------
387    def _path2list(self, path):
388        """
389        Used internally to convert a path name of the form
390        "foo.bar(x.y).baz" to a list of the form ['foo',
391        'bar(x.y)','baz'].  Note that it's careful to preserve
392        dots within ()'s but splits on those outside ()'s.
393        """
394
395        #
396        # Normally, we just split on .'s within the path.  But there
397        # might be some .'s embedded within ()'s in the path.  Change
398        # any embedded .'s to an out-of-band character, then split on
399        # the .'s, and change the embedded .'s back.
400        #
401        while self.re_dots.search(path):
402            path = self.re_dots.sub('\\1\007\\2',path)
403
404        path = re.split('\.', path)
405        return [re.sub('\007','.',elem) for elem in path]
406
407if __name__ == "__main__":
408    lib = library("""<?xml version="1.0"?>
409<device>
410<label>Heterojunction</label>
411
412<recipe>
413  <slab>
414    <thickness>0.1um</thickness>
415    <material>GaAs</material>
416  </slab>
417  <slab>
418    <thickness>0.1um</thickness>
419    <material>Al(0.3)Ga(0.7)As</material>
420  </slab>
421</recipe>
422<field id="donors">
423  <label>Doping</label>
424  <units>/cm3</units>
425  <scale>log</scale>
426  <color>green</color>
427  <component id="Nd+">
428    <constant>1.0e19/cm3</constant>
429    <domain>slab0</domain>
430  </component>
431</field>
432</device>
433""")
434    print lib.get('label')
435    print lib.get('recipe.slab1.thickness')
436    print lib.get('field(donors).component(Nd+).domain')
437    print lib.get('field(donors).component.domain')
Note: See TracBrowser for help on using the repository browser.