1515from collections .abc import Mapping , Sequence
1616from typing import Any
1717
18+ import numpy as np
19+
1820__all__ = ["SAFE_TYPES" , "safe_eval" ]
1921
2022# default set of safe AST node types
3739)
3840
3941
42+ class _RewriteConstNp (ast .NodeTransformer ):
43+ """Replaces int and float constants in the tree with those wrapped in Numpy types."""
44+ def __init__ (self , int_type_str : str , float_type_str : str ):
45+ self .int_type_str = int_type_str
46+ self .float_type_str = float_type_str
47+
48+ def visit_Constant (self , node ):
49+ if isinstance (node .value , (int , float )):
50+ type_str = self .int_type_str if isinstance (node .value , int ) else self .float_type_str
51+ return ast .parse (f"{ type_str } ({ node .value } )" )
52+
53+ return node
54+
55+
4056def safe_eval (
4157 expr : str ,
4258 globals_vars : Mapping [str , Any ] | None = None ,
4359 locals_vars : Mapping [str , object ] | None = None ,
4460 allowed_types : Sequence [type ] = SAFE_TYPES ,
61+ rewrite_np : bool = False ,
62+ int_type_str : str = "np.int32" ,
63+ float_type_str : str = "np.float32" ,
4564) -> Any :
4665 """
4766 Evaluate the Python expression `expr` using `eval`, but only if it is a safe expression in that its parsed AST
@@ -50,11 +69,20 @@ def safe_eval(
5069 expressions with constants and names can be evaluated, so excludes attribute access, indexing, and calls. Code
5170 injection is infeasible through such expressions, so this is a safe and secure way of evaluating simple expressions.
5271
72+ If `rewrite_np` is True, int and float constants in the given expression will be wrapped with Numpy types as given
73+ by `int_type_str` and `float_type_str`. These are expected to be constructor names prefixed with `np.` as Numpy
74+ will be present in the expression global variables under that name. The values can be changed to other types if
75+ needed, such as "int64". One advantage of doing this is to avoid denial-of-service attacks by attempting to evaluate
76+ an expressoini which is incredibly slow under native Python but fast (though potentially erroneous) under Numpy.
77+
5378 Args:
5479 expr: expression to evaluate, this will be stripped before parsing to avoid indentation complaints
5580 globals_vars: global variable mapping, this will be treated as read-only for this function, unlike `eval`
5681 locals_vars: local variable mapping
5782 allowed_types: sequence of allowed AST types which can be found in `expr` when parsed
83+ rewrite_np: if True, wrap int or float literals in Numpy types
84+ int_type_str: int Numpy wrapping type string
85+ float_type_str: float Numpy wrapping type string
5886
5987 Raises:
6088 ValueError: raised when any node in the AST parsed from `expr` has a type not in `allowed_types`
@@ -70,4 +98,8 @@ def safe_eval(
7098 if disallowed :
7199 raise ValueError (f"Unsafe expression `{ expr } ` not evaluated, contains disallowed components: { disallowed } " )
72100
101+ if rewrite_np :
102+ parsed = _RewriteConstNp (int_type_str , float_type_str ).visit (parsed )
103+ locals_vars = {"np" : np , ** (locals_vars or {})}
104+
73105 return eval (expr , dict (globals_vars ) if globals_vars else None , locals_vars )
0 commit comments