[IMP] safe_eval: speed up evaluation

* creating the huge builtins dict inline represents ~40% of safe_eval's
  total cost according to line_profiler
* for the specific situation of opcodes, using a set difference is
  apparently much faster than for+if. all(gencomp) is also faster than
  for+if but not as much as letting Python do (almost) all of it

  might be that returning a set from _get_opcodes would be even faster,
  but didn't test it
This commit is contained in:
Xavier Morel
2015-04-10 18:23:32 +02:00
committed by Olivier Dony
parent 60ad1fec4d
commit 20b6003dce
+52 -52
View File
@@ -144,10 +144,12 @@ def assert_valid_codeobj(allowed_codes, code_obj, expr):
is found in ``code_obj``
"""
assert_no_dunder_name(code_obj, expr)
for opcode in _get_opcodes(code_obj):
if opcode not in allowed_codes:
raise ValueError(
"opcode %s not allowed (%r)" % (opname[opcode], expr))
# almost twice as fast as a manual iteration + condition when loading
# /web according to line_profiler
if set(_get_opcodes(code_obj)) - allowed_codes:
raise ValueError("forbidden opcode(s) in %r" % expr)
for const in code_obj.co_consts:
if isinstance(const, CodeType):
assert_valid_codeobj(allowed_codes, const, 'lambda')
@@ -227,7 +229,44 @@ def _import(name, globals=None, locals=None, fromlist=None, level=-1):
if name in _ALLOWED_MODULES:
return __import__(name, globals, locals, level)
raise ImportError(name)
_BUILTINS = {
'__import__': _import,
'True': True,
'False': False,
'None': None,
'str': str,
'unicode': unicode,
'bool': bool,
'int': int,
'float': float,
'long': long,
'enumerate': enumerate,
'dict': dict,
'list': list,
'tuple': tuple,
'map': map,
'abs': abs,
'min': min,
'max': max,
'sum': sum,
'reduce': reduce,
'filter': filter,
'round': round,
'len': len,
'repr': repr,
'set': set,
'all': all,
'any': any,
'ord': ord,
'chr': chr,
'cmp': cmp,
'divmod': divmod,
'isinstance': isinstance,
'range': range,
'xrange': xrange,
'zip': zip,
'Exception': Exception,
}
def safe_eval(expr, globals_dict=None, locals_dict=None, mode="eval", nocopy=False, locals_builtins=False):
"""safe_eval(expression[, globals[, locals[, mode[, nocopy]]]]) -> result
@@ -245,12 +284,9 @@ def safe_eval(expr, globals_dict=None, locals_dict=None, mode="eval", nocopy=Fal
:throws NameError: If the expression provided accesses forbidden names
:throws ValueError: If the expression provided uses forbidden bytecode
"""
if isinstance(expr, CodeType):
if type(expr) is CodeType:
raise TypeError("safe_eval does not allow direct evaluation of code objects.")
if globals_dict is None:
globals_dict = {}
# prevent altering the globals/locals from within the sandbox
# by taking a copy.
if not nocopy:
@@ -260,55 +296,19 @@ def safe_eval(expr, globals_dict=None, locals_dict=None, mode="eval", nocopy=Fal
_logger.warning(
"Looks like you are trying to pass a dynamic environment, "
"you should probably pass nocopy=True to safe_eval().")
globals_dict = dict(globals_dict)
if globals_dict is not None:
globals_dict = dict(globals_dict)
if locals_dict is not None:
locals_dict = dict(locals_dict)
globals_dict.update(
__builtins__={
'__import__': _import,
'True': True,
'False': False,
'None': None,
'str': str,
'unicode': unicode,
'bool': bool,
'int': int,
'float': float,
'long': long,
'enumerate': enumerate,
'dict': dict,
'list': list,
'tuple': tuple,
'map': map,
'abs': abs,
'min': min,
'max': max,
'sum': sum,
'reduce': reduce,
'filter': filter,
'round': round,
'len': len,
'repr': repr,
'set': set,
'all': all,
'any': any,
'ord': ord,
'chr': chr,
'cmp': cmp,
'divmod': divmod,
'isinstance': isinstance,
'range': range,
'xrange': xrange,
'zip': zip,
'Exception': Exception,
}
)
if globals_dict is None:
globals_dict = {}
globals_dict['__builtins__'] = _BUILTINS
if locals_builtins:
if locals_dict is None:
locals_dict = {}
locals_dict.update(globals_dict.get('__builtins__'))
locals_dict.update(_BUILTINS)
c = test_expr(expr, _SAFE_OPCODES, mode=mode)
try:
return eval(c, globals_dict, locals_dict)