This is an automated email from the ASF dual-hosted git repository.

hsluoyz pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/casbin-pycasbin.git


The following commit(s) were added to refs/heads/master by this push:
     new d4db65a  fix: cache parsed matchers to speed up enforce
d4db65a is described below

commit d4db65a8f42ffef0fde1a58b1081cf0745d1e6d1
Author: Yang Luo <[email protected]>
AuthorDate: Wed Oct 21 00:26:03 2026 +0800

    fix: cache parsed matchers to speed up enforce
---
 casbin/core_enforcer.py     | 25 +++++++++++++++++--------
 casbin/util/expression.py   | 12 +++++++++++-
 tests/test_enforcer.py      | 15 +++++++++++++++
 tests/test_fast_enforcer.py | 19 ++++++++++++-------
 4 files changed, 55 insertions(+), 16 deletions(-)

diff --git a/casbin/core_enforcer.py b/casbin/core_enforcer.py
index a80744b..3473309 100644
--- a/casbin/core_enforcer.py
+++ b/casbin/core_enforcer.py
@@ -15,6 +15,7 @@
 import copy
 import logging
 import re
+from functools import lru_cache
 
 from casbin.effect import Effector, get_effector, effect_to_bool
 from casbin.model import Model, FunctionMap
@@ -436,8 +437,9 @@ class CoreEnforcer:
 
         exp_string = self.model["m"][mtype].value
         exp_has_eval = util.has_eval(exp_string)
-        if not exp_has_eval:
-            expression = self._get_expression(exp_string, functions)
+        expression = self._get_expression("" if exp_has_eval else exp_string, 
functions)
+        if exp_has_eval:
+            rule_names = util.get_eval_value(exp_string)
 
         policy_effects = set()
 
@@ -455,10 +457,8 @@ class CoreEnforcer:
                 parameters = dict(r_parameters, **p_parameters)
 
                 if exp_has_eval:
-                    rule_names = util.get_eval_value(exp_string)
-                    rules = [util.escape_assertion(p_parameters[rule_name]) 
for rule_name in rule_names]
-                    exp_with_rule = util.replace_eval(exp_string, rules)
-                    expression = self._get_expression(exp_with_rule, functions)
+                    rules = tuple(p_parameters[rule_name] for rule_name in 
rule_names)
+                    expression.set_expr(self._get_eval_expression(exp_string, 
rules))
 
                 result = expression.eval(parameters)
 
@@ -546,8 +546,17 @@ class CoreEnforcer:
 
     @staticmethod
     def _get_expression(expr, functions=None):
+        return SimpleEval(CoreEnforcer._normalize_expression(expr), functions)
+
+    @staticmethod
+    @lru_cache(maxsize=8192)
+    def _normalize_expression(expr):
         expr = expr.replace("&&", "and")
         expr = expr.replace("||", "or")
-        expr = re.sub(r"!(?!=)", "not ", expr)
+        return re.sub(r"!(?!=)", "not ", expr)
 
-        return SimpleEval(expr, functions)
+    @staticmethod
+    @lru_cache(maxsize=8192)
+    def _get_eval_expression(expr, rules):
+        rules = [util.escape_assertion(rule) for rule in rules]
+        return CoreEnforcer._normalize_expression(util.replace_eval(expr, 
rules))
diff --git a/casbin/util/expression.py b/casbin/util/expression.py
index e2abb5d..3df8d26 100644
--- a/casbin/util/expression.py
+++ b/casbin/util/expression.py
@@ -12,10 +12,16 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+from functools import lru_cache
 from simpleeval import EvalWithCompoundTypes
 import ast
 
 
+@lru_cache(maxsize=8192)
+def parse_expression(expr):
+    return ast.parse(expr.strip()).body[0].value
+
+
 class SimpleEval(EvalWithCompoundTypes):
     """Rewrite SimpleEval.
     >>> s = SimpleEval("20 + 30 - ( 10 * 5)")
@@ -29,9 +35,13 @@ class SimpleEval(EvalWithCompoundTypes):
         """Create the evaluator instance.  Set up valid operators (+,-, etc)
         functions (add, random, get_val, whatever) and names."""
         super(SimpleEval, self).__init__(functions=functions)
+        self.set_expr(expr)
+
+    def set_expr(self, expr):
+        """replace the expression to evaluate, keeping the functions."""
         if expr != "":
             self.expr = expr
-            self.ast_parsed_value = ast.parse(expr.strip()).body[0].value
+            self.ast_parsed_value = parse_expression(expr)
 
     def eval(self, names=None):
         """evaluate an expression, using the operators, functions and
diff --git a/tests/test_enforcer.py b/tests/test_enforcer.py
index 64f9e2f..c295283 100644
--- a/tests/test_enforcer.py
+++ b/tests/test_enforcer.py
@@ -420,6 +420,21 @@ class TestConfig(TestCaseBase):
         self.assertFalse(e.enforce(sub3, "/data1", "write"))
         self.assertFalse(e.enforce(sub3, "/data2", "write"))
 
+    def test_abac_with_updated_sub_rule(self):
+        e = self.get_enforcer(get_examples("abac_rule_model.conf"), 
get_examples("abac_rule_policy.csv"))
+        e.enable_auto_save(False)
+
+        sub = MockSub("alice", 16)
+        self.assertFalse(e.enforce(sub, "/data1", "read"))
+
+        e.remove_policy("r.sub.age > 18", "/data1", "read")
+        e.add_policy("r.sub.age > 10", "/data1", "read")
+        self.assertTrue(e.enforce(sub, "/data1", "read"))
+
+        e.remove_policy("r.sub.age > 10", "/data1", "read")
+        e.add_policy("r.sub.age > 18", "/data1", "read")
+        self.assertFalse(e.enforce(sub, "/data1", "read"))
+
     def test_abac_with_multiple_sub_rules(self):
         e = self.get_enforcer(
             get_examples("abac_multiple_rules_model.conf"),
diff --git a/tests/test_fast_enforcer.py b/tests/test_fast_enforcer.py
index 77a2ff5..c3d8ee1 100644
--- a/tests/test_fast_enforcer.py
+++ b/tests/test_fast_enforcer.py
@@ -12,6 +12,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+import gc
 import os
 import time
 from typing import Sequence
@@ -49,13 +50,17 @@ class TestFastEnforcer(TestCaseBase):
         N = 5
         t_e1_list = []
         t_e2_list = []
-        for _ in range(N):
-            s_e1 = time.perf_counter()
-            e1.enforce("alice", "data1", "read")
-            t_e1_list.append(time.perf_counter() - s_e1)
-            s_e2 = time.perf_counter()
-            e2.enforce("alice", "data1", "read")
-            t_e2_list.append(time.perf_counter() - s_e2)
+        gc.disable()
+        try:
+            for _ in range(N):
+                s_e1 = time.perf_counter()
+                e1.enforce("alice", "data1", "read")
+                t_e1_list.append(time.perf_counter() - s_e1)
+                s_e2 = time.perf_counter()
+                e2.enforce("alice", "data1", "read")
+                t_e2_list.append(time.perf_counter() - s_e2)
+        finally:
+            gc.enable()
         avg_t_e1 = sum(t_e1_list) / N
         avg_t_e2 = sum(t_e2_list) / N
         assert avg_t_e1 > avg_t_e2 * 5


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to