From dfbc69c36f8cacd12a1e563733273b319a35fc9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Edgar=20Ram=C3=ADrez=20Mondrag=C3=B3n?= Date: Sun, 15 Mar 2026 18:56:51 -0600 Subject: [PATCH] Address performance issues with `_check_disallowed_items` overhead MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Downstream in https://github.com/meltano/sdk/pull/3565#issuecomment-4057644699, we noticed a performance regression in the 1.0.5 release of `simpleeval`. The root problem seems to be that 1. there too many redundant instance checks for safe primitive types 2. `is_hashable` has try/except overhead. The fix is for 1 is to implement a fast path for simple types. The fix for 2 is to replace `is_hashable` with [`callable`](https://docs.python.org/3/library/functions.html#callable), which achieves the same purpose in this context. Signed-off-by: Edgar Ramírez Mondragón --- simpleeval.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/simpleeval.py b/simpleeval.py index 79561e3..18f7da9 100644 --- a/simpleeval.py +++ b/simpleeval.py @@ -135,12 +135,9 @@ ######################################## # Tiny helpers: - -def is_hashable(value): - try: - return hash(value) - except TypeError: - return False +# Primitive types that are always safe: can't be modules, can't be in DISALLOW_FUNCTIONS, +# and don't need recursive container checks. Used as a fast path in _check_disallowed_items. +_PRIMITIVE_TYPES = frozenset({int, float, str, bool, type(None), bytes, complex}) # Disallow functions: @@ -628,14 +625,16 @@ def _check_disallowed_items(self, item): Raises FeatureNotAvailable if forbidden content found. ModuleWrapper instances are allowed (explicit opt-in to module access). """ + # Fast path: primitive scalars are always safe (most common case) + if type(item) in _PRIMITIVE_TYPES: + return + # Allow ModuleWrapper (explicit opt-in to module access) if isinstance(item, ModuleWrapper): return if isinstance(item, types.ModuleType): raise FeatureNotAvailable("Sorry, modules are not allowed") - if is_hashable(item) and item in DISALLOW_FUNCTIONS: - raise FeatureNotAvailable("This function is forbidden") if isinstance(item, (list, tuple)): for element in item: @@ -643,6 +642,8 @@ def _check_disallowed_items(self, item): elif isinstance(item, dict): for value in item.values(): self._check_disallowed_items(value) + elif callable(item) and item in DISALLOW_FUNCTIONS: + raise FeatureNotAvailable("This function is forbidden") @staticmethod def parse(expr): @@ -875,7 +876,7 @@ def _eval_attribute(self, node): if item is not _ATTR_NOT_FOUND: if isinstance(item, types.ModuleType): raise FeatureNotAvailable("Sorry, modules are not allowed in attribute access") - if is_hashable(item) and item in DISALLOW_FUNCTIONS: + if callable(item) and item in DISALLOW_FUNCTIONS: raise FeatureNotAvailable("This function is forbidden") return item