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