From bddc89544dd26b2bae4bceed274a852179a5fc50 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Sat, 19 Jul 2025 18:04:29 +0530 Subject: [PATCH 1/4] perf: Cache safe_exec compilation --- frappe/utils/safe_exec.py | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/frappe/utils/safe_exec.py b/frappe/utils/safe_exec.py index 3b89408bcb..0bcc27d432 100644 --- a/frappe/utils/safe_exec.py +++ b/frappe/utils/safe_exec.py @@ -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 @@ -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, ttl=60 * 60) +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="", policy=FrappeTransformer, mode="eval"), - eval_globals, - eval_locals, - ) + return eval(_compile_code(code, filename="", mode="eval"), eval_globals, eval_locals) def _validate_safe_eval_syntax(code): @@ -567,7 +565,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): From 17a124458567898304d960c66b7406c1d9cc997a Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Sat, 19 Jul 2025 18:14:04 +0530 Subject: [PATCH 2/4] perf: Always use cached config for checking safe_exec It expires in 1min anyway --- frappe/utils/safe_exec.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frappe/utils/safe_exec.py b/frappe/utils/safe_exec.py index 0bcc27d432..a7ca893977 100644 --- a/frappe/utils/safe_exec.py +++ b/frappe/utils/safe_exec.py @@ -82,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( @@ -121,7 +121,7 @@ def safe_exec( return exec_globals, _locals -@site_cache(maxsize=32, ttl=60 * 60) +@site_cache(maxsize=32) def _compile_code(script: str, filename: str, mode: str = "exec"): return compile_restricted(script, filename=filename, policy=FrappeTransformer, mode=mode) From 38365beb52dfb56ba49745015d1bea4a21f2a754 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Sat, 19 Jul 2025 18:18:51 +0530 Subject: [PATCH 3/4] perf: Compute safe utils only once 300us -> 60us for this silly change LOL! --- frappe/utils/safe_exec.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/frappe/utils/safe_exec.py b/frappe/utils/safe_exec.py index a7ca893977..f036420976 100644 --- a/frappe/utils/safe_exec.py +++ b/frappe/utils/safe_exec.py @@ -177,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()) @@ -585,12 +585,6 @@ 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): for key, obj in module.__dict__.items(): if key.startswith("_"): @@ -722,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, From b94b6ec9394681a957e744c9502a317108d309c1 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Sat, 19 Jul 2025 18:42:43 +0530 Subject: [PATCH 4/4] perf!: Compute safe exceptions only once --- frappe/utils/safe_exec.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/frappe/utils/safe_exec.py b/frappe/utils/safe_exec.py index f036420976..ff43c90a24 100644 --- a/frappe/utils/safe_exec.py +++ b/frappe/utils/safe_exec.py @@ -295,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 @@ -585,7 +583,8 @@ def _write(obj): return 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 @@ -594,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 = ( @@ -735,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) +)