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

Last change on this file since 3 was 3, checked in by mmc, 19 years ago

Added put() and remove() options to the Rappture.library,
so you can add and remove information from the tree.

File size: 14.2 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 "name" (for name of the tail element),
54        to "type" (for the type of the tail element), to "component"
55        (for the component name "type(name)"), 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 == 'name':
68            return self._node2name(node)
69        elif flavor == 'type':
70            return node.tagName
71
72        raise ValueError, "bad flavor '%s': should be object, name, 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 "name" (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 == 'name':
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        if len(clist) == 1 and isinstance(clist[0],minidom.Text):
125            return clist[0].nodeValue.strip()
126
127        return None
128
129    # ------------------------------------------------------------------
130    def put(self, path="", value="", id=None, append=0):
131        """
132        Clients use this to set the value of a node.  If the path
133        is not specified, it sets the value for the root node.
134        Otherwise, it sets the value for the element specified
135        by the path.  If the value is a string, then it is treated
136        as the text within the tag at the tail of the path.  If it
137        is a DOM node or a library, then it is inserted into the
138        tree at the specified path.
139
140        If the optional id is specified, then it sets the identifier
141        for the tag at the tail of the path.  If the optional append
142        flag is specified, then the value is appended to the current
143        value.  Otherwise, the value replaces the current value.
144        """
145
146        node = self._find(path,create=1)
147
148        if append:
149            nlist = [n for n in node.childNodes
150                       if not n.nodeType == n.TEXT_NODE]
151        else:
152            nlist = node.childNodes
153
154        for n in nlist:
155            node.removeChild(n)
156
157        if value:
158            # if there's a value, then add it to the node
159            if isinstance(value, library):
160                node.appendChild(value.node)
161            elif isinstance(value, minidom.Node):
162                node.appendChild(value)
163            elif value and value != '':
164                n = self.doc.createTextNode(value)
165                node.appendChild(n)
166
167        # if there's an id, then set it on the node
168        if id:
169            node.setAttribute("id",id)
170
171    # ------------------------------------------------------------------
172    def remove(self, path=""):
173        """
174        Clients use this to remove the specified node.  Removes the
175        node from the tree and returns it as an element, so you can
176        put it somewhere else if you like.
177        """
178
179        node = self._find(path)
180        if not node:
181            return None
182
183        rval = library('<?xml version="1.0"?>' + node.toxml())
184        node.parentNode.removeChild(node)
185
186        return rval
187
188    # ------------------------------------------------------------------
189    def xml(self):
190        """
191        Clients use this to query the XML representation for this
192        object.
193        """
194        return self.doc.toxml()
195
196    # ------------------------------------------------------------------
197    def _node2name(self, node):
198        """
199        Used internally to create a name for the specified node.
200        If the node doesn't have a specific name ("id" attribute)
201        then a name of the form "type123" is constructed.
202        """
203
204        try:
205            # node has a name? then return it
206            name = node.getAttribute("id")
207        except AttributeError:
208            name = ''
209
210        if name == '':
211            # try to build a name like "type123"
212            type = node.tagName
213            siblings = node.parentNode.getElementsByTagName(type)
214            index = siblings.index(node)
215            if index == 0:
216                name = type
217            else:
218                name = "%s%d" % (type,index)
219
220        return name
221
222    # ------------------------------------------------------------------
223    def _node2comp(self, node):
224        """
225        Used internally to create a path component name for the
226        specified node.  A path component name has the form "type(name)"
227        or just "type##" if the node doesn't have a name.  This name
228        can be used in a path to uniquely address the component.
229        """
230
231        try:
232            # node has a name? then return it
233            name = node.getAttribute("id")
234            if name != '':
235                name = "%s(%s)" % (node.tagName, name)
236        except AttributeError:
237            name = ''
238
239        if name == '':
240            # try to build a name like "type123"
241            type = node.tagName
242            siblings = node.parentNode.getElementsByTagName(type)
243            index = siblings.index(node)
244            if index == 0:
245                name = type
246            else:
247                name = "%s%d" % (type,index)
248
249        return name
250
251    # ------------------------------------------------------------------
252    def _find(self, path, create=0):
253        """
254        Used internally to find a particular element within the
255        root node according to the path, which is a string of
256        the form "type##(name).type##(name). ...", where each
257        "type" is a tag <type>; if the optional ## is specified,
258        it indicates an index for the <type> tag within its parent;
259        if the optional (name) part is included, it indicates a
260        tag of the form <type id="name">.
261
262        By default, it looks for an element along the path and
263        returns None if not found.  If the create flag is set,
264        it creates various elements along the path as it goes.
265        This is useful for "put" operations.
266
267        Returns an object representing the element indicated by
268        the path, or None if the path is not found.
269        """
270
271        if path == "":
272            return self.node
273
274        path = self._path2list(path)
275
276        #
277        # Follow the given path and look for all of the parts.
278        #
279        node = lastnode = self.node
280        for part in path:
281            #
282            # Pick apart "type123(name)" for this part.
283            #
284            match = self.re_pathElem.search(part)
285            if not match:
286                raise ValueError, "bad path component '%s': should have the form 'type123(name)'" % part
287
288            (dummy, type, index, dummy, name) = match.groups()
289
290            if not name:
291                #
292                # If the name is like "type2", then look for elements with
293                # the type name and return the one with the given index.
294                # If the name is like "type", then assume the index is 0.
295                #
296                if not index or index == "":
297                    index = 0
298                else:
299                    index = int(index)
300
301                nlist = node.getElementsByTagName(type)
302                if index < len(nlist):
303                    node = nlist[index]
304                else:
305                    node = None
306            else:
307                #
308                # If the name is like "type(name)", then look for elements
309                # that match the type and see if one has the requested name.
310                # if the name is like "(name)", then look for any elements
311                # with the requested name.
312                #
313                if type:
314                    nlist = node.getElementsByTagName(type)
315                else:
316                    nlist = node.childNodes
317
318                node = None
319                for n in nlist:
320                    try:
321                        tag = n.getAttribute("id")
322                    except AttributeError:
323                        tag = ""
324
325                    if tag == name:
326                        node = n
327                        break
328
329            if not node:
330                if not create:
331                    return None
332
333                #
334                # If the "create" flag is set, then create a node
335                # with the specified "type(name)" and continue on.
336                #
337                match = self.re_pathCreateElem.search(part)
338                if match:
339                    (type, name) = match.groups()
340                else:
341                    type = part
342                    name = ""
343
344                node = self.doc.createElement(type)
345                lastnode.appendChild(node)
346
347                if name:
348                    node.setAttribute("name",name)
349
350            lastnode = node
351
352        # last node is the desired element
353        return node
354
355    # ------------------------------------------------------------------
356    def _path2list(self, path):
357        """
358        Used internally to convert a path name of the form
359        "foo.bar(x.y).baz" to a list of the form ['foo',
360        'bar(x.y)','baz'].  Note that it's careful to preserve
361        dots within ()'s but splits on those outside ()'s.
362        """
363
364        #
365        # Normally, we just split on .'s within the path.  But there
366        # might be some .'s embedded within ()'s in the path.  Change
367        # any embedded .'s to an out-of-band character, then split on
368        # the .'s, and change the embedded .'s back.
369        #
370        while self.re_dots.search(path):
371            path = self.re_dots.sub('\\1\007\\2',path)
372
373        path = re.split('\.', path)
374        return [re.sub('\007','.',elem) for elem in path]
375
376if __name__ == "__main__":
377    lib = library("""<?xml version="1.0"?>
378<device>
379<label>Heterojunction</label>
380
381<recipe>
382  <slab>
383    <thickness>0.1um</thickness>
384    <material>GaAs</material>
385  </slab>
386  <slab>
387    <thickness>0.1um</thickness>
388    <material>Al(0.3)Ga(0.7)As</material>
389  </slab>
390</recipe>
391<field id="donors">
392  <label>Doping</label>
393  <units>/cm3</units>
394  <scale>log</scale>
395  <color>green</color>
396  <component id="Nd+">
397    <constant>1.0e19/cm3</constant>
398    <domain>slab0</domain>
399  </component>
400</field>
401</device>
402""")
403    print lib.get('label')
404    print lib.get('recipe.slab1.thickness')
405    print lib.get('field(donors).component(Nd+).domain')
406    print lib.get('field(donors).component.domain')
Note: See TracBrowser for help on using the repository browser.