Stricter PEP-8 conformance.
[python-bulletml.git] / bulletml / expr.py
1 """BulletML expression evaluator.
2
3 http://www.asahi-net.or.jp/~cs8k-cyu/bulletml/index_e.html
4 """
5
6 # BulletML assumes 1/2 = 0.5.
7 from __future__ import division
8
9 import random
10 import re
11
12 from bulletml.errors import Error
13
14 __all__ = ["ExprError", "NumberDef", "INumberDef"]
15
16 class ExprError(Error):
17 """Raised when an invalid expression is evaluated/compiled."""
18 pass
19
20 class NumberDef(object):
21 """BulletML numeric expression.
22
23 This translates BulletML numeric expressions into Python expressions.
24
25 Examples:
26 35
27 360/16
28 0.7 + 0.9*$rand
29 180-$rank*20
30 (2+$1)*0.3
31
32 """
33
34 GLOBALS = dict(random=random.random, __builtins__={})
35
36 def __init__(self, expr):
37 try:
38 expr = expr.string
39 except AttributeError:
40 pass
41 try:
42 if "__" in expr:
43 # nedbatchelder.com/blog/201206/eval_really_is_dangerous.html
44 raise ExprError(expr)
45 except TypeError:
46 pass
47 self.string = expr = str(expr)
48 repl = lambda match: "params[%d]" % (int(match.group()[1:]) - 1)
49 expr = re.sub(r"\$\d+", repl, expr.lower())
50 self.__expr = expr.replace("$rand", "random()").replace("$rank", "rank")
51 try:
52 try:
53 self._value = eval(self.__expr, dict(__builtins__={}))
54 except NameError:
55 variables = dict(rank=1, params=[0] * 99)
56 value = eval(self.__expr, self.GLOBALS, variables)
57 if not isinstance(value, (int, float)):
58 raise TypeError(expr)
59 self._value = None
60 self.expr = self.string
61 else:
62 self.expr = self._value
63 except Exception:
64 raise ExprError(expr)
65 self.__expr = compile(self.__expr, __file__, "eval")
66
67 def __call__(self, params, rank):
68 """Evaluate the expression and return its value."""
69 if self._value is not None:
70 return self._value
71 variables = { 'rank': rank, 'params': params }
72 return eval(self.__expr, self.GLOBALS, variables)
73
74 def __repr__(self):
75 return "%s(%r)" % (type(self).__name__, self.expr)
76
77 class INumberDef(NumberDef):
78 """A NumberDef, but returns rounded integer results."""
79 def __init__(self, expr):
80 super(INumberDef, self).__init__(expr)
81 if self._value is not None:
82 self._value = int(round(self._value))
83
84 def __call__(self, params, rank):
85 # Avoid int(round(__call__())) overhead for constants.
86 if self._value is not None:
87 return self._value
88 return int(round(super(INumberDef, self).__call__(params, rank)))