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 e72f6b0  fix: match Go in regexMatch, inline comments, domain matching 
and explain
e72f6b0 is described below

commit e72f6b01fd91546e75cca1e688adfcefbadcea34
Author: Yang Luo <[email protected]>
AuthorDate: Wed Oct 21 01:49:20 2026 +0800

    fix: match Go in regexMatch, inline comments, domain matching and explain
---
 .github/workflows/build.yml      |  9 +++++++
 casbin/config/config.py          |  8 +++++-
 casbin/core_enforcer.py          | 40 ++++++++++++++++++++++++-----
 casbin/util/builtin_operators.py |  2 +-
 casbin/util/expression.py        | 12 ++++++++-
 tests/config/test_config.py      |  2 +-
 tests/test_conformance.py        | 54 ++++++++++++++++++++++++++++++++++++++++
 7 files changed, 117 insertions(+), 10 deletions(-)

diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 921e3af..3ab460b 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -18,6 +18,13 @@ jobs:
       - name: Checkout
         uses: actions/checkout@v4
 
+      - name: Checkout conformance cases
+        uses: actions/checkout@v4
+        with:
+          repository: apache/casbin
+          path: casbin-conformance
+          sparse-checkout: conformance
+
       - name: Set up Python ${{ matrix.python-version }}
         uses: actions/setup-python@v5
         with:
@@ -31,6 +38,8 @@ jobs:
 
       - name: Run tests
         run: coverage run -m unittest discover -s tests -t tests
+        env:
+          CASBIN_CONFORMANCE_DIR: ${{ github.workspace 
}}/casbin-conformance/conformance
 
       - name: Run benchmark
         run: python3 -m pytest
diff --git a/casbin/config/config.py b/casbin/config/config.py
index 74160ed..17c2873 100644
--- a/casbin/config/config.py
+++ b/casbin/config/config.py
@@ -93,7 +93,13 @@ class Config:
                 else:
                     p = line
                     can_write = True
-                buf.append(p)
+
+                end = len(p)
+                for i, value in enumerate(p):
+                    if value == self.DEFAULT_COMMENT or value == 
self.DEFAULT_COMMENT_SEM:
+                        end = i
+                        break
+                buf.append(p[:end])
 
     def _write(self, section, line_num, b):
         buf = "".join(b)
diff --git a/casbin/core_enforcer.py b/casbin/core_enforcer.py
index 3473309..0d4ddc8 100644
--- a/casbin/core_enforcer.py
+++ b/casbin/core_enforcer.py
@@ -17,12 +17,12 @@ import logging
 import re
 from functools import lru_cache
 
-from casbin.effect import Effector, get_effector, effect_to_bool
+from casbin.effect import Effector, AllowAndDenyEffector, get_effector, 
effect_to_bool
 from casbin.model import Model, FunctionMap
 from casbin.persist import Adapter
 from casbin.persist.adapters import FileAdapter
 from casbin.rbac import default_role_manager
-from casbin.util import generate_g_function, SimpleEval, util, 
generate_conditional_g_function
+from casbin.util import generate_g_function, SimpleEval, util, 
generate_conditional_g_function, key_match
 from casbin.util.log import configure_logging, disabled_logging
 
 
@@ -220,6 +220,28 @@ class CoreEnforcer:
                     else:
                         assertion.cond_rm = 
default_role_manager.ConditionalDomainManager(10)
                         self.cond_rm_map[ptype] = assertion.cond_rm
+                    self._register_domain_matching_func(ptype)
+
+    def _get_domain_tokens(self):
+        r_domain_token = ""
+        p_domain_token = ""
+        r_assertion = (self.model["r"] or {}).get("r")
+        if r_assertion is not None and len(r_assertion.tokens) > 1:
+            r_domain_token = r_assertion.tokens[1]
+        p_assertion = (self.model["p"] or {}).get("p")
+        if p_assertion is not None and len(p_assertion.tokens) > 1:
+            p_domain_token = p_assertion.tokens[1]
+        return r_domain_token, p_domain_token
+
+    def _register_domain_matching_func(self, ptype):
+        r_domain_token, p_domain_token = self._get_domain_tokens()
+        m_assertion = (self.model["m"] or {}).get("m")
+        if r_domain_token == "" or p_domain_token == "" or m_assertion is None:
+            return
+
+        match_fun = "keyMatch({}, {})".format(r_domain_token, p_domain_token)
+        if match_fun in m_assertion.value:
+            self.add_named_domain_matching_func(ptype, key_match)
 
     def load_policy(self):
         """reloads the policy from file/database."""
@@ -339,6 +361,9 @@ class CoreEnforcer:
         if ptype in self.rm_map.keys():
             self.rm_map[ptype].add_domain_matching_func(fn)
             return True
+        if ptype in self.cond_rm_map.keys():
+            self.cond_rm_map[ptype].add_domain_matching_func(fn)
+            return True
 
         return False
 
@@ -411,7 +436,7 @@ class CoreEnforcer:
             for key, ast in self.model["g"].items():
                 if len(self.rm_map) != 0:
                     functions[key] = generate_g_function(ast.rm)
-                if len(self.cond_rm_map) != 0:
+                if ast.cond_rm is not None:
                     functions[key] = 
generate_conditional_g_function(ast.cond_rm)
 
         if len(rvals) != 0:
