]> mj.ucw.cz Git - eval.git/blob - t/moe/config_parser.py
6dec8c9cd2a13d91e6a91b5ba32c5d7a8c933a7e
[eval.git] / t / moe / config_parser.py
1 r"""
2 Simple Moe configuration file syntax parser. 
3
4 Generally, whitespace and comments are alowed everywhere except in variable names and inside expressions. 
5 Also, COMMENT must not contain '\n'. 
6
7 FILE, BLOCK, STATEMENT, OPERATION, SUBTREE, CONDITION, FORMULA, AND, OR and NOT eat any preceding whitespace. 
8
9 The configuration syntax is the following::
10
11     FILE = BLOCK 
12     BLOCK = WS | STATEMENT ( SEP STATEMENT )* 
13
14     SEP = ( '\n' | ';' )
15     WS = ( ' ' | '\t' | '\n' | COMMENT )*
16
17     COMMENT = re('#[^\n]*\n')
18
19     STATEMENT = CONDITION | OPERATION | SUBTREE
20
21     OPERATION = WS VARNAME WS ( '=' | '+=' ) WS EXPRESSION
22     SUBTREE = WS VARNAME WS '{' BLOCK WS '}'
23     CONDITION = WS 'if' FORMULA WS '{' BLOCK WS '}'
24
25     FORMULA = WS (( EXPRESSION WS ( '!=' | '==' ) WS EXPRESSION ) | '(' AND WS ')' | '(' OR WS ')' | NOT )
26     AND = FORMULA WS 'and' FORMULA
27     OR = FORMULA WS 'or' FORMULA
28     NOT = WS 'not' FORMULA 
29
30     EXPRESSION = '"' ( ECHAR | '{' VARNAME '}' )* '"' | re"'[^'\n]*'" | VARNAME
31     ECHAR = re('([^\{}]|\\|\{|\}|\\n)*')
32     VARNAME = re('[a-zA-Z0-9-_]+(\.[a-zA-Z0-9-_]+)*')
33
34 .. todo:: should whitespace (incl. '\n') be allowed (almost) everywhere? 
35           can comment be anywhere whitespace can?
36 .. note:: ';' or '\n' is currently required even after CONDITION and SUBTREE block 
37 .. todo:: change to OPERATION only
38 .. note:: Formula can contain additional/unnecessary parentheses
39 """
40
41 import re, types, itertools, logging as log
42 import traceback
43
44 import moe.config as cf
45
46
47 class ConfigSyntaxError(cf.ConfigError):
48
49   def __init__(self, msg, source='<unknown>', line=None, column=None):
50     self.msg = msg
51     self.source = source
52     self.line = line
53     self.column = column
54
55   def __str__(self):
56     return('ConfigSyntaxError %s:%d:%d: %s'%(self.source, self.line, self.column, self.msg))
57
58
59 class ConfigParser(object):
60   c_varname_sep = u'.'
61   c_comment = u'#'
62   c_open = u'{'
63   c_close = u'}'
64   c_ws = u' \t\n'
65   c_sep = u';\n'
66   c_nl = u'\n'
67   c_if = u'if'
68   c_and = u'and'
69   c_or = u'or'
70   c_not = u'not'
71   c_eq = u'=='
72   c_neq = u'!='
73   c_set = u'='
74   c_append = u'+='
75
76   def __init__(self, s, tree, source='<unknown>', level=0):
77     """Create a config file parser. 
78     `s` is either a string, unicode or an open file. File is assumed to be utf-8, string is converted to unicode.
79     `tree` is a ConfigTree to fill the operations into.
80     `source` is an optional name of the file, for debugging and syntax errors. 
81     `level` indicates the precedence the operations should have in the ConfigTree
82     """
83     self.s = s          # Unicode, ascii string or an open file
84     self.buf = u""      # Read-buffer for s file, whole unicode string for s string/unicode
85     if isinstance(self.s, types.StringTypes):
86       self.buf = unicode(self.s)
87     elif (not isinstance(self.s, file)) or self.s.closed:
88       raise TypeError("Expected unicode, str or open file.")
89     self.bufpos = 0
90     self.source = source        # Usually filename
91     self.line = 1       
92     self.column = 1
93     self.tree = tree    # ConfTree to fill
94     self.level = level  # level of the parsed operations
95     self.prefix = ''    # Prefix of variable name, may begin with '.'
96     self.conditions = []        # Stack of nested conditions, these are chained, so only the last is necessary
97     self.read_ops = []  # List of parsed operations (varname, `Operation`), returned by `self.parse()`
98
99   def preread(self, l):
100     "Make sure buf contains at least `l` next characters, return True on succes and False on hitting EOF."
101     if isinstance(self.s, file):
102       self.buf = self.buf[self.bufpos:] + self.s.read(max(l, 1024)).decode('utf8')
103       self.bufpos = 0
104     return len(self.buf) >= self.bufpos + l
105
106   def peek(self, l = 1):
107     "Peek and return next `l` unicode characters or everything until EOF."
108     self.preread(l)
109     return self.buf[self.bufpos:self.bufpos+l]
110
111   def peeks(self, s):
112     "Peek and compare next `len(s)` characters to `s`. Converts `s` to unicode. False on hitting EOF."
113     s = unicode(s)
114     return self.peek(len(s)) == s
115
116   def next(self, l = 1):
117     "Eat and return next `l` unicode characters. Raise exception on EOF."
118     if not self.preread(l):
119       self.syntax_error("Unexpected end of file")
120     s = self.buf[self.bufpos:self.bufpos+l]
121     self.bufpos += l
122     rnl = s.rfind('\n')
123     if rnl<0:
124       # no newline
125       self.column += l
126     else:
127       # some newlines
128       self.line += s.count('\n')
129       self.column = l - rnl - 1 
130     return s
131
132   def nexts(self, s):
133     """Compare next `len(s)` characters to `s`. On match, eat them and return True. Otherwise just return False. 
134     Converts `s` to unicode. False on hitting EOF."""
135     s = unicode(s)
136     if self.peeks(s):
137       self.next(len(s))
138       return True
139     return False
140
141   def eof(self):
142     "Check for end-of-stream."
143     return not self.preread(1)
144
145   def expect(self, s, msg=None):
146     "Eat and compare next `len(s)` characters to `s`. If not equal, raise an error with `msg`. Unicode."
147     s = unicode(s)
148     if not self.nexts(s): 
149       self.syntax_error(msg or u"%r expected."%(s,))
150
151   def syntax_error(self, msg, *args):
152     "Raise a syntax error with file/line/column info"
153     raise ConfigSyntaxError(source=self.source, line=self.line, column=self.column, msg=(msg%args))
154
155   def dbg(self):
156     n = None; s = ''
157     for i in traceback.extract_stack():
158       if i[2][:2]=='p_': 
159         s += ' '
160         n = i[2]
161     if n: log.debug(s + n + ' ' + repr(self.peek(15)) + '...')
162
163   def parse(self):
164     self.read_ops = []
165     self.p_BLOCK()
166     return self.read_ops
167
168   def p_BLOCK(self):
169     self.dbg() # Debug
170     self.p_WS()
171     while (not self.eof()) and (not self.peeks(self.c_close)):
172       self.p_STATEMENT()
173       l0 = self.line
174       self.p_WS()
175       if self.eof() or self.peeks(self.c_close):
176         break
177       if self.line == l0: # No newline skipped in p_WS
178         self.expect(';')
179       else:
180         self.nexts(';') # NOTE: this is weird - can ';' occur anywhere? Or at most once, but only after any p_WS debris?
181       self.p_WS()
182
183   def p_WS(self):
184     self.dbg() # Debug
185     while not self.eof():
186       if self.peek() in self.c_ws:
187         self.next()
188       elif self.peeks(self.c_comment):
189         self.p_COMMENT()
190       else:
191         break
192
193   def p_COMMENT(self):
194     self.dbg() # Debug
195     self.expect(self.c_comment, "'#' expected at the beginning of a comment.")
196     while (not self.eof()) and (not self.nexts(self.c_nl)):
197       self.next(1)
198
199   def p_STATEMENT(self):
200     self.dbg() # Debug
201     self.p_WS()
202     if self.peeks(self.c_if):
203       self.p_CONDITION()
204     else:
205       # for operation or subtree, read VARNAME
206       varname = self.p_VARNAME()
207       self.p_WS()
208       if self.peeks(self.c_open):
209         self.p_SUBTREE(varname)
210       else:
211         self.p_OPERATION(varname)
212
213   def p_SUBTREE(self, varname=None):
214     self.dbg() # Debug
215     if not varname:
216       self.p_WS()
217       varname = self.p_VARNAME()
218     self.p_WS()
219     self.expect(self.c_open)
220     # backup and extend the variable name prefix 
221     p = self.prefix
222     self.prefix = p + self.c_varname_sep + varname
223     self.p_BLOCK()
224     self.prefix = p
225     # close block and 
226     self.p_WS()
227     self.expect(self.c_close)
228
229   def p_OPERATION(self, varname=None):
230     self.dbg() # Debug
231     if not varname:
232       self.p_WS()
233       varname = self.p_VARNAME()
234     self.p_WS()
235     if self.nexts(self.c_set):
236       op = 'SET'
237     elif self.nexts(self.c_append):
238       op = 'APPEND'
239     elif self.eof():
240       self.syntax_error('Unexpected end of file.')
241     else:
242       self.syntax_error('Unknown operation: %r...', self.peek(10))
243     self.p_WS()
244     exp = self.p_EXPRESSION()
245     vname = (self.prefix+self.c_varname_sep+varname).lstrip(self.c_varname_sep)
246     v = self.tree.lookup(vname)
247     if self.conditions:
248       cnd = self.conditions[-1]
249     else:
250       cnd = None
251     op = cf.Operation(op, cnd, exp, level=self.level,
252           source="%s:%d:%d"%(self.source, self.line, self.column))
253     # NOTE/WARNING: The last character of operation will be reported in case of error.  
254     v.add_operation(op) 
255     self.read_ops.append( (vname, op) )
256
257   def p_CONDITION(self):
258     self.dbg() # Debug
259     self.p_WS()
260     t = u"condition at %s:%d:%d"%(self.source, self.line, self.column)
261     self.expect(self.c_if)
262     self.p_WS()
263     f = self.p_FORMULA()
264     cnd = cf.ConfigCondition(f, text=t, parent=(self.conditions and self.conditions[-1]) or None)
265     self.conditions.append(cnd)
266     # Parse a block
267     self.p_WS()
268     self.expect(self.c_open)
269     self.p_BLOCK()
270     self.p_WS()
271     self.expect(self.c_close)
272     # Cleanup
273     self.conditions.pop()
274
275   def p_VARNAME(self):
276     self.dbg() # Debug
277     vnl = []
278     while self.preread(1) and (self.peek().isalnum() or self.peek() in u'-_.'):
279       vnl.append(self.next())
280     vn = u''.join(vnl)
281     if not cf.re_VARNAME.match(vn):
282       self.syntax_error('Invalid variable name %r', vn)
283     return vn
284
285   def p_EXPRESSION(self):
286     self.dbg() # Debug
287     if self.peek() not in '\'"':
288       # Expect a variable name 
289       varname = self.p_VARNAME()
290       return cf.ConfigExpression((self.tree.lookup(varname),), varname)
291     op = self.next()
292     # Parse literal expression 
293     if op == u'\'':
294       exl = []
295       while not self.peeks(op):
296         exl.append(self.next())
297       self.expect(op)
298       s = u''.join(exl)
299       return cf.ConfigExpression((s,), s)
300     # Parse expression with variables
301     exl = [op]
302     expr = []
303     while not self.peeks(op):
304       exl.append(self.peek())
305       if self.nexts(u'\\'):
306         # Escape sequence
307         c = self.next()
308         if c not in u'\\"n' + self.c_open + self.c_close:
309           self.syntax_error('Illeal escape sequence in expression')
310         if c == 'n':
311           expr.append(u'\n')
312         else:
313           expr.append(c)
314         exl.append(c)
315       elif self.nexts(self.c_open):
316         # Parse a variable name in '{}'
317         varname = self.p_VARNAME()
318         self.expect(self.c_close)
319         exl.append(varname)
320         expr.append(self.tree.lookup(varname))
321       else:
322         # Regular character
323         expr.append(self.next())
324     self.expect(op)
325     exs = ''.join(exl)
326     # Concatenate consecutive characters in expr
327     expr2 = []
328     for i in expr:
329       if expr2 and isinstance(expr2[-1], unicode) and isinstance(i, unicode):
330         expr2[-1] = expr2[-1] + i
331       else:
332         expr2.append(i)
333     return cf.ConfigExpression(expr2, exs)
334
335   def p_FORMULA(self):
336     self.dbg() # Debug
337     self.p_WS()
338     # Combined logical formula
339     if self.nexts(u'('):
340       f1 = self.p_FORMULA()
341       self.p_WS()
342       if self.nexts(self.c_and):
343         if self.peek(1).isalnum():
344           self.syntax_error('trailing characters after %r', self.c_and)
345         f2 = self.p_FORMULA()
346         self.p_WS()
347         self.expect(u')')
348         return ('AND', f1, f2)
349       elif self.nexts(self.c_or):
350         if self.peek(1).isalnum():
351           self.syntax_error('trailing characters after %r', self.c_or)
352         f2 = self.p_FORMULA()
353         self.p_WS()
354         self.expect(u')')
355         return ('OR', f1, f2)
356       elif self.nexts(u')'):
357         # Only extra parenthes
358         return f1
359       else:
360         self.syntax_error("Logic operator or ')' expected")
361     elif self.nexts(self.c_not):
362       if self.peek().isalnum():
363         self.syntax_error('trailing characters after %r', self.c_not)
364       # 'not' formula
365       f = self.p_FORMULA()
366       return ('NOT', f)
367     else:
368       # Should be (in)equality condition
369       e1 = self.p_EXPRESSION()
370       self.p_WS()
371       if self.nexts(self.c_eq):
372         self.p_WS()
373         e2 = self.p_EXPRESSION()
374         return ('==', e1, e2)
375       elif self.nexts(self.c_neq):
376         self.p_WS()
377         e2 = self.p_EXPRESSION()
378         return ('!=', e1, e2)
379       else:
380         self.syntax_error("Comparation operator expected")
381