Best way to evaluate boolean expressions from strings?
Arnaud Delobelle
arnodel at googlemail.com
Tue Apr 28 16:09:36 EDT 2009
Gustavo Narea <me at 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
More information about the Python-list
mailing list