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