Fixed query algorithm. Added '@@@' operator. Added parseExt. Added documentation.
authorFrederic Jolliton <frederic@jolliton.com>
Thu, 30 Jun 2005 14:41:39 +0000 (14:41 +0000)
committerFrederic Jolliton <frederic@jolliton.com>
Thu, 30 Jun 2005 14:41:39 +0000 (14:41 +0000)
* Fixed query algorithm, which was returning wrong result for query
  such as '//foo/*'.

* Added a '@@@' operator which return result like '@@' but with node
  name in addition to the values.

* Added parseExt similar to parse but which return a NodeSet object
  hierarchy.

* Added a README file describing some of the feature of the query
  stuff.
git-archimport-id: frederic@jolliton.com--2005-private/confparser--main--0.1--patch-16

README [new file with mode: 0644]
confparser.py
confparser_ext.py

diff --git a/README b/README
new file mode 100644 (file)
index 0000000..85832af
--- /dev/null
+++ b/README
@@ -0,0 +1,243 @@
+ConfParser
+----------
+
+This python module allow to parse configuration file with a simple
+syntax.
+
+Example of configuration:
+
+conf = '''
+options {
+  default {
+    allow-snmp-trap true ;
+  }
+}
+server samba-test1 {
+  created '2005-09-19T02:43:30Z' ;
+  monitoring {
+    priority auto ;
+    service web {
+      name 'Apache' ;
+      port 80 443 ;
+    }
+    service mail {
+      name 'Sendmail' ;
+      port 25 ;
+    }
+  }
+  network {
+    nic eth0 {
+      ip '192.168.1.10/24' '192.168.1.11/24' ;
+      speed 100 ;
+      duplex full ;
+    }
+    nic eth1 {
+      comment 'Admin NIC' ;
+      ip '192.168.250.10/24' ;
+      speed auto ;
+      duplex auto ;
+    }
+  }
+}
+'''
+
+The confparser provide the function 'parser' to transform such text
+into a particular tree.
+
+>>> import pprint
+>>> import confparser
+>>> conf = ''' <text above> '''
+>>> example = confparser.parse( conf )
+>>> pprint.pprint( example )
+('__root__',
+ None,
+ [('options',
+   [],
+   [('default',
+     [],
+     [('allow-snmp-trap', ['true'], [], (3, 5, None))],
+     (2, 3, None))],
+   (1, 1, None)),
+  ('server',
+   ['samba-test1'],
+   [('created', ['2005-09-19T02:43:30Z'], [], (7, 3, None)),
+    ('monitoring',
+     [],
+     [('priority', ['auto'], [], (9, 5, None)),
+...)
+>>>
+
+Notice that an artificial root node called '__root__' is added to hold
+all the top elements of the configuration.
+
+But it's not practical to work with such structure.
+
+A recent addition to confparser allow to get an object tree (mode of
+NodeSet/Node objects) instead of this raw structure (which is made of
+list, tuple, string and integer.) The function parseExt do that.
+
+[continuing previous session]
+>>> ns = confparser.parseExt( conf )
+>>> print ns
+<NodeSet with 1 elements>
+>>>
+
+NodeSet provide a method pstr (pretty string) which convert back
+this object structure into a text that look like the original.
+
+>>> print ns.pstr()
+__root__ {
+  options {
+    default {
+      allow-snmp-trap true ;
+    }
+  }
+  server samba-test1 {
+    created "2005-09-19T02:43:30Z" ;
+    monitoring {
+      priority auto ;
+...
+}
+>>>
+
+One of the main advantage of the NodeSet object is that it provide
+support to find certain nodes inside it, a la XPath. For that you have
+to specify a path as argument to "index" NodeSet like in the following
+example to find the 'server' node:
+
+>>> ns[ 'server' ]
+<NodeSet with 1 elements>
+
+To find the 'created' node inside this node, we can either look in the
+previous NodeSet:
+
+>>> ns[ 'server' ][ 'created' ]
+<NodeSet with 1 elements>
+>>> print ns[ 'server' ][ 'created' ].pstr()
+created "2005-09-19T02:43:30Z" ;
+
+or combine path:
+
+>>> print ns[ 'server/created' ].pstr()
+created "2005-09-19T02:43:30Z" ;
+
+To extract the values following the 'created' keyword, we can
+use the special path element '?' which give them:
+
+>>> ns[ 'server/created/?' ]
+'2005-09-19T02:43:30Z'
+
+To find a node with a particular name, append : to node name followed
+by the value to match. To find the 'nic' with values 'eth0':
+
+>>> print ns[ 'server/network/nic:eth0' ].pstr()
+nic eth0 {
+  ip "192.168.1.10/24" "192.168.1.11/24" ;
+  speed 100 ;
+  duplex full ;
+}
+
+To find the IPs of this node:
+
+>>> ns[ 'server/network/nic:eth0/ip/?' ]
+'192.168.1.10/24 192.168.1.11/24'
+
+Notice that this result is a single string.
+
+To find the list of IP, use '@' or '@@' instead of '?'.
+
+>>> ns[ 'server/network/nic:eth0/ip/@' ]
+('192.168.1.10/24', '192.168.1.11/24')
+
+The difference between '@' and '@@' is that the later return a set of
+tuple for each matching line instead of returning a flat list like '@'
+do. It's also possible to extend '@@' further by including also the
+node name by using '@@@' instead.
+
+To find a node at any depth use an empty path element, like in this
+example to find all nodes named 'ip':
+
+>>> ns[ '//ip' ]
+<NodeSet with 2 elements>
+>>> ns[ '//ip/@' ]
+('192.168.1.10/24', '192.168.1.11/24', '192.168.250.10/24')
+>>> ns[ '//ip/@@' ]
+[('192.168.1.10/24', '192.168.1.11/24'), ('192.168.250.10/24',)]
+>>> ns[ '//ip/@@@' ]
+[('ip', '192.168.1.10/24', '192.168.1.11/24'), ('ip', '192.168.250.10/24')]
+
+To match any node use '*'.
+
+>>> ns[ '*' ]
+<NodeSet with 2 elements>
+>>> ns[ '*/@@' ]
+[(), ('samba-test1',)]
+
+(Note: this last result extract the name of the node 'options' which
+have no values hence the empty list, and the name of the 'server'
+node.)
+
+By default '*' match any node with any values.
+
+To match particular values, specify them using ':' as separator, like
+in this example:
+
+>>> ns[ '//nic:eth0/ip/@' ]
+('192.168.1.10/24', '192.168.1.11/24')
+
+You can use wildcard for values:
+
+>>> ns[ '//nic:*' ]
+<NodeSet with 2 elements>
+
+Notice that 'nic' and 'nic:*' don't match the same thing. The former
+match a node named 'nic' with any number of values, while the later
+match a node named 'nic' with only one value (not even zero.)
+
+Wildcard can also be used to match substring. To find all 'nic' node
+with value ending with '1':
+
+>>> ns[ '//nic:*1/?' ]
+'eth1'
+
+To find all the node in the tree, and extract all the values:
+
+>>> ns[ '//*' ]
+<NodeSet with 23 elements>
+>>> ns[ '//*/?' ]
+'true samba-test1 2005-09-19T02:43:30Z auto web Apache 80\
+ 443 mail Sendmail 25 eth0 192.168.1.10/24 192.168.1.11/24\
+ 100 full eth1 Admin NIC 192.168.250.10/24 auto auto'
+>>> ns[ '//*/@@@' ]
+[('options',), ('default',), ('allow-snmp-trap', 'true'),
+ ('server', 'samba-test1'), ('created', '2005-09-19T02:43:30Z'),
+ ('monitoring',), ('priority', 'auto'), ('service', 'web'),
+ ('name', 'Apache'), ('port', 80, 443), ('service', 'mail'),
+ ('name', 'Sendmail'), ('port', 25), ('network',), ('nic', 'eth0'),
+ ('ip', '192.168.1.10/24', '192.168.1.11/24'), ('speed', 100),
+ ('duplex', 'full'), ('nic', 'eth1'), ('comment', 'Admin NIC'),
+ ('ip', '192.168.250.10/24'), ('speed', 'auto'), ('duplex', 'auto')]
+
+More examples:
+
+>>> ns[ '/@@@' ]
+[('__root__',)]
+>>> ns[ '/*/@@@' ]
+[('options',), ('server', 'samba-test1')]
+>>> ns[ '/*/*/@@@' ]
+[('default',), ('created', '2005-09-19T02:43:30Z'), ('monitoring',), ('network',)]
+
+To find any nodes at depth 4:
+
+>>> print ns[ '/*/*/*/*' ].pstr()
+name Apache ;
+port 80 443 ;
+name Sendmail ;
+port 25 ;
+ip "192.168.1.10/24" "192.168.1.11/24" ;
+speed 100 ;
+duplex full ;
+comment "Admin NIC" ;
+ip "192.168.250.10/24" ;
+speed auto ;
+duplex auto ;
index a274f73..0edd9d1 100644 (file)
@@ -199,6 +199,8 @@ def readConfiguration( filename , validator = None ) :
                raise Exception( 'While reading file %s:\n%s' % ( filename , str( e ) ) )
        return conf
 
+#--[ Extended stuff ]---------------------------------------------------------
+
 def readConfigurationExt( filename , validator = None ) :
 
        conf = readConfiguration( filename , validator )
@@ -207,6 +209,12 @@ def readConfigurationExt( filename , validator = None ) :
                conf = confparser_ext.confToNodeset( conf )
        return conf
 
+def parseExt( doc , filename = None ) :
+
+       import confparser_ext
+       conf = parse( doc , filename )
+       return confparser_ext.confToNodeset( conf )
+
 #--[ Dump configuration tree ]------------------------------------------------
 
 def printTreeInner( t , prt = sys.stdout.write , prefix = '' , verbose = False ) :
index a49846e..e6a0443 100644 (file)
@@ -161,62 +161,100 @@ def confToNodeset( node ) :
 #
 # FIXME: UGLY
 #
-def select( ns , path ) :
-
-       def select_( ns , path ) :
-
-               result = NodeSet( ns )
-               while path :
-                       ns = result
-                       recPath = path
-                       part , path = path[ 0 ] , path[ 1 : ]
-                       recurse = False
-                       if part == '' :
-                               recurse = True
-                               part , path = path[ 0 ] , path[ 1 : ]
-                       #
-                       # Build predicates
-                       #
-                       if ':' in part :
-                               part = part.split( ':' )
-                               def matcher( sub ) :
-                                       result = ( part[ 0 ] in [ sub.name , '*' ] and len( sub.values ) == len( part ) - 1 )
-                                       if result :
-                                               for v1 , v2 in zip( sub.values , part[ 1 : ] ) :
-                                                       if re.match( v2
-                                                               .replace( '.' , '\\.' )
-                                                               .replace( '*' , '.*' )
-                                                               .replace( '?' , '. ') , v1 , re.I ) is None :
-                                                               result = False
-                                                               break
-                                       return result
-                       else :
-                               def matcher( sub ) :
-                                       return part in [ sub.name , '*' ]
-                       #
-                       # Select subnodes according to the predicate
-                       #
-                       result = NodeSet()
-                       for item in ns :
-                               for sub in item.subs :
-                                       if matcher( sub ) :
-                                               result += sub
-                                       if recurse :
-                                               result += select_( sub , recPath )
+def select( nodeset , path ) :
+
+       def matchValue_( match , value ) :
+
+               return re.match( match
+                                                .replace( '.' , '\\.' )
+                                                .replace( '*' , '.*' )
+                                                .replace( '?' , '. ') , str( value ) , re.I ) is not None
+
+       def select_( nodeset , path ) :
+
+               '''Search *children* of nodeset matching 'path'.'''
+
+               assert path
+               assert isinstance( nodeset , NodeSet )
+
+               # Extract first path element
+               element , subPath = path[ 0 ] , path[ 1 : ]
+
+               #
+               # Remove leading empty element(s) and set recurse flag
+               # in such case.
+               #
+               recurse = False
+               while element == '' : # the '//' path element
+                       assert subPath , '// must to be followed by a path element.'
+                       element , subPath = subPath[ 0 ] , subPath[ 1 : ]
+                       recurse = True
+
+               #
+               # Build predicates
+               #
+               if ':' in element :
+                       element = element.split( ':' )
+                       def matcher( sub ) :
+                               result = ( element[ 0 ] in [ sub.name , '*' ] and len( sub.values ) == len( element ) - 1 )
+                               if result :
+                                       for v1 , v2 in zip( sub.values , element[ 1 : ] ) :
+                                               if not matchValue_( v2 , v1 ) :
+                                                       result = False
+                                                       break
+                               return result
+               else :
+                       def matcher( sub ) :
+                               return element in [ sub.name , '*' ]
+
+               #
+               # process either continue filtering process, or keep current
+               # result according if path fully processed.
+               #
+               if subPath :
+                       def process( result ) :
+                               return select_( NodeSet( result ) , subPath )
+               else :
+                       def process( result ) :
+                               return result
+
+               #
+               # Select subnodes according to the predicate
+               #
+               result = NodeSet()
+               for item in nodeset :
+                       for sub in item.subs :
+                               if matcher( sub ) :
+                                       result += process( sub )
+                               if recurse :
+                                       result += select_( NodeSet( sub ) , path )
+
                return result
 
-       path = path.split( '/' )
-       if path and path[ 0 ] == '' :
-               # assume // at beginning.. BAD BAD BAD
-               del path[ 0 ]
-       if not path :
-               result = ns
-       else :
-               if path[ -1 ] == '@@' :
+       if not path.startswith( '/' ) :
+               # Always assume path from the root
+               # (It's the only possible case anyway.)
+               path = '/' + path
+
+       nodeset = NodeSet( nodeset )
+
+       path = path.split( '/' )[ 1 : ]
+
+       def finally_( result ) :
+               return result
+
+       if path :
+               if path[ -1 ] == '@@@' :
+                       def finally_( result ) :
+                               return [ ( node.name , ) + node.values for node in result ]
+                       path.pop()
+               elif path[ -1 ] == '@@' :
+                       # FIXME: Factorize with @@@
                        def finally_( result ) :
                                return [ node.values for node in result ]
                        path.pop()
                elif path[ -1 ] == '@' :
+                       # FIXME: Factorize with @@ or @@@
                        def finally_( result ) :
                                return sum( [ node.values for node in result ] , () )
                        path.pop()
@@ -225,11 +263,11 @@ def select( ns , path ) :
                                if len( result ) :
                                        return ' '.join( [ str( s ) for node in result for s in node.values ] )
                        path.pop()
-               else :
-                       def finally_( result ) :
-                               return result
-               result = select_( ns , path )
-               result = finally_( result )
+       if not path or ( len( path ) == 1 and path[ 0 ] == '' ) :
+               result = nodeset
+       else :
+               result = select_( nodeset , path )
+       result = finally_( result )
 
        return result
 
@@ -246,10 +284,12 @@ def test() :
                print n[ 'default/unhide/?' ] # the string for the whole nodesets
                print n[ 'default/unhide/@' ] # the flatten list of text
                print n[ 'default/unhide/@@' ] # the list of list of text
+               print n[ 'default/unhide/@@@' ] # the list of list of text, include node name
                print n[ 'server/@' ]
                print n[ 'server:m*/rootdev/@' ]
                print n[ '*/@@' ]
                print n[ '//*/@@' ]
+               print n[ '//*/@@@' ]
        else :
                print 'Unable to parse %r' % filename