Gustavo Narea <m...@gustavonarea.net> writes: > Hello, everybody. > > I need to evaluate boolean expressions like "foo == 1" or "foo ==1 and > (bar > 2 or bar == 0)" which are defined as strings (in a database or > a plain text file, for example). How would you achieve this? > > These expressions will contain placeholders for Python objects (like > "foo" and "bar" in the examples above). Also, the Python objects that > will get injected in the expression will support at least one of the > following operations: "==", "!=", ">", "<", ">=", "<=", "&", "|", > "in". > > I don't need the ability to import modules, define classes, define > functions, etc. I just need to evaluate boolean expressions defined as > strings (using the Python syntax is fine, or even desirable). > > Here's a complete example: > > I have the "user_ip" and "valid_ips" placeholders defined in Python as > follows: > """ > user_ip = '111.111.111.111' > > class IPCollection(object): > def __init__(self, *valid_ips): > self.valid_ips = valid_ips > def __contains__(self, value): > return value in self.valid_ips > > valid_ips = IPCollection('222.222.222.222', '111.111.111.111') > """ > > So the following boolean expressions given as strings should be > evaluated as: > * "user_ip == '127.0.0.1'" ---> False > * "user_ip == '127.0.0.1' or user_ip in valid_ips" ---> True > * "user_ip not in valid_ips" ---> False > > That's it. How would you deal with this? I would love to re-use > existing stuff as much as possible, that works in Python 2.4-2.6 and > also that has a simple syntax (these expressions may not be written by > technical people; hence I'm not sure about using TALES). > > Thanks in advance!
Here is a proof of concept using the ast module (Python >= 2.6): ---------------------------------------- import ast class UnsafeError(Exception): pass class SafetyChecker(ast.NodeVisitor): def __init__(self, allowed_nodes, allowed_names): self.allowed_nodes = allowed_nodes self.allowed_names = allowed_names def visit(self, node): if isinstance(node, ast.Name): if node.id in self.allowed_names: return raise UnsafeError('unsafe name: %s' % node.id) if type(node) not in self.allowed_nodes: if not any(tp in self.allowed_nodes for tp in type(node).__bases__): raise UnsafeError('unsafe node: %s' % type(node).__name__) ast.NodeVisitor.visit(self, node) node_whitelist = [ 'Expression', 'Load', 'operator', 'unaryop', 'UnaryOp', 'BoolOp', 'BinOp', 'boolop', 'cmpop', # Operators 'Num', 'Str', 'List', 'Tuple', # Literals ] node_whitelist = [getattr(ast, name) for name in node_whitelist] checker = SafetyChecker(node_whitelist, ['a', 'b', 'foo', 'bar']) def safe_eval(expr, checker=checker): t = ast.parse(expr, 'test.py', 'eval') checker.visit(t) return eval(expr) ---------------------------------------- Example: >>> safe_eval('2*a - bar') -4 >>> safe_eval('[1, 2] + [3, 4]') [1, 2, 3, 4] >>> safe_eval('f(foo)') Traceback (most recent call last): [...] __main__.UnsafeError: unsafe node: Call >>> safe_eval('x + 1') Traceback (most recent call last): [...] __main__.UnsafeError: unsafe name: x >>> safe_eval('lambda: x') Traceback (most recent call last): [...] __main__.UnsafeError: unsafe node: Lambda >>> safe_eval('foo or not foo') 'hello' You'd have to tweak the node_whitelist using the info at http://docs.python.org/library/ast.html#abstract-grammar -- Arnaud -- http://mail.python.org/mailman/listinfo/python-list