Merge pull request #33391 from ankush/perf/safe_exec

perf: ~10x faster server script execution
This commit is contained in:
Ankush Menat 2025-07-19 19:45:06 +05:30 committed by GitHub
commit 996843f11b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -31,6 +31,7 @@ from frappe.model.mapper import get_mapped_doc
from frappe.model.rename_doc import rename_doc
from frappe.modules import scrub
from frappe.utils.background_jobs import enqueue, get_jobs
from frappe.utils.caching import site_cache
from frappe.utils.number_format import NumberFormat
from frappe.utils.response import json_handler
from frappe.website.utils import get_next_link, get_toc
@ -81,7 +82,7 @@ class FrappePrintCollector(PrintCollector):
def is_safe_exec_enabled() -> bool:
# server scripts can only be enabled via common_site_config.json
return bool(frappe.get_common_site_config(cached=bool(frappe.request)).get(SAFE_EXEC_CONFIG_KEY))
return bool(frappe.get_common_site_config(cached=True).get(SAFE_EXEC_CONFIG_KEY))
def safe_exec(
@ -115,15 +116,16 @@ def safe_exec(
with safe_exec_flags(), patched_qb():
# execute script compiled by RestrictedPython
exec(
compile_restricted(script, filename=filename, policy=FrappeTransformer),
exec_globals,
_locals,
)
exec(_compile_code(script, filename=filename), exec_globals, _locals)
return exec_globals, _locals
@site_cache(maxsize=32)
def _compile_code(script: str, filename: str, mode: str = "exec"):
return compile_restricted(script, filename=filename, policy=FrappeTransformer, mode=mode)
def safe_eval(code, eval_globals=None, eval_locals=None):
import unicodedata
@ -137,11 +139,7 @@ def safe_eval(code, eval_globals=None, eval_locals=None):
eval_globals["__builtins__"] = {}
eval_globals.update(WHITELISTED_SAFE_EVAL_GLOBALS)
return eval(
compile_restricted(code, filename="<safe_eval>", policy=FrappeTransformer, mode="eval"),
eval_globals,
eval_locals,
)
return eval(_compile_code(code, filename="<safe_eval>", mode="eval"), eval_globals, eval_locals)
def _validate_safe_eval_syntax(code):
@ -179,7 +177,7 @@ def get_safe_globals():
time_format = "HH:mm:ss"
number_format = NumberFormat.from_string("#,###.##")
add_data_utils(datautils)
datautils.update(SAFE_DATA_UTILS)
form_dict = getattr(frappe.local, "form_dict", frappe._dict())
@ -297,9 +295,7 @@ def get_safe_globals():
get_visible_columns=get_visible_columns,
)
add_module_properties(
frappe.exceptions, out.frappe, lambda obj: inspect.isclass(obj) and issubclass(obj, Exception)
)
out.frappe.update(SAFE_EXCEPTIONS)
if frappe.response:
out.frappe.response = frappe.response
@ -567,7 +563,7 @@ def _validate_attribute_read(object, name):
raise SyntaxError(f"Reading {object} attributes is not allowed")
if name.startswith("_"):
raise AttributeError(f'"{name}" is an invalid attribute name because it ' 'starts with "_"')
raise AttributeError(f'"{name}" is an invalid attribute name because it starts with "_"')
def _write(obj):
@ -587,13 +583,8 @@ def _write(obj):
return obj
def add_data_utils(data):
for key, obj in frappe.utils.data.__dict__.items():
if key in VALID_UTILS:
data[key] = obj
def add_module_properties(module, data, filter_method):
def get_module_properties(module, filter_method):
data = {}
for key, obj in module.__dict__.items():
if key.startswith("_"):
# ignore
@ -602,6 +593,7 @@ def add_module_properties(module, data, filter_method):
if filter_method(obj):
# only allow functions
data[key] = obj
return data
VALID_UTILS = (
@ -724,6 +716,9 @@ VALID_UTILS = (
)
SAFE_DATA_UTILS = {key: frappe.utils.data.__dict__[key] for key in VALID_UTILS}
WHITELISTED_SAFE_EVAL_GLOBALS = {
"int": int,
"float": float,
@ -740,3 +735,7 @@ SAFE_ORJSON = NamespaceDict(loads=orjson.loads, dumps=orjson.dumps)
for key, val in vars(orjson).items():
if key.startswith("OPT_"):
SAFE_ORJSON[key] = val
SAFE_EXCEPTIONS = get_module_properties(
frappe.exceptions, lambda obj: inspect.isclass(obj) and issubclass(obj, Exception)
)