MathExpression: Safer evaluation

Updated TrustyTony 0 Tallied Votes 637 Views Share

I googled little around for correct way to not reinventing of the wheel for expression evaluation (even I had done some simple evaluation check for my Tkinter calculator snippet earlier). After getting totally upset with this example, I made this with some struggles with __builtins__ globals parameter of eval. First I kept the dictionary of variables approach, then I turned to keyword parameters, the format is so much nicer.

To put some topping, I put __call__ also as synonym for evaluation. That means you can call the class as functions and give any values of variables as keyword parameters. See example at line 94. There you can see(line 92), that you can add to allowed functions dynamically, complex failed the first test. Be sure to append, not extend if you add single function name or interesting things happen ;)

Output of test cases:

abs(-3) = 3
name 'complex' is not defined
1 + float(2) + sin(x + y) / e + pi = 5.9787984802
name 'abc' is not defined
__ and builtins not allowed
dir() = ['abs', 'acos', 'acosh', 'asin', 'asinh', 'atan', 'atan2', 'atanh', 'ceil', 'copysign', 'cos', 'cosh', 'degrees', 'dir', 'e', 'erf', 'erfc', 'exp', 'expm1', 'fabs', 'factorial', 'float', 'floor', 'fmod', 'frexp', 'fsum', 'gamma', 'hypot', 'int', 'isinf', 'isnan', 'ldexp', 'lgamma', 'log', 'log10', 'log1p', 'modf', 'pi', 'pow', 'radians', 'round', 'sin', 'sinh', 'sqrt', 'tan', 'tanh', 'trunc', 'x', 'y']
name 'g' is not defined
__ and builtins not allowed
complex(z) = (-4.3+0j)
float(x) = 6.0
# debug functions
def echo(n):
    print(n)
    return n

def nothing(n):
    return n

debug = nothing

class MathExpression(object):
    ''' Mathematical expression safe evaluator class.
        Call evaluate() function, or use the instance as function
        if you need not give extra variables. It will give the result of the
        mathematical expression given as a string when initializing.
        math library functions allowed, no __ allowed,
        additionally safe functions from globals is added (defined by class variable
        safe) and specified class method (here dir giving only math functions allowed)
        
    '''
    __slots__ = 'expression', 'math'
    safe = ['int', 'float', 'abs', 'trunc', 'round']
    
    def __init__(self, expression='', **variables):
        # exception to import first in file to protect the namespace of module
        # alternatively we could add __all__ definition in beginning of the file
        import math
        self.math = dict((a,b) for a,b in math.__dict__.items() if not a.startswith('__') and '__' not in str(b))
        self.math.update((f, val) for f, val in __builtins__.__dict__.items() if f in self.safe)
        self.math.update({'dir':self.dir})
        self.expression = expression
        if variables:
            self.math.update(variables)

    def check(self, to_check):
        " check to_check for __ names and self.math for builtins (Python3 name for __builtins__) "
        if any('__' in debug(str(n)) for n in to_check) or 'builtins' in self.math:
            raise ValueError('__ and builtins not allowed')
        return True

    def dir(self):
        return sorted(self.math.keys())

    def evaluate(self, **variables):
        """Evaluate the mathematical expression given as a string in the expression member
            variable.
       
        """
        if variables:
            self.math.update(variables)

        self.check(self.math.values())
        self.check(self.math.keys())
        self.check(MathExpression.safe)

        return eval(self.expression, {"__builtins__":None}, self.math)
        
    def __str__(self):
        " human readable format "
        return self.expression

    def __repr__(self):
        """ evaluation format
            (variables not shown)
            
        """
        return 'MathExpression(%r)' % (self.expression)

    " enable use as function, keyword parameters added to variables"
    __call__ = evaluate
    

if __name__ == "__main__":

    g = 3
    for expr in ('abs(-3)', 'complex(4)', '1 + float(2) + sin(x + y) / e + pi',
                 '1 + 2 + sin(x + y) / abc(x)',
                 "[]+__import__('os').listdir('/')", "dir()", 'g + x'):
        try:
            p = MathExpression(expr, x=2.4, y=1.2)
            print('%s = %s' % (p, p()))
        except (NameError, ValueError) as e:
            print(e)
            del p

    # update variable in evaluated epression
    #debug = echo
    try:
        p = MathExpression('builtins.complex(-4.3)')
        print('%s = %s' % (p,p(builtins = __builtins__)))
    except (NameError, ValueError) as e:
        print(e)
    
    MathExpression.safe.append('complex')
    p = MathExpression('complex(z)')
    print('%s = %s' % (p,p(z=-4.3)))
    p = MathExpression('float(x)', x=6)
    print('%s = %s' % (p, p()))