@@ -448,6 +473,7 @@ class CoreEnforcer:
         policy_len = len(self.model["p"][ptype].policy)
 
         explain_index = -1
+        allow_index = -1
         if not 0 == policy_len:
             for i, pvals in enumerate(self.model["p"][ptype].policy):
                 if len(p_tokens) != len(pvals):
@@ -485,11 +511,11 @@ class CoreEnforcer:
                 else:
                     policy_effects.add(Effector.ALLOW)
 
-                # Update explain_index for any matching policy before checking 
early break condition
-                # to ensure explanations are captured for allow rules in deny 
models
-                explain_index = i
+                if allow_index == -1 and Effector.ALLOW in policy_effects:
+                    allow_index = i
 
                 if self.eft.intermediate_effect(policy_effects) != 
Effector.INDETERMINATE:
+                    explain_index = i
                     break
 
         else:
@@ -510,6 +536,8 @@ class CoreEnforcer:
 
         final_effect = self.eft.final_effect(policy_effects)
         result = effect_to_bool(final_effect)
+        if explain_index == -1 and final_effect == Effector.ALLOW and 
isinstance(self.eft, AllowAndDenyEffector):
+            explain_index = allow_index
 
         # Log request.
         if (result and self.logger.isEnabledFor(logging.INFO)) or (
diff --git a/casbin/util/builtin_operators.py b/casbin/util/builtin_operators.py
index 99075a9..3efe5f2 100644
--- a/casbin/util/builtin_operators.py
+++ b/casbin/util/builtin_operators.py
@@ -293,7 +293,7 @@ def key_match5_func(*args) -> bool:
 def regex_match(key1, key2):
     """determines whether key1 matches the pattern of key2 in regular 
expression."""
 
-    res = re.match(key2, key1)
+    res = re.search(key2, key1)
     if res:
         return True
     else:
diff --git a/casbin/util/expression.py b/casbin/util/expression.py
index 3df8d26..37b7448 100644
--- a/casbin/util/expression.py
+++ b/casbin/util/expression.py
@@ -13,7 +13,7 @@
 # limitations under the License.
 
 from functools import lru_cache
-from simpleeval import EvalWithCompoundTypes
+from simpleeval import EvalWithCompoundTypes, NameNotDefined
 import ast
 
 
@@ -51,3 +51,13 @@ class SimpleEval(EvalWithCompoundTypes):
             self.names = names
 
         return self._eval(self.ast_parsed_value)
+
+    def _eval_name(self, node):
+        try:
+            return super(SimpleEval, self)._eval_name(node)
+        except NameNotDefined:
+            if node.id == "true":
+                return True
+            if node.id == "false":
+                return False
+            raise
diff --git a/tests/config/test_config.py b/tests/config/test_config.py
index 3d9d80e..b29f58f 100644
--- a/tests/config/test_config.py
+++ b/tests/config/test_config.py
@@ -38,7 +38,7 @@ class TestConfig(TestCase):
         self.assertEqual(config.get("math::math.f64"), "64.1")
 
         # other::key test
-        self.assertEqual(config.get("other::name"), "ATC自动化测试^-^&($#……#")
+        self.assertEqual(config.get("other::name"), "ATC自动化测试^-^&($")
         self.assertEqual(config.get("other::key1"), "test key")
 
         config.set("other::key1", "new test key")
diff --git a/tests/test_conformance.py b/tests/test_conformance.py
new file mode 100644
index 0000000..c462272
--- /dev/null
+++ b/tests/test_conformance.py
@@ -0,0 +1,54 @@
+# Copyright 2026 The casbin Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import json
+import os
+from unittest import TestCase, skipUnless
+
+import casbin
+
+CONFORMANCE_DIR = os.environ.get("CASBIN_CONFORMANCE_DIR")
+
+
+@skipUnless(CONFORMANCE_DIR, "CASBIN_CONFORMANCE_DIR is not set")
+class TestConformance(TestCase):
+    def test_conformance(self):
+        cases_dir = os.path.join(CONFORMANCE_DIR, "cases")
+        data_dir = os.path.join(CONFORMANCE_DIR, "data")
+        case_files = sorted(name for name in os.listdir(cases_dir) if 
name.endswith(".json"))
+        self.assertTrue(case_files, "no conformance cases found in 
{}".format(cases_dir))
+
+        mismatches = []
+        for case_file in case_files:
+            with open(os.path.join(cases_dir, case_file), encoding="utf-8") as 
f:
+                case = json.load(f)
+            try:
+                e = casbin.Enforcer(os.path.join(data_dir, case["model"]), 
os.path.join(data_dir, case["policy"]))
+            except Exception as err:
+                mismatches.append("{}: failed to load: {!r}".format(case_file, 
err))
+                continue
+
+            for test in case["tests"]:
+                request = test["request"]
+                want = (test["allow"], test["explain"])
+                try:
+                    allow, explain = e.enforce_ex(*request)
+                    got = (allow, list(explain or []))
+                except Exception as err:
+                    got = repr(err)
+                if got != want:
+                    mismatches.append("{}: request {}: got {}, want 
{}".format(case_file, request, got, want))
+
+        if mismatches:
+            self.fail("{} mismatches:\n{}".format(len(mismatches), 
"\n".join(mismatches)))


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

Reply via email to