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