1d093197a61863649b0e4e4470ff2d88151623a1
[confparser-old] / confparser.py
1 # -*- coding: iso-8859-1 -*-
2
3 #
4 # Configuration parser
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 import re
23 import types
24 import sys
25 import os
26 import stat
27 import errno
28
29 try :
30         import cPickle as pickle
31 except :
32         import pickle
33
34 import basicparser
35 import basicvalidator
36
37 class Error( Exception ) : pass
38
39 #
40 # Configuration parser check *syntax*.
41 # Validator check *structure*, and eventually some values.
42 #
43
44 #--[ Parser ]-----------------------------------------------------------------
45
46 def parseString( s ) :
47
48         '''Strip string delimiters.'''
49
50         if s.startswith( "'" ) :
51                 return s[ 1 : -1 ]
52         elif s.startswith( '"' ) :
53                 return s[ 1 : -1 ]
54         else :
55                 return s
56
57 def parseConf( p , meta = None ) :
58
59         def parseNode() :
60
61                 x , y = p.x , p.y
62                 #
63                 # Node name
64                 #
65                 t = p.next( 'keyword' )
66                 kw = t[ 1 ]
67                 #
68                 # Values
69                 #
70                 values = []
71                 while 1 :
72                         t = p.next( 'string' , 'integer' , '{' , ';' )
73                         if t[ 0 ] in [ '{' , ';' ] :
74                                 break
75                         if t[ 0 ] == 'integer' :
76                                 values.append( int( t[ 1 ] ) )
77                         else :
78                                 values.append( parseString( t[ 1 ] ) )
79                 #
80                 # Contents
81                 #
82                 subNodes = []
83                 if t[ 0 ] == '{' :
84                         subNodes = []
85                         while not p.snext( '}' ) :
86                                 r = parseNode()
87                                 subNodes.append( r )
88                 return ( kw , values , subNodes , ( y , x , meta ) )
89
90         nodes = []
91         #
92         # Parse the entire file
93         #
94         while not p.snext( 'eot' ) :
95                 r = parseNode()
96                 if not r :
97                         break
98                 nodes.append( r )
99         return ('__root__',None,nodes,None)
100
101 def parse( doc , filename = None ) :
102
103         tokenMatches = {
104                 'eot'     : '$' ,
105                 'blank'   : r'\s+' ,
106                 'integer' : r'[0-9]+' ,
107                 'keyword' : r'[_a-zA-Z][_a-zA-Z0-9]*' ,
108                 'string'  : r'[_a-zA-Z][_a-zA-Z0-9]*|\'(?:[^\\\']|\\.)*\'' ,
109                 'comment' : r'#[^\n]*(?:\n|$)' ,
110                 '{'       : '{' ,
111                 '}'       : '}' ,
112                 ';'       : ';'
113         }
114         p = basicparser.Parser( tokenMatches , doc )
115         p.ignore( 'blank' , 'comment' )
116         try :
117                 result = parseConf( p , filename )
118         except basicparser.ParserError , e :
119                 msg = p.point()
120                 msg += str( e )
121                 raise Error( msg )
122         return result
123
124 #--[ Read&Write configuration ]-----------------------------------------------
125
126 def lastModificationTime( filename ) :
127
128         try :
129                 result = os.stat( filename )[ stat.ST_MTIME ]
130         except :
131                 result = None
132         return result
133
134 #
135 # Return None | ( tree , isValid )
136 #
137 def readCachedConfiguration( filename ) :
138
139         result = None
140         cachedFilename = filename + '.cache'
141         #
142         # Check if cached file is older than the source.
143         #
144         dateCached = lastModificationTime( cachedFilename )
145         if dateCached is not None :
146                 dateSource = lastModificationTime( filename )
147                 if dateSource is not None and dateCached > dateSource :
148                         try :
149                                 result = pickle.load( open( cachedFilename ) )
150                         except :
151                                 pass
152         return result
153
154 def writeCachedConfiguration( filename , tree , isValid ) :
155
156         try :
157                 pickle.dump( ( tree , isValid ) , open( filename + '.cache' , 'w' ) )
158         except :
159                 pass
160
161 def readConfiguration( filename , validator = None ) :
162
163         try :
164                 #
165                 # 1. Read from cache file
166                 #
167                 r = readCachedConfiguration( filename )
168                 cached = False
169                 if r :
170                         conf , isValid = r
171                         cached = True
172                 else :
173                         isValid = False
174                         #
175                         # 2. Parse the file
176                         #
177                         conf = open( filename ).read()
178                         conf = parse( conf , filename )
179                 if not isValid :
180                         #
181                         # 3. Validate it
182                         #
183                         if validator is not None :
184                                 basicvalidator.validate( conf , validator )
185                                 isValid = True
186                 #
187                 # 4. Keep cached result
188                 #
189                 writeCachedConfiguration( filename , conf , isValid )
190         except IOError , e :
191                 if e[ 0 ] == errno.ENOENT :
192                         conf = None
193                 else :
194                         raise Exception( 'While reading file %s:\n%s' % ( filename , str( e ) ) )
195         except Exception , e :
196                 raise Exception( 'While reading file %s:\n%s' % ( filename , str( e ) ) )
197         return conf
198
199 #--[ Dump configuration tree ]------------------------------------------------
200
201 def printTreeInner( t , prt = sys.stdout.write , prefix = '' ) :
202
203         prt( prefix )
204         prt( t[ 0 ] )
205         for kw in t[ 1 ] :
206                 prt( ' ' + kw )
207         if t[ 2 ] :
208                 prt( ' {' )
209         else :
210                 prt( ' ;' )
211         if t[ 3 ] :
212                 prt( ' # ' )
213                 if t[ 3 ][ 2 ] :
214                         prt( '%s:' % t[ 3 ][ 2 ] )
215                 prt( '%s:%s' % ( t[ 3 ][ 0 ] , t[ 3 ][ 1 ] ) )
216         prt( '\n' )
217
218         if t[ 2 ] :
219                 for sub in t[ 2 ] :
220                         printTreeInner( sub , prt , prefix + '  ' )
221                 prt( prefix )
222                 prt( '}\n' )
223
224 def printTree( t ) :
225
226         for sub in t[ 2 ] or [] :
227                 printTreeInner( sub )