From 570ada4b72d66f3ec1c2889e0b4d1392ae3d964f Mon Sep 17 00:00:00 2001 From: Cristopher Hernandez <22552070+CristopherH95@users.noreply.github.com> Date: Sat, 14 Mar 2026 13:12:21 -0700 Subject: [PATCH 1/4] Implement core async base permission --- adrf/permissions.py | 136 ++++++++++++++++++++++++++++++++++++++ tests/test_permissions.py | 74 ++++++++++++++++++++- 2 files changed, 209 insertions(+), 1 deletion(-) create mode 100644 adrf/permissions.py diff --git a/adrf/permissions.py b/adrf/permissions.py new file mode 100644 index 0000000..a4af85e --- /dev/null +++ b/adrf/permissions.py @@ -0,0 +1,136 @@ +import asyncio + +from asgiref.sync import async_to_sync, sync_to_async +from rest_framework import permissions + +class AsyncOperandHolderMixin: + def __and__(self, other): + return AsyncOperandHolder(AAND, self, other) + + def __or__(self, other): + return AsyncOperandHolder(AOR, self, other) + + def __rand__(self, other): + return AsyncOperandHolder(AAND, other, self) + + def __ror__(self, other): + return AsyncOperandHolder(AOR, other, self) + + def __invert__(self): + return AsyncSingleOperandHolder(ANOT, self) + + +class AsyncLogicOperatorMixin: + def __init__(self, op1, op2): + super().__init__(op1, op2) + self.op1_has_perm_is_async = asyncio.iscoroutinefunction(op1.has_permission) + self.op2_has_perm_is_async = asyncio.iscoroutinefunction(op2.has_permission) + self.op1_obj_perm_is_async = asyncio.iscoroutinefunction(op1.has_object_permission) + self.op2_obj_perm_is_async = asyncio.iscoroutinefunction(op2.has_object_permission) + + def _get_async_has_perm(self): + async_has_perm_a = ( + self.op1.has_permission if self.op1_has_perm_is_async else sync_to_async(self.op1.has_permission) + ) + async_has_perm_b = ( + self.op2.has_permission if self.op2_has_perm_is_async else sync_to_async(self.op2.has_permission) + ) + return async_has_perm_a, async_has_perm_b + + def _get_async_has_obj_perm(self): + async_obj_perm_a = ( + self.op1.has_object_permission + if self.op1_obj_perm_is_async else sync_to_async(self.op1.has_object_permission) + ) + async_obj_perm_b = ( + self.op2.has_object_permission + if self.op2_obj_perm_is_async else sync_to_async(self.op2.has_object_permission) + ) + return async_obj_perm_a, async_obj_perm_b + + +class AsyncSingleLogicOperatorMixin: + def __init__(self, op1): + super().__init__(op1) + self.op1_has_perm_is_async = asyncio.iscoroutinefunction(op1.has_permission) + self.op1_obj_perm_is_async = asyncio.iscoroutinefunction(op1.has_object_permission) + + def _get_async_has_perm(self): + return self.op1.has_permission if self.op1_has_perm_is_async else sync_to_async(self.op1.has_permission) + + def _get_async_has_obj_perm(self): + return ( + self.op1.has_object_permission + if self.op1_obj_perm_is_async else sync_to_async(self.op1.has_object_permission) + ) + + +class AsyncSingleOperandHolder(AsyncOperandHolderMixin, permissions.SingleOperandHolder): + pass + + +class AsyncOperandHolder(AsyncOperandHolderMixin, permissions.OperandHolder): + pass + + +class AAND(AsyncLogicOperatorMixin, permissions.AND): + async def has_permission(self, request, view): + async_has_perm_a, async_has_perm_b = self._get_async_has_perm() + return ( + await async_has_perm_a(request, view) and await async_has_perm_b(request, view) + ) + + async def has_object_permission(self, request, view, obj): + async_obj_perm_a, async_obj_perm_b = self._get_async_has_obj_perm() + return ( + await async_obj_perm_a(request, view, obj) and + await async_obj_perm_b(request, view, obj) + ) + + +class AOR(AsyncLogicOperatorMixin, permissions.OR): + async def has_permission(self, request, view): + async_has_perm_a, async_has_perm_b = self._get_async_has_perm() + return ( + await async_has_perm_a(request, view) or + await async_has_perm_b(request, view) + ) + + async def has_object_permission(self, request, view, obj): + async_has_perm_a, async_has_perm_b = self._get_async_has_perm() + async_obj_perm_a, async_obj_perm_b = self._get_async_has_obj_perm() + return ( + await async_has_perm_a(request, view) + and await async_obj_perm_a(request, view, obj) + ) or ( + await async_has_perm_b(request, view) + and await async_obj_perm_b(request, view, obj) + ) + + +class ANOT(AsyncSingleLogicOperatorMixin, permissions.NOT): + async def has_permission(self, request, view): + async_has_perm = self._get_async_has_perm() + return not await async_has_perm(request, view) + + async def has_object_permission(self, request, view, obj): + async_obj_perm = self._get_async_has_obj_perm() + return not await async_obj_perm(request, view, obj) + + +class AsyncBasePermissionMetaClass(AsyncOperandHolderMixin, permissions.BasePermissionMetaclass): + pass + + +class AsyncBasePermission(permissions.BasePermission, metaclass=AsyncBasePermissionMetaClass): + async def has_permission(self, request, view): + """ + Return `True` if permission is granted, `False` otherwise. + """ + return True + + async def has_object_permission(self, request, view, obj): + """ + Return `True` if permission is granted, `False` otherwise. + """ + return True diff --git a/tests/test_permissions.py b/tests/test_permissions.py index 51ebfe7..1c237d6 100644 --- a/tests/test_permissions.py +++ b/tests/test_permissions.py @@ -1,14 +1,17 @@ +import unittest.mock + from django.http import HttpResponse from django.test import TestCase, override_settings from rest_framework.permissions import BasePermission from rest_framework.test import APIRequestFactory from adrf.views import APIView +from adrf.permissions import AsyncBasePermission factory = APIRequestFactory() -class AsyncPermission(BasePermission): +class AsyncPermission(AsyncBasePermission): async def has_permission(self, request, view): path = request.path_info.lstrip("/") @@ -21,6 +24,14 @@ async def has_object_permission(self, request, view, obj): return True +class AsyncRejectPermission(AsyncBasePermission): + async def has_permission(self, request, view): + return False + + async def has_object_permission(self, request, view, obj): + return False + + class SyncPermission(BasePermission): def has_permission(self, request, view): path = request.path_info.lstrip("/") @@ -73,3 +84,64 @@ async def test_sync_permission_reject(self): response = await MockView.as_view(permission_classes=(SyncPermission,))(request) self.assertEqual(response.status_code, 403) + + +class TestAsyncPermissionLogicOperators(TestCase): + async def test_pure_async_logical_and_permission(self): + request = factory.get("/view/async/allow/") + + with unittest.mock.patch.object(AsyncPermission, "has_permission", return_value=True) as mock_has_perm_a: + with unittest.mock.patch.object(AsyncRejectPermission, "has_permission", return_value=False) as mock_has_perm_b: + combined_permission = AsyncPermission & AsyncRejectPermission + response = await MockView.as_view(permission_classes=(combined_permission,))(request) + mock_has_perm_a.assert_awaited() + mock_has_perm_b.assert_awaited() + + self.assertEqual(response.status_code, 403) + + async def test_pure_async_logical_or_permission(self): + request = factory.get("/view/async/allow/") + + with unittest.mock.patch.object(AsyncPermission, "has_permission", return_value=True) as mock_has_perm_a: + with unittest.mock.patch.object(AsyncRejectPermission, "has_permission", + return_value=False) as mock_has_perm_b: + combined_permission = AsyncRejectPermission | AsyncPermission + response = await MockView.as_view(permission_classes=(combined_permission,))(request) + mock_has_perm_a.assert_awaited() + mock_has_perm_b.assert_awaited() + + self.assertEqual(response.status_code, 200) + + async def test_pure_async_logical_neg_permission(self): + request = factory.get("/view/async/allow/") + + with unittest.mock.patch.object(AsyncRejectPermission, "has_permission", return_value=False) as mock_has_perm: + negated_permission = ~AsyncRejectPermission + response = await MockView.as_view(permission_classes=(negated_permission,))(request) + mock_has_perm.assert_awaited() + + self.assertEqual(response.status_code, 200) + + async def test_mixed_logical_and_permission(self): + request = factory.get("/view/async/allow/") + + with unittest.mock.patch.object(AsyncPermission, "has_permission", return_value=True) as mock_has_perm_async: + with unittest.mock.patch.object(SyncPermission, "has_permission", return_value=True) as mock_has_perm_sync: + combined_permission = SyncPermission & AsyncPermission + response = await MockView.as_view(permission_classes=(combined_permission,))(request) + mock_has_perm_async.assert_awaited() + mock_has_perm_sync.assert_called() + + self.assertEqual(response.status_code, 200) + + async def test_mixed_logical_or_permission(self): + request = factory.get("/view/async/allow/") + + with unittest.mock.patch.object(AsyncRejectPermission, "has_permission", return_value=False) as mock_has_perm_async: + with unittest.mock.patch.object(SyncPermission, "has_permission", return_value=True) as mock_has_perm_sync: + combined_permission = AsyncRejectPermission | SyncPermission + response = await MockView.as_view(permission_classes=(combined_permission,))(request) + mock_has_perm_async.assert_awaited() + mock_has_perm_sync.assert_called() + + self.assertEqual(response.status_code, 200) From c0b0f5c72b1efac19907a13ce9a9177a74e2ab59 Mon Sep 17 00:00:00 2001 From: Cristopher Hernandez <22552070+CristopherH95@users.noreply.github.com> Date: Tue, 24 Mar 2026 15:23:49 -0700 Subject: [PATCH 2/4] Fix issues with mixed operator types --- adrf/permissions.py | 67 +++++++++++++++++++-------- adrf/views.py | 8 ++++ tests/test_permissions.py | 95 +++++++++++++++++++++++++-------------- 3 files changed, 117 insertions(+), 53 deletions(-) diff --git a/adrf/permissions.py b/adrf/permissions.py index a4af85e..269e14b 100644 --- a/adrf/permissions.py +++ b/adrf/permissions.py @@ -1,8 +1,40 @@ import asyncio -from asgiref.sync import async_to_sync, sync_to_async +from asgiref.sync import sync_to_async from rest_framework import permissions + +def try_convert_operator(operator_instance): + if not is_perm_operator(operator_instance): + return operator_instance + if isinstance(operator_instance, permissions.AND): + operator_class = AAND + operands = [operator_instance.op1, operator_instance.op2] + elif isinstance(operator_instance, permissions.OR): + operator_class = AOR + operands = [operator_instance.op1, operator_instance.op2] + elif isinstance(operator_instance, permissions.NOT): + operator_class = ANOT + operands = [operator_instance.op1] + else: + raise TypeError( + f"Cannot translate sync operator class '{operator_instance.__class__.__name__}' to async" + ) + operands = [ + try_convert_operator(operand) + for operand in operands + ] + return operator_class(*operands) + + +def is_perm_operator(operator_instance): + return isinstance(operator_instance, (permissions.AND, permissions.OR, permissions.NOT)) + + +def is_async_perm_operator(operator_instance): + return isinstance(operator_instance, (AAND, AOR, ANOT)) + + class AsyncOperandHolderMixin: def __and__(self, other): return AsyncOperandHolder(AAND, self, other) @@ -21,47 +53,44 @@ def __invert__(self): class AsyncLogicOperatorMixin: - def __init__(self, op1, op2): - super().__init__(op1, op2) - self.op1_has_perm_is_async = asyncio.iscoroutinefunction(op1.has_permission) - self.op2_has_perm_is_async = asyncio.iscoroutinefunction(op2.has_permission) - self.op1_obj_perm_is_async = asyncio.iscoroutinefunction(op1.has_object_permission) - self.op2_obj_perm_is_async = asyncio.iscoroutinefunction(op2.has_object_permission) - def _get_async_has_perm(self): async_has_perm_a = ( - self.op1.has_permission if self.op1_has_perm_is_async else sync_to_async(self.op1.has_permission) + self.op1.has_permission + if asyncio.iscoroutinefunction(self.op1.has_permission) else sync_to_async(self.op1.has_permission) ) async_has_perm_b = ( - self.op2.has_permission if self.op2_has_perm_is_async else sync_to_async(self.op2.has_permission) + self.op2.has_permission + if asyncio.iscoroutinefunction(self.op2.has_permission) else sync_to_async(self.op2.has_permission) ) return async_has_perm_a, async_has_perm_b def _get_async_has_obj_perm(self): async_obj_perm_a = ( self.op1.has_object_permission - if self.op1_obj_perm_is_async else sync_to_async(self.op1.has_object_permission) + if asyncio.iscoroutinefunction(self.op1.has_object_permission) + else sync_to_async(self.op1.has_object_permission) ) async_obj_perm_b = ( self.op2.has_object_permission - if self.op2_obj_perm_is_async else sync_to_async(self.op2.has_object_permission) + if asyncio.iscoroutinefunction(self.op2.has_object_permission) + else sync_to_async(self.op2.has_object_permission) ) return async_obj_perm_a, async_obj_perm_b class AsyncSingleLogicOperatorMixin: - def __init__(self, op1): - super().__init__(op1) - self.op1_has_perm_is_async = asyncio.iscoroutinefunction(op1.has_permission) - self.op1_obj_perm_is_async = asyncio.iscoroutinefunction(op1.has_object_permission) - def _get_async_has_perm(self): - return self.op1.has_permission if self.op1_has_perm_is_async else sync_to_async(self.op1.has_permission) + return ( + self.op1.has_permission + if asyncio.iscoroutinefunction(self.op1.has_permission) + else sync_to_async(self.op1.has_permission) + ) def _get_async_has_obj_perm(self): return ( self.op1.has_object_permission - if self.op1_obj_perm_is_async else sync_to_async(self.op1.has_object_permission) + if asyncio.iscoroutinefunction(self.op1.has_object_permission) + else sync_to_async(self.op1.has_object_permission) ) diff --git a/adrf/views.py b/adrf/views.py index 27fe8af..bd971d8 100755 --- a/adrf/views.py +++ b/adrf/views.py @@ -8,6 +8,7 @@ from rest_framework.views import APIView as DRFAPIView from adrf.requests import AsyncRequest +from adrf.permissions import try_convert_operator class APIView(DRFAPIView): @@ -99,6 +100,13 @@ def initialize_request(self, request, *args, **kwargs): parser_context=parser_context, ) + def get_permissions(self): + permissions = super().get_permissions() + return [ + try_convert_operator(permission) + for permission in permissions + ] + def check_permissions(self, request: Request) -> None: permissions = self.get_permissions() diff --git a/tests/test_permissions.py b/tests/test_permissions.py index 1c237d6..3ffeb4f 100644 --- a/tests/test_permissions.py +++ b/tests/test_permissions.py @@ -87,61 +87,88 @@ async def test_sync_permission_reject(self): class TestAsyncPermissionLogicOperators(TestCase): - async def test_pure_async_logical_and_permission(self): + @unittest.mock.patch.object(AsyncPermission, "has_permission", return_value=True) + @unittest.mock.patch.object(AsyncRejectPermission, "has_permission", return_value=False) + async def test_pure_async_logical_and_permission(self, mock_has_perm_a, mock_has_perm_b): request = factory.get("/view/async/allow/") + combined_permission = AsyncPermission & AsyncRejectPermission - with unittest.mock.patch.object(AsyncPermission, "has_permission", return_value=True) as mock_has_perm_a: - with unittest.mock.patch.object(AsyncRejectPermission, "has_permission", return_value=False) as mock_has_perm_b: - combined_permission = AsyncPermission & AsyncRejectPermission - response = await MockView.as_view(permission_classes=(combined_permission,))(request) - mock_has_perm_a.assert_awaited() - mock_has_perm_b.assert_awaited() + response = await MockView.as_view(permission_classes=(combined_permission,))(request) + mock_has_perm_a.assert_awaited() + mock_has_perm_b.assert_awaited() self.assertEqual(response.status_code, 403) - async def test_pure_async_logical_or_permission(self): + @unittest.mock.patch.object(AsyncPermission, "has_permission", return_value=True) + @unittest.mock.patch.object(AsyncRejectPermission, "has_permission", return_value=False) + async def test_pure_async_logical_or_permission(self, mock_has_perm_a, mock_has_perm_b): request = factory.get("/view/async/allow/") + combined_permission = AsyncRejectPermission | AsyncPermission - with unittest.mock.patch.object(AsyncPermission, "has_permission", return_value=True) as mock_has_perm_a: - with unittest.mock.patch.object(AsyncRejectPermission, "has_permission", - return_value=False) as mock_has_perm_b: - combined_permission = AsyncRejectPermission | AsyncPermission - response = await MockView.as_view(permission_classes=(combined_permission,))(request) - mock_has_perm_a.assert_awaited() - mock_has_perm_b.assert_awaited() + response = await MockView.as_view(permission_classes=(combined_permission,))(request) + mock_has_perm_a.assert_awaited() + mock_has_perm_b.assert_awaited() self.assertEqual(response.status_code, 200) - async def test_pure_async_logical_neg_permission(self): + @unittest.mock.patch.object(AsyncRejectPermission, "has_permission", return_value=False) + async def test_pure_async_logical_neg_permission(self, mock_has_perm): request = factory.get("/view/async/allow/") + negated_permission = ~AsyncRejectPermission - with unittest.mock.patch.object(AsyncRejectPermission, "has_permission", return_value=False) as mock_has_perm: - negated_permission = ~AsyncRejectPermission - response = await MockView.as_view(permission_classes=(negated_permission,))(request) - mock_has_perm.assert_awaited() + response = await MockView.as_view(permission_classes=(negated_permission,))(request) + mock_has_perm.assert_awaited() self.assertEqual(response.status_code, 200) - async def test_mixed_logical_and_permission(self): + @unittest.mock.patch.object(SyncPermission, "has_permission", return_value=True) + @unittest.mock.patch.object(AsyncPermission, "has_permission", return_value=True) + async def test_mixed_logical_and_permission(self, mock_has_perm_async, mock_has_perm_sync): request = factory.get("/view/async/allow/") + combined_permission = SyncPermission & AsyncPermission - with unittest.mock.patch.object(AsyncPermission, "has_permission", return_value=True) as mock_has_perm_async: - with unittest.mock.patch.object(SyncPermission, "has_permission", return_value=True) as mock_has_perm_sync: - combined_permission = SyncPermission & AsyncPermission - response = await MockView.as_view(permission_classes=(combined_permission,))(request) - mock_has_perm_async.assert_awaited() - mock_has_perm_sync.assert_called() + response = await MockView.as_view(permission_classes=(combined_permission,))(request) + mock_has_perm_async.assert_awaited() + mock_has_perm_sync.assert_called() self.assertEqual(response.status_code, 200) - async def test_mixed_logical_or_permission(self): + @unittest.mock.patch.object(SyncPermission, "has_permission", return_value=True) + @unittest.mock.patch.object(AsyncRejectPermission, "has_permission", return_value=False) + async def test_mixed_logical_or_permission(self, mock_has_perm_async, mock_has_perm_sync): request = factory.get("/view/async/allow/") + combined_permission = AsyncRejectPermission | SyncPermission - with unittest.mock.patch.object(AsyncRejectPermission, "has_permission", return_value=False) as mock_has_perm_async: - with unittest.mock.patch.object(SyncPermission, "has_permission", return_value=True) as mock_has_perm_sync: - combined_permission = AsyncRejectPermission | SyncPermission - response = await MockView.as_view(permission_classes=(combined_permission,))(request) - mock_has_perm_async.assert_awaited() - mock_has_perm_sync.assert_called() + response = await MockView.as_view(permission_classes=(combined_permission,))(request) + mock_has_perm_async.assert_awaited() + mock_has_perm_sync.assert_called() + self.assertEqual(response.status_code, 200) + + @unittest.mock.patch.object(SyncPermission, "has_permission", return_value=False) + @unittest.mock.patch.object(AsyncPermission, "has_permission", return_value=True) + @unittest.mock.patch.object(AsyncRejectPermission, "has_permission", return_value=False) + async def test_async_first_complex_mixed_permission(self, mock_async_reject, mock_async_accept, mock_sync_reject): + request = factory.get("/view/async/allow/") + combined_permission = AsyncPermission & (SyncPermission | ~AsyncRejectPermission) + + response = await MockView.as_view(permission_classes=(combined_permission,))(request) + + mock_async_reject.assert_awaited() + mock_async_accept.assert_awaited() + mock_sync_reject.assert_called() + self.assertEqual(response.status_code, 200) + + @unittest.mock.patch.object(SyncPermission, "has_permission", return_value=True) + @unittest.mock.patch.object(AsyncPermission, "has_permission", return_value=False) + @unittest.mock.patch.object(AsyncRejectPermission, "has_permission", return_value=False) + async def test_sync_first_complex_mixed_permission(self, mock_async_reject, mock_async_accept, mock_sync_reject): + request = factory.get("/view/async/allow/") + combined_permission = SyncPermission & (AsyncPermission | ~AsyncRejectPermission) + + response = await MockView.as_view(permission_classes=(combined_permission,))(request) + + mock_async_reject.assert_awaited() + mock_async_accept.assert_awaited() + mock_sync_reject.assert_called() self.assertEqual(response.status_code, 200) From 2d30ca747a0384e4f9dad1472b18fe6bbe3ddfe9 Mon Sep 17 00:00:00 2001 From: Cristopher Hernandez <22552070+CristopherH95@users.noreply.github.com> Date: Tue, 24 Mar 2026 15:37:03 -0700 Subject: [PATCH 3/4] Clean up permissions module --- adrf/permissions.py | 83 ++++++++++++++++++++++++++++++++++++--------- 1 file changed, 67 insertions(+), 16 deletions(-) diff --git a/adrf/permissions.py b/adrf/permissions.py index 269e14b..e26d71a 100644 --- a/adrf/permissions.py +++ b/adrf/permissions.py @@ -5,21 +5,32 @@ def try_convert_operator(operator_instance): + """ + Helper function which attempts to convert a given permissions operator (i.e., AND, OR, NOT) and any sub-operators + to their async equivalent. This addresses issues with mixed operator types, where a sync operator (e.g., AND) + does not await the result of an async sub-operator. If the given parameter is NOT an operator, then this function + returns the original argument unchanged. + """ if not is_perm_operator(operator_instance): return operator_instance + if is_async_perm_operator(operator_instance): + # Avoid mixed async/sync operators within the operands of the given async operator + if isinstance(operator_instance, (AAND, AOR)): + operator_instance.op1 = try_convert_operator(operator_instance.op1) + operator_instance.op2 = try_convert_operator(operator_instance.op2) + else: + operator_instance.op1 = try_convert_operator(operator_instance.op1) + return operator_instance + # Convert sync operator types to async if isinstance(operator_instance, permissions.AND): operator_class = AAND operands = [operator_instance.op1, operator_instance.op2] elif isinstance(operator_instance, permissions.OR): operator_class = AOR operands = [operator_instance.op1, operator_instance.op2] - elif isinstance(operator_instance, permissions.NOT): + else: operator_class = ANOT operands = [operator_instance.op1] - else: - raise TypeError( - f"Cannot translate sync operator class '{operator_instance.__class__.__name__}' to async" - ) operands = [ try_convert_operator(operand) for operand in operands @@ -28,14 +39,24 @@ def try_convert_operator(operator_instance): def is_perm_operator(operator_instance): + """ + Helper function which checks whether the given parameter is a permissions operator (i.e., AND, OR, NOT). + """ return isinstance(operator_instance, (permissions.AND, permissions.OR, permissions.NOT)) def is_async_perm_operator(operator_instance): + """ + Helper function which checks whether the given parameter is an async permissions operator (i.e., AAND, AOR, ANOT). + """ return isinstance(operator_instance, (AAND, AOR, ANOT)) class AsyncOperandHolderMixin: + """ + Async version of rest framework's operand holder mixin. This uses the async versions of permissions operators, + rather than the sync equivalents. + """ def __and__(self, other): return AsyncOperandHolder(AAND, self, other) @@ -53,7 +74,10 @@ def __invert__(self): class AsyncLogicOperatorMixin: - def _get_async_has_perm(self): + """ + Mixin containing common methods for permissions logic operators with two operands. + """ + def get_async_has_perm(self): async_has_perm_a = ( self.op1.has_permission if asyncio.iscoroutinefunction(self.op1.has_permission) else sync_to_async(self.op1.has_permission) @@ -64,7 +88,7 @@ def _get_async_has_perm(self): ) return async_has_perm_a, async_has_perm_b - def _get_async_has_obj_perm(self): + def get_async_has_obj_perm(self): async_obj_perm_a = ( self.op1.has_object_permission if asyncio.iscoroutinefunction(self.op1.has_object_permission) @@ -79,14 +103,17 @@ def _get_async_has_obj_perm(self): class AsyncSingleLogicOperatorMixin: - def _get_async_has_perm(self): + """ + Mixin containing common methods for permissions logic operators with one operand. + """ + def get_async_has_perm(self): return ( self.op1.has_permission if asyncio.iscoroutinefunction(self.op1.has_permission) else sync_to_async(self.op1.has_permission) ) - def _get_async_has_obj_perm(self): + def get_async_has_obj_perm(self): return ( self.op1.has_object_permission if asyncio.iscoroutinefunction(self.op1.has_object_permission) @@ -95,22 +122,32 @@ def _get_async_has_obj_perm(self): class AsyncSingleOperandHolder(AsyncOperandHolderMixin, permissions.SingleOperandHolder): + """ + Extension to the rest framework single operand holder which uses async operators. + """ pass class AsyncOperandHolder(AsyncOperandHolderMixin, permissions.OperandHolder): + """ + Extension to the rest framework operand holder which uses async operators. + """ pass class AAND(AsyncLogicOperatorMixin, permissions.AND): + """ + Asynchronous logical AND operator for permissions checks, based on the synchronous equivalent defined by rest + framework. + """ async def has_permission(self, request, view): - async_has_perm_a, async_has_perm_b = self._get_async_has_perm() + async_has_perm_a, async_has_perm_b = self.get_async_has_perm() return ( await async_has_perm_a(request, view) and await async_has_perm_b(request, view) ) async def has_object_permission(self, request, view, obj): - async_obj_perm_a, async_obj_perm_b = self._get_async_has_obj_perm() + async_obj_perm_a, async_obj_perm_b = self.get_async_has_obj_perm() return ( await async_obj_perm_a(request, view, obj) and await async_obj_perm_b(request, view, obj) @@ -118,16 +155,20 @@ async def has_object_permission(self, request, view, obj): class AOR(AsyncLogicOperatorMixin, permissions.OR): + """ + Asynchronous logical OR operator for permissions checks, based on the synchronous equivalent defined by rest + framework. + """ async def has_permission(self, request, view): - async_has_perm_a, async_has_perm_b = self._get_async_has_perm() + async_has_perm_a, async_has_perm_b = self.get_async_has_perm() return ( await async_has_perm_a(request, view) or await async_has_perm_b(request, view) ) async def has_object_permission(self, request, view, obj): - async_has_perm_a, async_has_perm_b = self._get_async_has_perm() - async_obj_perm_a, async_obj_perm_b = self._get_async_has_obj_perm() + async_has_perm_a, async_has_perm_b = self.get_async_has_perm() + async_obj_perm_a, async_obj_perm_b = self.get_async_has_obj_perm() return ( await async_has_perm_a(request, view) and await async_obj_perm_a(request, view, obj) @@ -138,20 +179,30 @@ async def has_object_permission(self, request, view, obj): class ANOT(AsyncSingleLogicOperatorMixin, permissions.NOT): + """ + Asynchronous logical NOT operator for permissions checks, based on the synchronous equivalent defined by rest + framework. + """ async def has_permission(self, request, view): - async_has_perm = self._get_async_has_perm() + async_has_perm = self.get_async_has_perm() return not await async_has_perm(request, view) async def has_object_permission(self, request, view, obj): - async_obj_perm = self._get_async_has_obj_perm() + async_obj_perm = self.get_async_has_obj_perm() return not await async_obj_perm(request, view, obj) class AsyncBasePermissionMetaClass(AsyncOperandHolderMixin, permissions.BasePermissionMetaclass): + """ + Extension to the rest framework base permission metaclass which uses async operators. + """ pass class AsyncBasePermission(permissions.BasePermission, metaclass=AsyncBasePermissionMetaClass): + """ + Asynchronous base permission which can be combined with other permissions using logical operators. + """ async def has_permission(self, request, view): """ Return `True` if permission is granted, `False` otherwise. From 9ed62aca39ba38cb62fafacaeb7b04fb0fe7ecb5 Mon Sep 17 00:00:00 2001 From: Cristopher Hernandez <22552070+CristopherH95@users.noreply.github.com> Date: Tue, 24 Mar 2026 15:47:41 -0700 Subject: [PATCH 4/4] Run formatting --- adrf/permissions.py | 51 +++++++++++++++-------- adrf/views.py | 7 +--- tests/test_permissions.py | 86 +++++++++++++++++++++++++++++---------- 3 files changed, 99 insertions(+), 45 deletions(-) diff --git a/adrf/permissions.py b/adrf/permissions.py index e26d71a..41523f3 100644 --- a/adrf/permissions.py +++ b/adrf/permissions.py @@ -31,10 +31,7 @@ def try_convert_operator(operator_instance): else: operator_class = ANOT operands = [operator_instance.op1] - operands = [ - try_convert_operator(operand) - for operand in operands - ] + operands = [try_convert_operator(operand) for operand in operands] return operator_class(*operands) @@ -42,7 +39,9 @@ def is_perm_operator(operator_instance): """ Helper function which checks whether the given parameter is a permissions operator (i.e., AND, OR, NOT). """ - return isinstance(operator_instance, (permissions.AND, permissions.OR, permissions.NOT)) + return isinstance( + operator_instance, (permissions.AND, permissions.OR, permissions.NOT) + ) def is_async_perm_operator(operator_instance): @@ -57,6 +56,7 @@ class AsyncOperandHolderMixin: Async version of rest framework's operand holder mixin. This uses the async versions of permissions operators, rather than the sync equivalents. """ + def __and__(self, other): return AsyncOperandHolder(AAND, self, other) @@ -77,14 +77,17 @@ class AsyncLogicOperatorMixin: """ Mixin containing common methods for permissions logic operators with two operands. """ + def get_async_has_perm(self): async_has_perm_a = ( self.op1.has_permission - if asyncio.iscoroutinefunction(self.op1.has_permission) else sync_to_async(self.op1.has_permission) + if asyncio.iscoroutinefunction(self.op1.has_permission) + else sync_to_async(self.op1.has_permission) ) async_has_perm_b = ( self.op2.has_permission - if asyncio.iscoroutinefunction(self.op2.has_permission) else sync_to_async(self.op2.has_permission) + if asyncio.iscoroutinefunction(self.op2.has_permission) + else sync_to_async(self.op2.has_permission) ) return async_has_perm_a, async_has_perm_b @@ -106,6 +109,7 @@ class AsyncSingleLogicOperatorMixin: """ Mixin containing common methods for permissions logic operators with one operand. """ + def get_async_has_perm(self): return ( self.op1.has_permission @@ -121,10 +125,13 @@ def get_async_has_obj_perm(self): ) -class AsyncSingleOperandHolder(AsyncOperandHolderMixin, permissions.SingleOperandHolder): +class AsyncSingleOperandHolder( + AsyncOperandHolderMixin, permissions.SingleOperandHolder +): """ Extension to the rest framework single operand holder which uses async operators. """ + pass @@ -132,6 +139,7 @@ class AsyncOperandHolder(AsyncOperandHolderMixin, permissions.OperandHolder): """ Extension to the rest framework operand holder which uses async operators. """ + pass @@ -140,17 +148,17 @@ class AAND(AsyncLogicOperatorMixin, permissions.AND): Asynchronous logical AND operator for permissions checks, based on the synchronous equivalent defined by rest framework. """ + async def has_permission(self, request, view): async_has_perm_a, async_has_perm_b = self.get_async_has_perm() - return ( - await async_has_perm_a(request, view) and await async_has_perm_b(request, view) + return await async_has_perm_a(request, view) and await async_has_perm_b( + request, view ) async def has_object_permission(self, request, view, obj): async_obj_perm_a, async_obj_perm_b = self.get_async_has_obj_perm() - return ( - await async_obj_perm_a(request, view, obj) and - await async_obj_perm_b(request, view, obj) + return await async_obj_perm_a(request, view, obj) and await async_obj_perm_b( + request, view, obj ) @@ -159,11 +167,11 @@ class AOR(AsyncLogicOperatorMixin, permissions.OR): Asynchronous logical OR operator for permissions checks, based on the synchronous equivalent defined by rest framework. """ + async def has_permission(self, request, view): async_has_perm_a, async_has_perm_b = self.get_async_has_perm() - return ( - await async_has_perm_a(request, view) or - await async_has_perm_b(request, view) + return await async_has_perm_a(request, view) or await async_has_perm_b( + request, view ) async def has_object_permission(self, request, view, obj): @@ -183,6 +191,7 @@ class ANOT(AsyncSingleLogicOperatorMixin, permissions.NOT): Asynchronous logical NOT operator for permissions checks, based on the synchronous equivalent defined by rest framework. """ + async def has_permission(self, request, view): async_has_perm = self.get_async_has_perm() return not await async_has_perm(request, view) @@ -192,17 +201,23 @@ async def has_object_permission(self, request, view, obj): return not await async_obj_perm(request, view, obj) -class AsyncBasePermissionMetaClass(AsyncOperandHolderMixin, permissions.BasePermissionMetaclass): +class AsyncBasePermissionMetaClass( + AsyncOperandHolderMixin, permissions.BasePermissionMetaclass +): """ Extension to the rest framework base permission metaclass which uses async operators. """ + pass -class AsyncBasePermission(permissions.BasePermission, metaclass=AsyncBasePermissionMetaClass): +class AsyncBasePermission( + permissions.BasePermission, metaclass=AsyncBasePermissionMetaClass +): """ Asynchronous base permission which can be combined with other permissions using logical operators. """ + async def has_permission(self, request, view): """ Return `True` if permission is granted, `False` otherwise. diff --git a/adrf/views.py b/adrf/views.py index bd971d8..579d403 100755 --- a/adrf/views.py +++ b/adrf/views.py @@ -7,8 +7,8 @@ from rest_framework.throttling import BaseThrottle from rest_framework.views import APIView as DRFAPIView -from adrf.requests import AsyncRequest from adrf.permissions import try_convert_operator +from adrf.requests import AsyncRequest class APIView(DRFAPIView): @@ -102,10 +102,7 @@ def initialize_request(self, request, *args, **kwargs): def get_permissions(self): permissions = super().get_permissions() - return [ - try_convert_operator(permission) - for permission in permissions - ] + return [try_convert_operator(permission) for permission in permissions] def check_permissions(self, request: Request) -> None: permissions = self.get_permissions() diff --git a/tests/test_permissions.py b/tests/test_permissions.py index 3ffeb4f..566e741 100644 --- a/tests/test_permissions.py +++ b/tests/test_permissions.py @@ -5,8 +5,8 @@ from rest_framework.permissions import BasePermission from rest_framework.test import APIRequestFactory -from adrf.views import APIView from adrf.permissions import AsyncBasePermission +from adrf.views import APIView factory = APIRequestFactory() @@ -88,58 +88,84 @@ async def test_sync_permission_reject(self): class TestAsyncPermissionLogicOperators(TestCase): @unittest.mock.patch.object(AsyncPermission, "has_permission", return_value=True) - @unittest.mock.patch.object(AsyncRejectPermission, "has_permission", return_value=False) - async def test_pure_async_logical_and_permission(self, mock_has_perm_a, mock_has_perm_b): + @unittest.mock.patch.object( + AsyncRejectPermission, "has_permission", return_value=False + ) + async def test_pure_async_logical_and_permission( + self, mock_has_perm_a, mock_has_perm_b + ): request = factory.get("/view/async/allow/") combined_permission = AsyncPermission & AsyncRejectPermission - response = await MockView.as_view(permission_classes=(combined_permission,))(request) + response = await MockView.as_view(permission_classes=(combined_permission,))( + request + ) mock_has_perm_a.assert_awaited() mock_has_perm_b.assert_awaited() self.assertEqual(response.status_code, 403) @unittest.mock.patch.object(AsyncPermission, "has_permission", return_value=True) - @unittest.mock.patch.object(AsyncRejectPermission, "has_permission", return_value=False) - async def test_pure_async_logical_or_permission(self, mock_has_perm_a, mock_has_perm_b): + @unittest.mock.patch.object( + AsyncRejectPermission, "has_permission", return_value=False + ) + async def test_pure_async_logical_or_permission( + self, mock_has_perm_a, mock_has_perm_b + ): request = factory.get("/view/async/allow/") combined_permission = AsyncRejectPermission | AsyncPermission - response = await MockView.as_view(permission_classes=(combined_permission,))(request) + response = await MockView.as_view(permission_classes=(combined_permission,))( + request + ) mock_has_perm_a.assert_awaited() mock_has_perm_b.assert_awaited() self.assertEqual(response.status_code, 200) - @unittest.mock.patch.object(AsyncRejectPermission, "has_permission", return_value=False) + @unittest.mock.patch.object( + AsyncRejectPermission, "has_permission", return_value=False + ) async def test_pure_async_logical_neg_permission(self, mock_has_perm): request = factory.get("/view/async/allow/") negated_permission = ~AsyncRejectPermission - response = await MockView.as_view(permission_classes=(negated_permission,))(request) + response = await MockView.as_view(permission_classes=(negated_permission,))( + request + ) mock_has_perm.assert_awaited() self.assertEqual(response.status_code, 200) @unittest.mock.patch.object(SyncPermission, "has_permission", return_value=True) @unittest.mock.patch.object(AsyncPermission, "has_permission", return_value=True) - async def test_mixed_logical_and_permission(self, mock_has_perm_async, mock_has_perm_sync): + async def test_mixed_logical_and_permission( + self, mock_has_perm_async, mock_has_perm_sync + ): request = factory.get("/view/async/allow/") combined_permission = SyncPermission & AsyncPermission - response = await MockView.as_view(permission_classes=(combined_permission,))(request) + response = await MockView.as_view(permission_classes=(combined_permission,))( + request + ) mock_has_perm_async.assert_awaited() mock_has_perm_sync.assert_called() self.assertEqual(response.status_code, 200) @unittest.mock.patch.object(SyncPermission, "has_permission", return_value=True) - @unittest.mock.patch.object(AsyncRejectPermission, "has_permission", return_value=False) - async def test_mixed_logical_or_permission(self, mock_has_perm_async, mock_has_perm_sync): + @unittest.mock.patch.object( + AsyncRejectPermission, "has_permission", return_value=False + ) + async def test_mixed_logical_or_permission( + self, mock_has_perm_async, mock_has_perm_sync + ): request = factory.get("/view/async/allow/") combined_permission = AsyncRejectPermission | SyncPermission - response = await MockView.as_view(permission_classes=(combined_permission,))(request) + response = await MockView.as_view(permission_classes=(combined_permission,))( + request + ) mock_has_perm_async.assert_awaited() mock_has_perm_sync.assert_called() @@ -147,12 +173,20 @@ async def test_mixed_logical_or_permission(self, mock_has_perm_async, mock_has_p @unittest.mock.patch.object(SyncPermission, "has_permission", return_value=False) @unittest.mock.patch.object(AsyncPermission, "has_permission", return_value=True) - @unittest.mock.patch.object(AsyncRejectPermission, "has_permission", return_value=False) - async def test_async_first_complex_mixed_permission(self, mock_async_reject, mock_async_accept, mock_sync_reject): + @unittest.mock.patch.object( + AsyncRejectPermission, "has_permission", return_value=False + ) + async def test_async_first_complex_mixed_permission( + self, mock_async_reject, mock_async_accept, mock_sync_reject + ): request = factory.get("/view/async/allow/") - combined_permission = AsyncPermission & (SyncPermission | ~AsyncRejectPermission) + combined_permission = AsyncPermission & ( + SyncPermission | ~AsyncRejectPermission + ) - response = await MockView.as_view(permission_classes=(combined_permission,))(request) + response = await MockView.as_view(permission_classes=(combined_permission,))( + request + ) mock_async_reject.assert_awaited() mock_async_accept.assert_awaited() @@ -161,12 +195,20 @@ async def test_async_first_complex_mixed_permission(self, mock_async_reject, moc @unittest.mock.patch.object(SyncPermission, "has_permission", return_value=True) @unittest.mock.patch.object(AsyncPermission, "has_permission", return_value=False) - @unittest.mock.patch.object(AsyncRejectPermission, "has_permission", return_value=False) - async def test_sync_first_complex_mixed_permission(self, mock_async_reject, mock_async_accept, mock_sync_reject): + @unittest.mock.patch.object( + AsyncRejectPermission, "has_permission", return_value=False + ) + async def test_sync_first_complex_mixed_permission( + self, mock_async_reject, mock_async_accept, mock_sync_reject + ): request = factory.get("/view/async/allow/") - combined_permission = SyncPermission & (AsyncPermission | ~AsyncRejectPermission) + combined_permission = SyncPermission & ( + AsyncPermission | ~AsyncRejectPermission + ) - response = await MockView.as_view(permission_classes=(combined_permission,))(request) + response = await MockView.as_view(permission_classes=(combined_permission,))( + request + ) mock_async_reject.assert_awaited() mock_async_accept.assert_awaited()