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