Fixed query algorithm. Added '@@@' operator. Added parseExt. Added documentation.
[confparser-old] / confparser_ext.py
1 # -*- coding: utf-8 -*-
2
3 #
4 # Configuration parser extension
5 # Copyright (C) 2005  Frédéric Jolliton <frederic@jolliton.com>
6 #
7 # This program is free software; you can redistribute it and/or modify
8 # it under the terms of the GNU General Public License as published by
9 # the Free Software Foundation; either version 2 of the License, or
10 # (at your option) any later version.
11 #
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 # GNU General Public License for more details.
16 #
17 # You should have received a copy of the GNU General Public License
18 # along with this program; if not, write to the Free Software
19 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
20 #
21
22 #
23 # This module is sort of prototype to support xpath-like
24 # way to fetch certain nodes from a configuration tree.
25 #
26 # Code is absolutely not nice.. It's really just for experimentation
27 # and need to be rewritten completely.
28 #
29
30 import re
31
32 def flatten( lst ) :
33
34         '''Flatten a list or tuple.
35         
36         [[1,2],[3,[4,5]]] => [1,2,3,4,5]'''
37
38         if isinstance( lst , list ) :
39                 r = []
40                 for item in lst :
41                         r += flatten( item )
42         elif isinstance( lst , tuple ) :
43                 r = ()
44                 for item in lst :
45                         r += tuple( flatten( item ) )
46         else :
47                 r = ( lst , )
48         return r
49
50 def quote( s ) :
51
52         s = str( s )
53         if re.search( r'^(?:\d+|[a-z_][a-z0-9_-]*)$' , s , re.I ) is None :
54                 s = '"%s"' % s.replace( '\\' , '\\\\' ).replace( '"' , '\\"' )
55         return s
56
57 class NodeSet :
58
59         def __init__( self , *nodes ) :
60
61                 self.__nodes = []
62                 self.__iadd__( flatten( nodes ) )
63
64         def __iadd__( self , *others ) :
65
66                 others = flatten( others )
67                 for item in others :
68                         assert isinstance( item , ( Node , NodeSet ) )
69                 for item in others :
70                         if isinstance( item , NodeSet ) :
71                                 self.__nodes += item.__nodes
72                         else :
73                                 self.__nodes.append( item )
74                 return self
75
76         def __getitem__( self , place ) :
77
78                 if isinstance( place , str ) :
79                         return select( self , place )
80                 return self.__nodes[ place ]
81
82         def __len__( self ) :
83
84                 return len( self.__nodes )
85
86         def pstr( self , indent = '' ) :
87
88                 return '\n'.join( [ node.pstr( indent ) for node in self.__nodes ] )
89
90         def __repr__( self ) :
91
92                 return '<NodeSet with %d elements>' % len( self.__nodes )
93
94 class Node :
95
96         def __init__( self , name , *values ) :
97
98                 self.__name = name
99                 self.__values = flatten( values )
100                 self.__subs = NodeSet()
101
102         def __iadd__( self , *others ) :
103
104                 self.__subs += others
105                 return self
106
107         def __getName( self ) :
108
109                 return self.__name
110
111         name = property( __getName )
112
113         def __getSubs( self ) :
114
115                 return self.__subs
116
117         subs = property( __getSubs )
118
119         def __getValues( self ) :
120
121                 return self.__values
122
123         values = property( __getValues )
124
125         def pstr( self , indent = '' ) :
126
127                 r = indent + quote( self.__name ) + ' '
128                 if self.__values :
129                         r += ' '.join( map( quote , self.__values ) ) + ' '
130                 if len( self.__subs ) != 0 :
131                         r += '{\n' + self.__subs.pstr( indent + '  ' ) + '\n' + indent + '}'
132                 else :
133                         r += ';'
134                 return r
135
136         def __getitem__( self , place ) :
137
138                 if isinstance( place , str ) :
139                         return select( self , place )
140                 return self.subs[ place ]
141
142         def __repr__( self ) :
143
144                 return '<Node :name %r>' % ( self.__name , )
145
146 def confToNodeset( node ) :
147
148         def confToNodeset_( node ) :
149
150                 if isinstance( node , tuple ) :
151                         result = Node( node[ 0 ] , node[ 1 ] or [] )
152                         result += confToNodeset_( node[ 2 ] )
153                 elif isinstance( node , list ) :
154                         result = NodeSet( [ confToNodeset_( sub ) for sub in node or () ] )
155                 else :
156                         result = None
157                 return result
158
159         return NodeSet( confToNodeset_( node ) )
160
161 #
162 # FIXME: UGLY
163 #
164 def select( nodeset , path ) :
165
166         def matchValue_( match , value ) :
167
168                 return re.match( match
169                                                  .replace( '.' , '\\.' )
170                                                  .replace( '*' , '.*' )
171                                                  .replace( '?' , '. ') , str( value ) , re.I ) is not None
172
173         def select_( nodeset , path ) :
174
175                 '''Search *children* of nodeset matching 'path'.'''
176
177                 assert path
178                 assert isinstance( nodeset , NodeSet )
179
180                 # Extract first path element
181                 element , subPath = path[ 0 ] , path[ 1 : ]
182
183                 #
184                 # Remove leading empty element(s) and set recurse flag
185                 # in such case.
186                 #
187                 recurse = False
188                 while element == '' : # the '//' path element
189                         assert subPath , '// must to be followed by a path element.'
190                         element , subPath = subPath[ 0 ] , subPath[ 1 : ]
191                         recurse = True
192
193                 #
194                 # Build predicates
195                 #
196                 if ':' in element :
197                         element = element.split( ':' )
198                         def matcher( sub ) :
199                                 result = ( element[ 0 ] in [ sub.name , '*' ] and len( sub.values ) == len( element ) - 1 )
200                                 if result :
201                                         for v1 , v2 in zip( sub.values , element[ 1 : ] ) :
202                                                 if not matchValue_( v2 , v1 ) :
203                                                         result = False
204                                                         break
205                                 return result
206                 else :
207                         def matcher( sub ) :
208                                 return element in [ sub.name , '*' ]
209
210                 #
211                 # process either continue filtering process, or keep current
212                 # result according if path fully processed.
213                 #
214                 if subPath :
215                         def process( result ) :
216                                 return select_( NodeSet( result ) , subPath )
217                 else :
218                         def process( result ) :
219                                 return result
220
221                 #
222                 # Select subnodes according to the predicate
223                 #
224                 result = NodeSet()
225                 for item in nodeset :
226                         for sub in item.subs :
227                                 if matcher( sub ) :
228                                         result += process( sub )
229                                 if recurse :
230                                         result += select_( NodeSet( sub ) , path )
231
232                 return result
233
234         if not path.startswith( '/' ) :
235                 # Always assume path from the root
236                 # (It's the only possible case anyway.)
237                 path = '/' + path
238
239         nodeset = NodeSet( nodeset )
240
241         path = path.split( '/' )[ 1 : ]
242
243         def finally_( result ) :
244                 return result
245
246         if path :
247                 if path[ -1 ] == '@@@' :
248                         def finally_( result ) :
249                                 return [ ( node.name , ) + node.values for node in result ]
250                         path.pop()
251                 elif path[ -1 ] == '@@' :
252                         # FIXME: Factorize with @@@
253                         def finally_( result ) :
254                                 return [ node.values for node in result ]
255                         path.pop()
256                 elif path[ -1 ] == '@' :
257                         # FIXME: Factorize with @@ or @@@
258                         def finally_( result ) :
259                                 return sum( [ node.values for node in result ] , () )
260                         path.pop()
261                 elif path[ -1 ] == '?' :
262                         def finally_( result ) :
263                                 if len( result ) :
264                                         return ' '.join( [ str( s ) for node in result for s in node.values ] )
265                         path.pop()
266         if not path or ( len( path ) == 1 and path[ 0 ] == '' ) :
267                 result = nodeset
268         else :
269                 result = select_( nodeset , path )
270         result = finally_( result )
271
272         return result
273
274 def test() :
275
276         filename = 'vserver.conf'
277
278         import confparser
279         n = confparser.readConfigurationExt( filename )
280         if n is not None :
281                 print n[ 'default/unhide' ].pstr()
282                 print '--'
283                 print n[ 'default/unhide' ] # the nodeset
284                 print n[ 'default/unhide/?' ] # the string for the whole nodesets
285                 print n[ 'default/unhide/@' ] # the flatten list of text
286                 print n[ 'default/unhide/@@' ] # the list of list of text
287                 print n[ 'default/unhide/@@@' ] # the list of list of text, include node name
288                 print n[ 'server/@' ]
289                 print n[ 'server:m*/rootdev/@' ]
290                 print n[ '*/@@' ]
291                 print n[ '//*/@@' ]
292                 print n[ '//*/@@@' ]
293         else :
294                 print 'Unable to parse %r' % filename
295
296 if __name__ == '__main__' :
297         test()
298
299 # Local Variables:
300 # tab-width: 4
301 # python-indent: 4
302 # End: