simpleeval

repository·main·Indexed 20 days ago

https://github.com/danthedeckie/simpleeval

A lightweight Python library for safely evaluating single expressions using the ast module. It provides a secure alternative to eval(), supporting standard arithmetic, logical expressions, ternary conditionals, and custom functions. Features include the SimpleEval class for repeated evaluations, EvalWithCompoundTypes for list comprehensions, and ModuleWrapper for safely exposing modules. Users can customize operators, manage variable names via dictionaries or handler functions, and implement opt-in security models using allowed_attrs.

Tokens
2.9K
Snippets
12
Records
12
Agent score
20%

What's inside simpleeval

  1. Enable support for compound types and comprehensions

    main

    By default, SimpleEval supports compound types (dict, tuple, list, set) if they are passed in via names.

    To allow the creation of these types within expressions (e.g., using list comprehensions like [x + 1 for x in [1,2,3]]), use the EvalWithCompoundTypes class instead of SimpleEval. This class includes a safety mechanism MAX_COMPREHENSION_LENGTH to prevent resource exhaustion from deeply nested or overly large comprehensions.

    from simpleeval import EvalWithCompoundTypes
    
    s = EvalWithCompoundTypes()
    s.eval("[x + 1 for x in [1, 2, 3]]")
  2. Secure attribute access with allowed_attrs

    main

    For a safer 'opt-in' security model, use the allowed_attrs parameter. When provided, all attribute access is denied unless explicitly listed in the allowlist.

    • Use BASIC_ALLOWED_ATTRS to enable a set of sensible, safe defaults (like .strip() for strings).
    • You can provide a mapping where keys are classes and values are sets of allowed attribute names for those classes.
    from simpleeval import simpleeval, BASIC_ALLOWED_ATTRS
    
    # 1. Disable all attribute access
    simpleeval("' hello '.strip()", allowed_attrs={})
    
    # 2. Use safe defaults
    simpleeval("' hello '.strip()", allowed_attrs=BASIC_ALLOWED_ATTRS)
    
    # 3. Granular control for custom classes
    class Foo:
        bar = 42
        hidden = "secret"
    
    our_attributes = BASIC_ALLOWED_ATTRS.copy()
    our_attributes[Foo] = {'bar'}
    
    # This works:
    simpleeval("foo.bar", names={"foo": Foo()}, allowed_attrs=our_attributes)
    
    # This raises FeatureNotAvailable:
    simpleeval("foo.hidden", names={"foo": Foo()}, allowed_attrs=our_attributes)
  3. Basic Usage of simple_eval

    main

    The simple_eval function provides a quick way to evaluate mathematical or logical expressions safely. It supports standard arithmetic, complex nested expressions, and custom functions.

    from simpleeval import simple_eval
    
    # Simple arithmetic
    result = simple_eval("21 + 21") # returns 42
    
    # Complex expressions
    result = simple_eval("21 + 19 / 7 + (8 % 3) ** 9") # returns 535.714285714
    
    # Using custom functions
    result = simple_eval("square(11)", functions={"square": lambda x: x*x}) # returns 121
    from simpleeval import simple_eval
    
    simple_eval("21 + 21")
    
    simple_eval("21 + 19 / 7 + (8 % 3) ** 9")
    
    simple_eval("square(11)", functions={"square": lambda x: x*x})
  4. Configure attribute access fallback (dot notation)

    main

    By default, simpleeval enables a 'sweetener' that allows accessing dictionary keys using dot notation (e.g., foo.bar where foo is {'bar': 42}).

    To disable this behavior, set the module global ATTR_INDEX_FALLBACK = False or set it on a specific instance: evaller.ATTR_INDEX_FALLBACK = False.

    # Using the dot notation sweetener
    simple_eval("foo.bar", names={"foo": {"bar": 42}})
    # Returns 42
  5. Use If Expressions

    main

    You can use Python-style ternary conditional expressions (x if condition else y) within your evaluated strings. These can be nested.

    from simpleeval import simple_eval
    
    # Basic if expression
    result = simple_eval("'equal' if x == y else 'not equal'", names={"x": 1, "y": 2})
    
    # Nested if expressions
    result = simple_eval("'a' if 1 == 2 else 'b' if 2 == 3 else 'c'") # returns 'c'
    simple_eval("'equal' if x == y else 'not equal'", names={"x": 1, "y": 2})
    simple_eval("'a' if 1 == 2 else 'b' if 2 == 3 else 'c'")
  6. Define and Use Custom Functions

    main

    You can provide a dictionary of functions to the functions argument to make them available within the expression.

    If you want to use the default functions (like int(), float(), str(), randint(), and rand()) alongside your own, you should copy simpleeval.DEFAULT_FUNCTIONS and update it.

    import simpleeval
    from simpleeval import simple_eval
    
    # Using a lambda
    simple_eval("double(21)", functions={"double": lambda x: x*2})
    
    # Using a real function and aliasing it
    def double(x):
        return x * 2
    
    simple_eval("d(100) + double(1)", functions={"d": double, "double": double})
    
    # Extending default functions
    my_functions = simpleeval.DEFAULT_FUNCTIONS.copy()
    my_functions.update({
        "square": lambda x: x*x,
        "double": lambda x: x+x,
    })
    simple_eval("square(randint(100))", functions=my_functions)
    my_functions = simpleeval.DEFAULT_FUNCTIONS.copy()
    my_functions.update(
        square=(lambda x:x*x),
        double=(lambda x:x+x),
    )
    simple_eval('square(randint(100))', functions=my_functions)
  7. Use the SimpleEval class for repeated evaluations

    main

    For high-frequency evaluations, instantiate a SimpleEval object instead of calling the simple_eval function repeatedly. This allows you to reuse configuration and, more importantly, parse an expression once and evaluate it multiple times with different names to improve performance.

    To optimize performance, use s.parse(expression) to get a parsed tree, then pass that tree to s.eval(expression, previously_parsed=parsed) in a loop while updating s.names.

    # Set up & Cache the parse tree:
    expression = "foo + bar"
    s = SimpleEval()
    parsed = s.parse(expression)
    
    # evaluate the expression multiple times:
    for names in [{"foo": 1, "bar": 10}, {"foo": 100, "bar": 42}]:
        s.names = names
        print(s.eval(expression, previously_parsed=parsed))
  8. Configure SimpleEval via object attributes

    main

    You can configure a SimpleEval instance either during initialization or by modifying its attributes after creation. Common attributes to modify include functions and names.

    Note: Because names and functions are dictionaries, you can even define functions within the expression that modify the evaluator's own state (e.g., a set function).

    # Configuration during creation
    s = SimpleEval(functions={"boo": boo})
    
    # Configuration after creation
    s.names['fortytwo'] = 42
    
    # Advanced: modifying state via an expression
    s = SimpleEval()
    def set_val(name, value):
        s.names[name.value] = value.value
        return value.value
    
    s.functions = {'set': set_val}
    s.eval("set('age', 111)")
  9. Customize Operators in SimpleEval

    main

    By default, simpleeval supports standard Python operators (arithmetic, comparison, bitwise, and in). You can override or add new operators by using the SimpleEval class and modifying its operators dictionary, which maps AST nodes to callable functions.

    For example, to change the ^ operator from bitwise XOR to a power function:

    import ast
    import operator
    from simpleeval import SimpleEval, safe_power
    
    s = SimpleEval()
    s.operators[ast.BitXor] = safe_power
    result = s.eval("3 ^ 2") # returns 9

    Security Note: The ** (power) operator is limited by default to prevent DOS attacks. You can adjust this limit by modifying the simpleeval.MAX_POWER module-level value. To remove all limits, you can map ast.Pow to operator.pow.

    import ast
    from simpleeval import SimpleEval, safe_power
    
    s = SimpleEval()
    s.operators[ast.BitXor] = safe_power
    s.eval("3 ^ 2")
  10. Safely expose modules using ModuleWrapper

    main

    Direct module access is disallowed by default. To safely expose a module (like os.path or numpy), wrap it in ModuleWrapper. This enforces restrictions on private attributes (starting with _) and methods listed in DISALLOW_METHODS even if you use an allowlist.

    from simpleeval import SimpleEval, ModuleWrapper
    import os.path
    
    # Basic module exposure
    s = SimpleEval(names={'path': ModuleWrapper(os.path)})
    s.eval("path.exists('/etc/passwd')")
    
    # Restricted module exposure
    s = SimpleEval(names={
        'path': ModuleWrapper(os.path, allowed_attrs={'exists', 'join'})
    })
    s.eval("path.exists('/etc/passwd')") # Works
    s.eval("path.dirname('/etc/passwd')") # Raises FeatureNotAvailable
  11. Extend SimpleEval by subclassing

    main

    You can extend SimpleEval by overriding internal methods. For example, to create an evaluator that explicitly forbids method calls (attribute access on functions), override _eval_call and check the AST node type.

    import ast
    import simpleeval
    
    class EvalNoMethods(simpleeval.SimpleEval):
        def _eval_call(self, node):
            if isinstance(node.func, ast.Attribute):
                raise simpleeval.FeatureNotAvailable("No methods please, we're British")
            return super(EvalNoMethods, self)._eval_call(node)
  12. Manage Variable Names

    main

    Variables in expressions are called 'names'. You can provide them in two ways:

    1. A dictionary: Pass a mapping of names to values via the names argument.
    2. A handler function: Pass a callable that takes an AST node and returns a value. This is useful for dynamic lookups (e.g., from a database).

    Important Behavior: If a name is not found in the names lookup, simpleeval will attempt to look it up in the functions dictionary. If you want the name handler to explicitly signal that a name is missing, it must raise simpleeval.NameNotDefined.

    Note on Defaults: True and False are provided by default. If you provide your own names dictionary, you should copy and update simpleeval.DEFAULT_NAMES to preserve them.

    from simpleeval import simple_eval, NameNotDefined
    
    # Using a dictionary
    simple_eval("a + b", names={"a": 11, "b": 100})
    
    # Using a handler function
    def name_handler(node):
        if node.id == 'a':
            return 21
        raise NameNotDefined(node.id, "Not found")
    
    simple_eval('a + a', names=name_handler, functions={"b": 100})
    simple_eval("a + b", names={"a": 11, "b": 100})
    
    def name_handler(node):
        if node.id == 'a':
            return 21
        raise NameNotDefined(node.id, "Not found")
    
    simple_eval('a + a', names=name_handler, functions={"b": 100})