Repository: casbin/pycasbin

Issue:
asyncio Adapter support
With the now built-in support for an async enforcer, it would be great if we could migrate / make available asyncio stubs for the `BatchAdapter`, `UpdateAdapter`, `FilteredAdapter`, and `Adapter` interfaces as well.

That would complete the full feature set of loading from db (purpose of `asynccasbin`) + enforcing at runtime, rather than just the latter.

`asynccasbin` also isn't maintained and is incompatible with Python 3.12, which is why I think it makes significantly more sense for the features to live here instead.

You could consider this a bug as well, since the async enforcers assume the underlying adapter methods are awaitable/coros, but the superclasses are not typed as such (and as an aside, those interfaces should really be ABCs with the abstract methods marked as such)

Discussion:
casbin-bot:  @Nekotoxin please review
CLAassistant: [![CLA assistant check](https://cla-assistant.io/pull/badge/not_signed)](https://cla-assistant.io/casbin/pycasbin?pullRequest=335) <br/>Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our [Contributor License Agreement](https://cla-assistant.io/casbin/pycasbin?pullRequest=335) before we can accept your contribution.<br/><sub>You have signed the CLA already but the status is still pending? Let us [recheck](https://cla-assistant.io/check/casbin/pycasbin?pullRequest=335) it.</sub>
hsluoyz: @thearchitector fix:

![image](https://github.com/casbin/pycasbin/assets/3787410/a6df04df-f06c-47c4-9d96-28d6aa28ab56)

hsluoyz: @thearchitector breaking changes are not allowed. Python is a dynamic language. I believe there must be ways to do this without any breaking changes (like via dynamic casting). So closed for now
thearchitector: @hsluoyz the function definitions themselves are different between the example implementation (async file adapter) and the rest. that is _not_ what Python supports when you say "dynamic."; so at the very least with no other changes, that impl change will be required --- technically the only feasible way given your constraints is to have the async implementations be synchronous functions that return coroutines vs already being coroutines.

in the greater scope, though, I think the position is somewhat foolish; no breaking changes means there is an implicit trust that the original and initial implementation was the best way to do it -- having looked through the code, I can guarantee that is not true.... So what you're actually saying is "foundational improvements are not allowed," and that is the textbook definition of tech debt.
thearchitector: it is worth mentioning, I suppose, that doing the "sync returns coro" approach _will_ work, but will also mean any existing adapters that use `asynccasbin` will have to introduce breaking changes to migrate over to pycasbin

of course, the problem can also just be ignored completely since it's not a runtime bug, in which case no PR is necessary. that's always an option
leeqvip: @thearchitector Hi, 
I support the following points:
- Added `casbin.persist.adapters.asyncio` and `AsyncEnforcer`, etc., and verified them in `AsyncInternalEnforcer`
- Rename `adapters.FilteredAdapter` to `adapters.FilteredFileAdapter`, but to ensure compatibility, `adapters.FilteredAdapter` should be retained to inherit `adapters.FilteredFileAdapter`

What I don't support are:
- Change `persist.FilteredAdapter` interface to `adapters.FilteredAdapter`. The` interface` should be placed in the `persist` directory

I look forward to your revision and I will reopen this PR.

hsluoyz: @thearchitector fix:

![image](https://github.com/casbin/pycasbin/assets/3787410/a6df04df-f06c-47c4-9d96-28d6aa28ab56)

thearchitector: > @thearchitector Hi, I support the following points:
> 
>     * Added `casbin.persist.adapters.asyncio` and `AsyncEnforcer`, etc., and verified them in `AsyncInternalEnforcer`
> 
>     * Rename `adapters.FilteredAdapter` to `adapters.FilteredFileAdapter`, but to ensure compatibility, `adapters.FilteredAdapter` should be retained to inherit `adapters.FilteredFileAdapter`
> 
> 
> What I don't support are:
> 
>     * Change `persist.FilteredAdapter` interface to `adapters.FilteredAdapter`. The` interface` should be placed in the `persist` directory
> 
> 
> I look forward to your revision and I will reopen this PR.

@leeqvip @hsluoyz thanks for the feedback. to be clear, you support everything _except_ moving the interfaces, correct? So all the existing interfaces (`adapter`, `adapter_filtered`, `batch_adapter`, etc) should remain in `persist`? And the new asyncio ones will go in `persist.adapters.asyncio`?
hsluoyz: @thearchitector yes
thearchitector: @hsluoyz @leeqvip i've moved the interfaces back to `persist` and included import aliases for backwards compatibility. i did notice, though, that `update_adapter` lives within `persist.adapters` instead of the top level `persist` -- should it be moved to be with the other interfaces? i think that would make sense. i can do the same import alias pattern as well to maintain backwards compat if you agree.

also, running `black` i kept running into [this bug](https://github.com/psf/black/issues/2964) and had to fiddle with my venv to get it to work. Since Black is stable now, I think it would make sense to bump the version of `super-linter` and update the black dep in `requirements_dev.txt` to match? I scanned through the super linter changelog and dont see any reason why we couldn't bump it to `5.7.2`, (and update the black dep to `23.11.0`)
thearchitector: > Thanks. In the same way, please move `update_adapter` to the `persist` directory. Regarding the `linter` warning you mentioned, I didn’t see it? https://github.com/casbin/pycasbin/actions/runs/7312454795/job/19923199403#step:4:1

@leeqvip it doesn't happen in the github action because super linter pins the incompatible dependency, but shows up locally because the dependency isn't pinned in `requirements_dev.txt`:

![image](https://github.com/casbin/pycasbin/assets/7488439/0d580104-27be-4c40-8538-be6726eb7564)

we can solve it by pinning the hidden dep in the local req.txt file to an older version, but i think it would be best to just bump the super linter version to `5.7.2` and `black` to `23.11.0`
leeqvip: > > Thanks. In the same way, please move `update_adapter` to the `persist` directory. Regarding the `linter` warning you mentioned, I didn’t see it? https://github.com/casbin/pycasbin/actions/runs/7312454795/job/19923199403#step:4:1
> 
> 
> 
> @leeqvip it doesn't happen in the github action because [super linter pins the broken dependency to an old version](https://github.com/super-linter/super-linter/blob/v4.9.2/dependencies/python/black.txt#L2), but shows up locally because the dependency isn't pinned in `requirements_dev.txt`:
> 
> 
> 
> ![image](https://github.com/casbin/pycasbin/assets/7488439/0d580104-27be-4c40-8538-be6726eb7564)
> 
> 
> 
> we can solve it by pinning the hidden dep in the local req.txt file to an older version, but i think it would be best to just bump the super linter version to `5.7.2` and `black` to `23.11.0`

So, bump to the new version plz.
hsluoyz: @thearchitector fix CI errors first:

![image](https://github.com/casbin/pycasbin/assets/3787410/57ca75bd-51e4-43d1-8126-467ff222154c)

thearchitector: @hsluoyz huh, gh actions cannot find the super linter version. weird, it definitely exists...

do I need to address the coveralls stuff too? Not sure what the error is there (the test percentage regression?)

Patch:
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 9b10174..db2a93f 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -11,55 +11,53 @@ jobs:
     strategy:
       fail-fast: false
       matrix:
-        python-version: ['3.9', '3.10', '3.11']
+        python-version: ["3.9", "3.10", "3.11"]
         os: [ubuntu-latest, macOS-latest, windows-latest]
 
     steps:
-    - name: Checkout
-      uses: actions/checkout@v2
+      - name: Checkout
+        uses: actions/checkout@v4
 
-    - name: Set up Python ${{ matrix.python-version }}
-      uses: actions/setup-python@v2
-      with:
-        python-version: ${{ matrix.python-version }}
+      - name: Set up Python ${{ matrix.python-version }}
+        uses: actions/setup-python@v5
+        with:
+          python-version: ${{ matrix.python-version }}
 
-    - name: Install dependencies
-      run: |
-        pip install -r requirements.txt
-        pip install -r requirements_dev.txt
-        pip install coveralls
-        pip install pytest
-        pip install pytest-benchmark
+      - name: Install dependencies
+        run: |
+          pip install -r requirements.txt
+          pip install -r requirements_dev.txt
+          pip install coveralls
 
-    - name: Run tests
-      run: coverage run -m unittest discover -s tests -t tests
+      - name: Run tests
+        run: coverage run -m unittest discover -s tests -t tests
 
-    - name: Run benchmark
-      run: python3 -m pytest 
-            --benchmark-verbose 
-            --benchmark-columns=mean,stddev,iqr,ops,rounds 
-            tests/benchmarks/benchmark_model.py
-            tests/benchmarks/benchmark_management_api.py
-            tests/benchmarks/benchmark_role_manager.py
+      - name: Run benchmark
+        run: python3 -m pytest
+          --benchmark-verbose
+          --benchmark-columns=mean,stddev,iqr,ops,rounds
+          tests/benchmarks/benchmark_model.py
+          tests/benchmarks/benchmark_management_api.py
+          tests/benchmarks/benchmark_role_manager.py
 
-    - name: Upload coverage data to coveralls.io
-      run: coveralls --service=github
-      env:
-        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-        COVERALLS_FLAG_NAME: ${{ matrix.os }} - ${{ matrix.python-version }}
-        COVERALLS_PARALLEL: true
+      - name: Upload coverage data to coveralls.io
+        run: coveralls --service=github
+        env:
+          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+          COVERALLS_FLAG_NAME: ${{ matrix.os }} - ${{ matrix.python-version }}
+          COVERALLS_PARALLEL: true
 
   lint:
     name: Run Linters
     runs-on: ubuntu-latest
     steps:
       - name: Checkout
-        uses: actions/checkout@v2
+        uses: actions/checkout@v4
         with:
           fetch-depth: 0
 
       - name: Super-Linter
-        uses: github/super-linter@v4.9.2
+        uses: super-linter/super-linter@v5.7.2
         env:
           VALIDATE_ALL_CODEBASE: false
           VALIDATE_PYTHON_BLACK: true
@@ -74,36 +72,36 @@ jobs:
     runs-on: ubuntu-latest
     container: python:3-slim
     steps:
-    - name: Finished
-      run: |
-        pip3 install --upgrade coveralls
-        coveralls --finish
-      env:
-        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+      - name: Finished
+        run: |
+          pip3 install --upgrade coveralls
+          coveralls --finish
+        env:
+          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
 
   release:
     name: Release
     runs-on: ubuntu-latest
-    needs: [ test, coveralls ]
+    needs: [test, coveralls]
     steps:
       - name: Checkout
-        uses: actions/checkout@v2
+        uses: actions/checkout@v4
         with:
           fetch-depth: 0
-          
+
       - name: Setup Node.js
-        uses: actions/setup-node@v2
+        uses: actions/setup-node@v4
         with:
-          node-version: '18'
+          node-version: "18"
 
       - name: Setup
         run: npm install
-      
+
       - name: Set up python
-        uses: actions/setup-python@v4
+        uses: actions/setup-python@v5
         with:
           python-version: 3.11
-      
+
       - name: Release
         env:
           GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/README.md b/README.md
index baede44..fccb6c5 100644
--- a/README.md
+++ b/README.md
@@ -232,7 +232,7 @@ https://casbin.org/docs/role-managers
 
 If your code use `async` / `await` and is heavily dependent on I/O operations, you can adopt Async Enforcer!
 
-1. Create an async engine and new a Casbin AsyncEnforcer with a model file and an async Pycasbin adapter:
+1. Create an async engine and new a Casbin AsyncEnforcer with a model file and an async Pycasbin adapter (AsyncAdapter subclass):
 
 ```python
 import asyncio
@@ -266,6 +266,8 @@ async def get_enforcer():
 
 Note: you can see all supported adapters in [Adapters | Casbin](https://casbin.org/docs/adapters).
 
+Built-in async adapters are available in `casbin.persist.adapters.asyncio`.
+
 2. Add an enforcement hook into your code right before the access happens:
 
 ```python
diff --git a/casbin/async_internal_enforcer.py b/casbin/async_internal_enforcer.py
index e62a8bd..f2bfc5f 100644
--- a/casbin/async_internal_enforcer.py
+++ b/casbin/async_internal_enforcer.py
@@ -15,8 +15,7 @@ import copy
 
 from casbin.core_enforcer import CoreEnforcer
 from casbin.model import Model, FunctionMap
-from casbin.persist import Adapter
-from casbin.persist.adapters.async_file_adapter import AsyncFileAdapter
+from casbin.persist.adapters.asyncio import AsyncFileAdapter, AsyncAdapter
 
 
 class AsyncInternalEnforcer(CoreEnforcer):
@@ -32,7 +31,7 @@ class AsyncInternalEnforcer(CoreEnforcer):
     def init_with_model_and_adapter(self, m, adapter=None):
         """initializes an enforcer with a model and a database adapter."""
 
-        if not isinstance(m, Model) or adapter is not None and not isinstance(adapter, Adapter):
+        if not isinstance(m, Model) or adapter is not None and not isinstance(adapter, AsyncAdapter):
             raise RuntimeError("Invalid parameters for enforcer.")
 
         self.adapter = adapter
diff --git a/casbin/config/config.py b/casbin/config/config.py
index 56c6ba3..30299b4 100644
--- a/casbin/config/config.py
+++ b/casbin/config/config.py
@@ -96,7 +96,6 @@ class Config:
                 buf.append(p)
 
     def _write(self, section, line_num, b):
-
         buf = "".join(b)
         if len(buf) <= 0:
             return
diff --git a/casbin/distributed_enforcer.py b/casbin/distributed_enforcer.py
index a5e3b7b..63012bf 100644
--- a/casbin/distributed_enforcer.py
+++ b/casbin/distributed_enforcer.py
@@ -13,8 +13,7 @@
 # limitations under the License.
 
 from casbin.model.policy_op import PolicyOp
-from casbin.persist import batch_adapter
-from casbin.persist.adapters import update_adapter
+from casbin.persist import batch_adapter, update_adapter
 from casbin.synced_enforcer import SyncedEnforcer
 
 
diff --git a/casbin/persist/__init__.py b/casbin/persist/__init__.py
index 78c1353..b3de4c1 100644
--- a/casbin/persist/__init__.py
+++ b/casbin/persist/__init__.py
@@ -14,5 +14,5 @@
 
 from .adapter import *
 from .adapter_filtered import *
-from .batch_adapter import *
 from .adapters import *
+from .batch_adapter import *
diff --git a/casbin/persist/adapters/__init__.py b/casbin/persist/adapters/__init__.py
index 5bee4fc..46f177e 100644
--- a/casbin/persist/adapters/__init__.py
+++ b/casbin/persist/adapters/__init__.py
@@ -13,5 +13,10 @@
 # limitations under the License.
 
 from .file_adapter import FileAdapter
-from .adapter_filtered import FilteredAdapter
-from .update_adapter import UpdateAdapter
+from .filtered_file_adapter import FilteredFileAdapter
+from ..update_adapter import UpdateAdapter
+
+# alias import for backwards compatibility
+FilteredAdapter = FilteredFileAdapter
+
+__all__ = ["FileAdapter", "FilteredFileAdapter", "FilteredAdapter", "UpdateAdapter"]
diff --git a/casbin/persist/adapters/adapter_filtered.py b/casbin/persist/adapters/adapter_filtered.py
index 0fa8582..58a4982 100644
--- a/casbin/persist/adapters/adapter_filtered.py
+++ b/casbin/persist/adapters/adapter_filtered.py
@@ -1,107 +1,7 @@
-# Copyright 2021 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.
+# NOTE: this file exists as a backwards compatible alias. please directly
+# use FilteredFileAdapter from `casbin.persist.adapters.filtered_file_adapter` instead.
 
-from casbin import persist
-from .file_adapter import FileAdapter
-import os
+from .filtered_file_adapter import Filter
+from .filtered_file_adapter import FilteredFileAdapter as FilteredAdapter
 
-
-class Filter:
-    # P,G are string []
-    P = []
-    G = []
-
-
-class FilteredAdapter(FileAdapter, persist.FilteredAdapter):
-    filtered = False
-    _file_path = ""
-    filter = Filter()
-    # new_filtered_adapte is the constructor for FilteredAdapter.
-    def __init__(self, file_path):
-        self.filtered = True
-        self._file_path = file_path
-
-    def load_policy(self, model):
-        if not os.path.isfile(self._file_path):
-            raise RuntimeError("invalid file path, file path cannot be empty")
-        self.filtered = False
-        self._load_policy_file(model)
-
-    # load_filtered_policy loads only policy rules that match the filter.
-    def load_filtered_policy(self, model, filter):
-        if filter == None:
-            return self.load_policy(model)
-
-        if not os.path.isfile(self._file_path):
-            raise RuntimeError("invalid file path, file path cannot be empty")
-
-        try:
-            filter_value = [filter.__dict__["P"]] + [filter.__dict__["G"]]
-        except:
-            raise RuntimeError("invalid filter type")
-
-        self.load_filtered_policy_file(model, filter_value, persist.load_policy_line)
-        self.filtered = True
-
-    def load_filtered_policy_file(self, model, filter, hanlder):
-        with open(self._file_path, "rb") as file:
-            while True:
-                line = file.readline()
-                line = line.decode().strip()
-                if line == "\n":
-                    continue
-                if not line:
-                    break
-                if filter_line(line, filter):
-                    continue
-
-                hanlder(line, model)
-
-    # is_filtered returns true if the loaded policy has been filtered.
-    def is_filtered(self):
-        return self.filtered
-
-    def save_policy(self, model):
-        if self.filtered:
-            raise RuntimeError("cannot save a filtered policy")
-
-        self._save_policy_file(model)
-
-
-def filter_line(line, filter):
-    if filter == None:
-        return False
-
-    p = line.split(",")
-    if len(p) == 0:
-        return True
-    filter_slice = []
-
-    if p[0].strip() == "p":
-        filter_slice = filter[0]
-    elif p[0].strip() == "g":
-        filter_slice = filter[1]
-    return filter_words(p, filter_slice)
-
-
-def filter_words(line, filter):
-    if len(line) < len(filter) + 1:
-        return True
-    skip_line = False
-    for i, v in enumerate(filter):
-        if len(v) > 0 and (v.strip() != line[i + 1].strip()):
-            skip_line = True
-            break
-
-    return skip_line
+__all__ = ["Filter", "FilteredAdapter"]
diff --git a/casbin/persist/adapters/asyncio/__init__.py b/casbin/persist/adapters/asyncio/__init__.py
new file mode 100644
index 0000000..1c3c181
--- /dev/null
+++ b/casbin/persist/adapters/asyncio/__init__.py
@@ -0,0 +1,13 @@
+from .adapter import AsyncAdapter
+from .adapter_filtered import AsyncFilteredAdapter
+from .batch_adapter import AsyncBatchAdapter
+from .file_adapter import AsyncFileAdapter
+from .update_adapter import AsyncUpdateAdapter
+
+__all__ = [
+    "AsyncAdapter",
+    "AsyncFilteredAdapter",
+    "AsyncBatchAdapter",
+    "AsyncFileAdapter",
+    "AsyncUpdateAdapter",
+]
diff --git a/casbin/persist/adapters/asyncio/adapter.py b/casbin/persist/adapters/asyncio/adapter.py
new file mode 100644
index 0000000..9287c21
--- /dev/null
+++ b/casbin/persist/adapters/asyncio/adapter.py
@@ -0,0 +1,32 @@
+from abc import ABCMeta, abstractmethod
+
+
+class AsyncAdapter(metaclass=ABCMeta):
+    """The interface for async Casbin adapters."""
+
+    @abstractmethod
+    async def load_policy(self, model):
+        """loads all policy rules from the storage."""
+        pass
+
+    @abstractmethod
+    async def save_policy(self, model):
+        """saves all policy rules to the storage."""
+        pass
+
+    @abstractmethod
+    async def add_policy(self, sec, ptype, rule):
+        """adds a policy rule to the storage."""
+        pass
+
+    @abstractmethod
+    async def remove_policy(self, sec, ptype, rule):
+        """removes a policy rule from the storage."""
+        pass
+
+    @abstractmethod
+    async def remove_filtered_policy(self, sec, ptype, field_index, *field_values):
+        """removes policy rules that match the filter from the storage.
+        This is part of the Auto-Save feature.
+        """
+        pass
diff --git a/casbin/persist/adapters/asyncio/adapter_filtered.py b/casbin/persist/adapters/asyncio/adapter_filtered.py
new file mode 100644
index 0000000..d77ec3b
--- /dev/null
+++ b/casbin/persist/adapters/asyncio/adapter_filtered.py
@@ -0,0 +1,17 @@
+from abc import ABCMeta, abstractmethod
+
+
+class AsyncFilteredAdapter(metaclass=ABCMeta):
+    """AsyncFilteredAdapter is the interface for async Casbin adapters supporting filtered policies."""
+
+    @abstractmethod
+    async def is_filtered(self):
+        """IsFiltered returns true if the loaded policy has been filtered
+        Marks if the loaded policy is filtered or not
+        """
+        pass
+
+    @abstractmethod
+    async def load_filtered_policy(self, model, filter):
+        """Loads policy rules that match the filter from the storage."""
+        pass
diff --git a/casbin/persist/adapters/asyncio/batch_adapter.py b/casbin/persist/adapters/asyncio/batch_adapter.py
new file mode 100644
index 0000000..00abc7a
--- /dev/null
+++ b/casbin/persist/adapters/asyncio/batch_adapter.py
@@ -0,0 +1,15 @@
+from abc import ABCMeta, abstractmethod
+
+
+class AsyncBatchAdapter(metaclass=ABCMeta):
+    """AsyncBatchAdapter is the interface for async Casbin adapters with multiple add and remove policy functions."""
+
+    @abstractmethod
+    async def add_policies(self, sec, ptype, rules):
+        """AddPolicies adds policy rules to the storage."""
+        pass
+
+    @abstractmethod
+    async def remove_policies(self, sec, ptype, rules):
+        """RemovePolicies removes policy rules from the storage."""
+        pass
diff --git a/casbin/persist/adapters/async_file_adapter.py b/casbin/persist/adapters/asyncio/file_adapter.py
similarity index 93%
rename from casbin/persist/adapters/async_file_adapter.py
rename to casbin/persist/adapters/asyncio/file_adapter.py
index 2734131..274eeff 100644
--- a/casbin/persist/adapters/async_file_adapter.py
+++ b/casbin/persist/adapters/asyncio/file_adapter.py
@@ -12,11 +12,13 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-from casbin import persist
 import os
 
+from ...adapter import load_policy_line
+from .adapter import AsyncAdapter
 
-class AsyncFileAdapter(persist.Adapter):
+
+class AsyncFileAdapter(AsyncAdapter):
     """the async file adapter for Casbin.
     It can load policy from file or save policy to file.
     """
@@ -42,7 +44,7 @@ class AsyncFileAdapter(persist.Adapter):
         with open(self._file_path, "rb") as file:
             line = file.readline()
             while line:
-                persist.load_policy_line(line.decode().strip(), model)
+                load_policy_line(line.decode().strip(), model)
                 line = file.readline()
 
     def _save_policy_file(self, model):
diff --git a/casbin/persist/adapters/asyncio/update_adapter.py b/casbin/persist/adapters/asyncio/update_adapter.py
new file mode 100644
index 0000000..bcce22d
--- /dev/null
+++ b/casbin/persist/adapters/asyncio/update_adapter.py
@@ -0,0 +1,27 @@
+from abc import ABCMeta, abstractmethod
+
+
+class AsyncUpdateAdapter(metaclass=ABCMeta):
+    """AsyncUpdateAdapter is the interface for async Casbin adapters with add update policy function."""
+
+    @abstractmethod
+    async def update_policy(self, sec, ptype, old_rule, new_policy):
+        """
+        update_policy updates a policy rule from storage.
+        This is part of the Auto-Save feature.
+        """
+        pass
+
+    @abstractmethod
+    async def update_policies(self, sec, ptype, old_rules, new_rules):
+        """
+        UpdatePolicies updates some policy rules to storage, like db, redis.
+        """
+        pass
+
+    @abstractmethod
+    async def update_filtered_policies(self, sec, ptype, new_rules, field_index, *field_values):
+        """
+        update_filtered_policies deletes old rules and adds new rules.
+        """
+        pass
diff --git a/casbin/persist/adapters/file_adapter.py b/casbin/persist/adapters/file_adapter.py
index 0e80759..d5727a7 100644
--- a/casbin/persist/adapters/file_adapter.py
+++ b/casbin/persist/adapters/file_adapter.py
@@ -12,11 +12,12 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-from casbin import persist
 import os
 
+from ..adapter import Adapter, load_policy_line
 
-class FileAdapter(persist.Adapter):
+
+class FileAdapter(Adapter):
     """the file adapter for Casbin.
     It can load policy from file or save policy to file.
     """
@@ -42,7 +43,7 @@ class FileAdapter(persist.Adapter):
         with open(self._file_path, "rb") as file:
             line = file.readline()
             while line:
-                persist.load_policy_line(line.decode().strip(), model)
+                load_policy_line(line.decode().strip(), model)
                 line = file.readline()
 
     def _save_policy_file(self, model):
diff --git a/casbin/persist/adapters/filtered_file_adapter.py b/casbin/persist/adapters/filtered_file_adapter.py
new file mode 100644
index 0000000..eeb3a6c
--- /dev/null
+++ b/casbin/persist/adapters/filtered_file_adapter.py
@@ -0,0 +1,111 @@
+# Copyright 2021 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 os
+
+from casbin import persist
+
+from .file_adapter import FileAdapter
+from ..adapter_filtered import FilteredAdapter
+
+
+class Filter:
+    # P,G are string []
+    P = []
+    G = []
+
+
+class FilteredFileAdapter(FileAdapter, FilteredAdapter):
+    filtered = False
+    _file_path = ""
+    filter = Filter()
+
+    # new_filtered_adapter is the constructor for FilteredAdapter.
+    def __init__(self, file_path):
+        self.filtered = True
+        self._file_path = file_path
+
+    def load_policy(self, model):
+        if not os.path.isfile(self._file_path):
+            raise RuntimeError("invalid file path, file path cannot be empty")
+        self.filtered = False
+        self._load_policy_file(model)
+
+    # load_filtered_policy loads only policy rules that match the filter.
+    def load_filtered_policy(self, model, filter):
+        if filter == None:
+            return self.load_policy(model)
+
+        if not os.path.isfile(self._file_path):
+            raise RuntimeError("invalid file path, file path cannot be empty")
+
+        try:
+            filter_value = [filter.__dict__["P"]] + [filter.__dict__["G"]]
+        except:
+            raise RuntimeError("invalid filter type")
+
+        self.load_filtered_policy_file(model, filter_value, persist.load_policy_line)
+        self.filtered = True
+
+    def load_filtered_policy_file(self, model, filter, hanlder):
+        with open(self._file_path, "rb") as file:
+            while True:
+                line = file.readline()
+                line = line.decode().strip()
+                if line == "\n":
+                    continue
+                if not line:
+                    break
+                if filter_line(line, filter):
+                    continue
+
+                hanlder(line, model)
+
+    # is_filtered returns true if the loaded policy has been filtered.
+    def is_filtered(self):
+        return self.filtered
+
+    def save_policy(self, model):
+        if self.filtered:
+            raise RuntimeError("cannot save a filtered policy")
+
+        self._save_policy_file(model)
+
+
+def filter_line(line, filter):
+    if filter == None:
+        return False
+
+    p = line.split(",")
+    if len(p) == 0:
+        return True
+    filter_slice = []
+
+    if p[0].strip() == "p":
+        filter_slice = filter[0]
+    elif p[0].strip() == "g":
+        filter_slice = filter[1]
+    return filter_words(p, filter_slice)
+
+
+def filter_words(line, filter):
+    if len(line) < len(filter) + 1:
+        return True
+    skip_line = False
+    for i, v in enumerate(filter):
+        if len(v) > 0 and (v.strip() != line[i + 1].strip()):
+            skip_line = True
+            break
+
+    return skip_line
diff --git a/casbin/persist/adapters/string_adapter.py b/casbin/persist/adapters/string_adapter.py
index 0076cae..b2e9e95 100644
--- a/casbin/persist/adapters/string_adapter.py
+++ b/casbin/persist/adapters/string_adapter.py
@@ -12,13 +12,12 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-from casbin import persist, load_policy_line
-import os
-
 from casbin.util import util
 
+from ..adapter import Adapter, load_policy_line
+
 
-class StringAdapter(persist.Adapter):
+class StringAdapter(Adapter):
     """the string adapter for Casbin.
     It can load policy from string or save policy to string.
     """
diff --git a/casbin/persist/adapters/update_adapter.py b/casbin/persist/adapters/update_adapter.py
index 94a8c01..542be9a 100644
--- a/casbin/persist/adapters/update_adapter.py
+++ b/casbin/persist/adapters/update_adapter.py
@@ -1,36 +1,6 @@
-# Copyright 2021 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.
+# NOTE: this file exists as a backwards compatible alias. please directly
+# use `casbin.persist.update_adapter` instead.
 
+from ..update_adapter import UpdateAdapter
 
-class UpdateAdapter:
-    """UpdateAdapter is the interface for Casbin adapters with add update policy function."""
-
-    def update_policy(self, sec, ptype, old_rule, new_policy):
-        """
-        update_policy updates a policy rule from storage.
-        This is part of the Auto-Save feature.
-        """
-        pass
-
-    def update_policies(self, sec, ptype, old_rules, new_rules):
-        """
-        UpdatePolicies updates some policy rules to storage, like db, redis.
-        """
-        pass
-
-    def update_filtered_policies(self, sec, ptype, new_rules, field_index, *field_values):
-        """
-        update_filtered_policies deletes old rules and adds new rules.
-        """
-        pass
+__all__ = ["UpdateAdapter"]
diff --git a/casbin/persist/update_adapter.py b/casbin/persist/update_adapter.py
new file mode 100644
index 0000000..94a8c01
--- /dev/null
+++ b/casbin/persist/update_adapter.py
@@ -0,0 +1,36 @@
+# Copyright 2021 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.
+
+
+class UpdateAdapter:
+    """UpdateAdapter is the interface for Casbin adapters with add update policy function."""
+
+    def update_policy(self, sec, ptype, old_rule, new_policy):
+        """
+        update_policy updates a policy rule from storage.
+        This is part of the Auto-Save feature.
+        """
+        pass
+
+    def update_policies(self, sec, ptype, old_rules, new_rules):
+        """
+        UpdatePolicies updates some policy rules to storage, like db, redis.
+        """
+        pass
+
+    def update_filtered_policies(self, sec, ptype, new_rules, field_index, *field_values):
+        """
+        update_filtered_policies deletes old rules and adds new rules.
+        """
+        pass
diff --git a/requirements_dev.txt b/requirements_dev.txt
index 53cde02..af37e82 100644
--- a/requirements_dev.txt
+++ b/requirements_dev.txt
@@ -1,4 +1,4 @@
 -r requirements.txt
-black==21.6b0
+black==23.11.0
 pytest==7.0.1
 pytest-benchmark==3.4.1
\ No newline at end of file


Test patch:
diff --git a/tests/__init__.py b/tests/__init__.py
index 6e64b18..e3d8d3c 100644
--- a/tests/__init__.py
+++ b/tests/__init__.py
@@ -12,15 +12,11 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-from . import benchmarks
-from . import config
-from . import model
-from . import rbac
-from . import util
+from . import benchmarks, config, model, rbac, util
 from .test_distributed_api import TestDistributedApi
 from .test_enforcer import *
 from .test_fast_enforcer import TestFastEnforcer
-from .test_filter import TestFilteredAdapter
+from .test_filter import TestFilteredFileAdapter
 from .test_frontend import TestFrontend
 from .test_management_api import TestManagementApi, TestManagementApiSynced
 from .test_rbac_api import TestRbacApi, TestRbacApiSynced
diff --git a/tests/test_enforcer.py b/tests/test_enforcer.py
index 8df3407..11af386 100644
--- a/tests/test_enforcer.py
+++ b/tests/test_enforcer.py
@@ -25,7 +25,7 @@ def get_examples(path):
     return os.path.abspath(examples_path + path)
 
 
-class TestSub:
+class MockSub:
     def __init__(self, name, age):
         self.name = name
         self.age = age
@@ -249,8 +249,8 @@ class TestConfig(TestCaseBase):
         enforce_context = e.new_enforce_context("2")
         enforce_context.etype = "e"
 
-        sub1 = TestSub("alice", 70)
-        sub2 = TestSub("bob", 30)
+        sub1 = MockSub("alice", 70)
+        sub2 = MockSub("bob", 30)
 
         self.assertTrue(e.enforce("alice", "data2", "read"))
         self.assertFalse(e.enforce(enforce_context, sub1, "/data1", "read"))
@@ -370,9 +370,9 @@ class TestConfig(TestCaseBase):
     def test_abac_with_sub_rule(self):
         e = self.get_enforcer(get_examples("abac_rule_model.conf"), get_examples("abac_rule_policy.csv"))
 
-        sub1 = TestSub("alice", 16)
-        sub2 = TestSub("bob", 20)
-        sub3 = TestSub("alice", 65)
+        sub1 = MockSub("alice", 16)
+        sub2 = MockSub("bob", 20)
+        sub3 = MockSub("alice", 65)
 
         self.assertFalse(e.enforce(sub1, "/data1", "read"))
         self.assertFalse(e.enforce(sub1, "/data2", "read"))
@@ -395,10 +395,10 @@ class TestConfig(TestCaseBase):
             get_examples("abac_multiple_rules_policy.csv"),
         )
 
-        sub1 = TestSub("alice", 16)
-        sub2 = TestSub("alice", 20)
-        sub3 = TestSub("bob", 65)
-        sub4 = TestSub("bob", 35)
+        sub1 = MockSub("alice", 16)
+        sub2 = MockSub("alice", 20)
+        sub3 = MockSub("bob", 65)
+        sub4 = MockSub("bob", 35)
 
         self.assertFalse(e.enforce(sub1, "/data1", "read"))
         self.assertFalse(e.enforce(sub1, "/data2", "read"))
@@ -834,8 +834,8 @@ class TestConfigAsync(IsolatedAsyncioTestCase):
         enforce_context = e.new_enforce_context("2")
         enforce_context.etype = "e"
 
-        sub1 = TestSub("alice", 70)
-        sub2 = TestSub("bob", 30)
+        sub1 = MockSub("alice", 70)
+        sub2 = MockSub("bob", 30)
 
         self.assertTrue(e.enforce("alice", "data2", "read"))
         self.assertFalse(e.enforce(enforce_context, sub1, "/data1", "read"))
@@ -970,9 +970,9 @@ class TestConfigAsync(IsolatedAsyncioTestCase):
         e = self.get_enforcer(get_examples("abac_rule_model.conf"), get_examples("abac_rule_policy.csv"))
         await e.load_policy()
 
-        sub1 = TestSub("alice", 16)
-        sub2 = TestSub("bob", 20)
-        sub3 = TestSub("alice", 65)
+        sub1 = MockSub("alice", 16)
+        sub2 = MockSub("bob", 20)
+        sub3 = MockSub("alice", 65)
 
         self.assertFalse(e.enforce(sub1, "/data1", "read"))
         self.assertFalse(e.enforce(sub1, "/data2", "read"))
@@ -996,10 +996,10 @@ class TestConfigAsync(IsolatedAsyncioTestCase):
         )
         await e.load_policy()
 
-        sub1 = TestSub("alice", 16)
-        sub2 = TestSub("alice", 20)
-        sub3 = TestSub("bob", 65)
-        sub4 = TestSub("bob", 35)
+        sub1 = MockSub("alice", 16)
+        sub2 = MockSub("alice", 20)
+        sub3 = MockSub("bob", 65)
+        sub4 = MockSub("bob", 35)
 
         self.assertFalse(e.enforce(sub1, "/data1", "read"))
         self.assertFalse(e.enforce(sub1, "/data2", "read"))
diff --git a/tests/test_filter.py b/tests/test_filter.py
index 7515160..536505f 100644
--- a/tests/test_filter.py
+++ b/tests/test_filter.py
@@ -15,6 +15,7 @@
 import casbin
 from unittest import TestCase
 from tests.test_enforcer import get_examples
+from casbin.persist.adapters import FilteredFileAdapter
 
 
 class Filter:
@@ -23,14 +24,14 @@ class Filter:
     G = []
 
 
-class TestFilteredAdapter(TestCase):
+class TestFilteredFileAdapter(TestCase):
     def test_init_filtered_adapter(self):
-        adapter = casbin.persist.adapters.FilteredAdapter(get_examples("rbac_with_domains_policy.csv"))
+        adapter = FilteredFileAdapter(get_examples("rbac_with_domains_policy.csv"))
         e = casbin.Enforcer(get_examples("rbac_with_domains_model.conf"), adapter)
         self.assertFalse(e.has_policy(["admin", "domain1", "data1", "read"]))
 
     def test_load_filtered_policy(self):
-        adapter = casbin.persist.adapters.FilteredAdapter(get_examples("rbac_with_domains_policy.csv"))
+        adapter = FilteredFileAdapter(get_examples("rbac_with_domains_policy.csv"))
         e = casbin.Enforcer(get_examples("rbac_with_domains_model.conf"), adapter)
         filter = Filter()
         filter.P = ["", "domain1"]
@@ -59,7 +60,7 @@ class TestFilteredAdapter(TestCase):
             e.get_adapter().save_policy(e.get_model())
 
     def test_append_filtered_policy(self):
-        adapter = casbin.persist.adapters.FilteredAdapter(get_examples("rbac_with_domains_policy.csv"))
+        adapter = FilteredFileAdapter(get_examples("rbac_with_domains_policy.csv"))
         e = casbin.Enforcer(get_examples("rbac_with_domains_model.conf"), adapter)
         filter = Filter()
         filter.P = ["", "domain1"]
@@ -92,7 +93,7 @@ class TestFilteredAdapter(TestCase):
         self.assertTrue(e.has_policy(["admin", "domain2", "data2", "read"]))
 
     def test_filtered_policy_invalid_filter(self):
-        adapter = casbin.persist.adapters.FilteredAdapter(get_examples("rbac_with_domains_policy.csv"))
+        adapter = FilteredFileAdapter(get_examples("rbac_with_domains_policy.csv"))
         e = casbin.Enforcer(get_examples("rbac_with_domains_model.conf"), adapter)
         filter = ["", "domain1"]
 
@@ -100,7 +101,7 @@ class TestFilteredAdapter(TestCase):
             e.load_filtered_policy(filter)
 
     def test_filtered_policy_empty_filter(self):
-        adapter = casbin.persist.adapters.FilteredAdapter(get_examples("rbac_with_domains_policy.csv"))
+        adapter = FilteredFileAdapter(get_examples("rbac_with_domains_policy.csv"))
         e = casbin.Enforcer(get_examples("rbac_with_domains_model.conf"), adapter)
 
         try:
@@ -128,14 +129,14 @@ class TestFilteredAdapter(TestCase):
             e.load_filtered_policy(filter)
 
     def test_filtered_adapter_empty_filepath(self):
-        adapter = casbin.persist.adapters.FilteredAdapter("")
+        adapter = FilteredFileAdapter("")
         e = casbin.Enforcer(get_examples("rbac_with_domains_model.conf"), adapter)
 
         with self.assertRaises(RuntimeError):
             e.load_filtered_policy(None)
 
     def test_filtered_adapter_invalid_filepath(self):
-        adapter = casbin.persist.adapters.FilteredAdapter(get_examples("does_not_exist_policy.csv"))
+        adapter = FilteredFileAdapter(get_examples("does_not_exist_policy.csv"))
         e = casbin.Enforcer(get_examples("rbac_with_domains_model.conf"), adapter)
 
         with self.assertRaises(RuntimeError):