mirror of
https://github.com/encode/django-rest-framework.git
synced 2025-12-04 16:54:02 +03:00
Add validation for decorator order with @api_view
Raise TypeError when API policy decorators (@permission_classes, @renderer_classes, etc.) are applied after @api_view instead of before it. Fixes #9596
This commit is contained in:
parent
577bb3c819
commit
17a2d44abc
|
|
@ -87,8 +87,26 @@ def api_view(http_method_names=None):
|
||||||
return decorator
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
|
def _check_decorator_order(func, decorator_name):
|
||||||
|
"""
|
||||||
|
Check if an API policy decorator is being applied after @api_view.
|
||||||
|
"""
|
||||||
|
# Check if func is actually a view function (result of APIView.as_view())
|
||||||
|
if hasattr(func, 'cls') and issubclass(func.cls, APIView):
|
||||||
|
raise TypeError(
|
||||||
|
f"@{decorator_name} must be applied before @api_view. "
|
||||||
|
f"The correct order is:\n\n"
|
||||||
|
f" @api_view(['GET'])\n"
|
||||||
|
f" @{decorator_name}(...)\n"
|
||||||
|
f" def my_view(request):\n"
|
||||||
|
f" ...\n\n"
|
||||||
|
f"See https://www.django-rest-framework.org/api-guide/views/#api-policy-decorators"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def renderer_classes(renderer_classes):
|
def renderer_classes(renderer_classes):
|
||||||
def decorator(func):
|
def decorator(func):
|
||||||
|
_check_decorator_order(func, 'renderer_classes')
|
||||||
func.renderer_classes = renderer_classes
|
func.renderer_classes = renderer_classes
|
||||||
return func
|
return func
|
||||||
return decorator
|
return decorator
|
||||||
|
|
@ -96,6 +114,7 @@ def renderer_classes(renderer_classes):
|
||||||
|
|
||||||
def parser_classes(parser_classes):
|
def parser_classes(parser_classes):
|
||||||
def decorator(func):
|
def decorator(func):
|
||||||
|
_check_decorator_order(func, 'parser_classes')
|
||||||
func.parser_classes = parser_classes
|
func.parser_classes = parser_classes
|
||||||
return func
|
return func
|
||||||
return decorator
|
return decorator
|
||||||
|
|
@ -103,6 +122,7 @@ def parser_classes(parser_classes):
|
||||||
|
|
||||||
def authentication_classes(authentication_classes):
|
def authentication_classes(authentication_classes):
|
||||||
def decorator(func):
|
def decorator(func):
|
||||||
|
_check_decorator_order(func, 'authentication_classes')
|
||||||
func.authentication_classes = authentication_classes
|
func.authentication_classes = authentication_classes
|
||||||
return func
|
return func
|
||||||
return decorator
|
return decorator
|
||||||
|
|
@ -110,6 +130,7 @@ def authentication_classes(authentication_classes):
|
||||||
|
|
||||||
def throttle_classes(throttle_classes):
|
def throttle_classes(throttle_classes):
|
||||||
def decorator(func):
|
def decorator(func):
|
||||||
|
_check_decorator_order(func, 'throttle_classes')
|
||||||
func.throttle_classes = throttle_classes
|
func.throttle_classes = throttle_classes
|
||||||
return func
|
return func
|
||||||
return decorator
|
return decorator
|
||||||
|
|
@ -117,6 +138,7 @@ def throttle_classes(throttle_classes):
|
||||||
|
|
||||||
def permission_classes(permission_classes):
|
def permission_classes(permission_classes):
|
||||||
def decorator(func):
|
def decorator(func):
|
||||||
|
_check_decorator_order(func, 'permission_classes')
|
||||||
func.permission_classes = permission_classes
|
func.permission_classes = permission_classes
|
||||||
return func
|
return func
|
||||||
return decorator
|
return decorator
|
||||||
|
|
@ -124,6 +146,7 @@ def permission_classes(permission_classes):
|
||||||
|
|
||||||
def content_negotiation_class(content_negotiation_class):
|
def content_negotiation_class(content_negotiation_class):
|
||||||
def decorator(func):
|
def decorator(func):
|
||||||
|
_check_decorator_order(func, 'content_negotiation_class')
|
||||||
func.content_negotiation_class = content_negotiation_class
|
func.content_negotiation_class = content_negotiation_class
|
||||||
return func
|
return func
|
||||||
return decorator
|
return decorator
|
||||||
|
|
@ -131,6 +154,7 @@ def content_negotiation_class(content_negotiation_class):
|
||||||
|
|
||||||
def metadata_class(metadata_class):
|
def metadata_class(metadata_class):
|
||||||
def decorator(func):
|
def decorator(func):
|
||||||
|
_check_decorator_order(func, 'metadata_class')
|
||||||
func.metadata_class = metadata_class
|
func.metadata_class = metadata_class
|
||||||
return func
|
return func
|
||||||
return decorator
|
return decorator
|
||||||
|
|
@ -138,6 +162,7 @@ def metadata_class(metadata_class):
|
||||||
|
|
||||||
def versioning_class(versioning_class):
|
def versioning_class(versioning_class):
|
||||||
def decorator(func):
|
def decorator(func):
|
||||||
|
_check_decorator_order(func, 'versioning_class')
|
||||||
func.versioning_class = versioning_class
|
func.versioning_class = versioning_class
|
||||||
return func
|
return func
|
||||||
return decorator
|
return decorator
|
||||||
|
|
@ -145,6 +170,7 @@ def versioning_class(versioning_class):
|
||||||
|
|
||||||
def schema(view_inspector):
|
def schema(view_inspector):
|
||||||
def decorator(func):
|
def decorator(func):
|
||||||
|
_check_decorator_order(func, 'schema')
|
||||||
func.schema = view_inspector
|
func.schema = view_inspector
|
||||||
return func
|
return func
|
||||||
return decorator
|
return decorator
|
||||||
|
|
|
||||||
|
|
@ -204,6 +204,124 @@ class DecoratorTestCase(TestCase):
|
||||||
|
|
||||||
assert isinstance(view.cls.schema, CustomSchema)
|
assert isinstance(view.cls.schema, CustomSchema)
|
||||||
|
|
||||||
|
def test_incorrect_decorator_order_permission_classes(self):
|
||||||
|
"""
|
||||||
|
If @permission_classes is applied after @api_view, we should raise a TypeError.
|
||||||
|
"""
|
||||||
|
with self.assertRaises(TypeError) as cm:
|
||||||
|
@permission_classes([IsAuthenticated])
|
||||||
|
@api_view(['GET'])
|
||||||
|
def view(request):
|
||||||
|
return Response({})
|
||||||
|
|
||||||
|
assert '@permission_classes must be applied before @api_view' in str(cm.exception)
|
||||||
|
|
||||||
|
def test_incorrect_decorator_order_renderer_classes(self):
|
||||||
|
"""
|
||||||
|
If @renderer_classes is applied after @api_view, we should raise a TypeError.
|
||||||
|
"""
|
||||||
|
with self.assertRaises(TypeError) as cm:
|
||||||
|
@renderer_classes([JSONRenderer])
|
||||||
|
@api_view(['GET'])
|
||||||
|
def view(request):
|
||||||
|
return Response({})
|
||||||
|
|
||||||
|
assert '@renderer_classes must be applied before @api_view' in str(cm.exception)
|
||||||
|
|
||||||
|
def test_incorrect_decorator_order_parser_classes(self):
|
||||||
|
"""
|
||||||
|
If @parser_classes is applied after @api_view, we should raise a TypeError.
|
||||||
|
"""
|
||||||
|
with self.assertRaises(TypeError) as cm:
|
||||||
|
@parser_classes([JSONParser])
|
||||||
|
@api_view(['GET'])
|
||||||
|
def view(request):
|
||||||
|
return Response({})
|
||||||
|
|
||||||
|
assert '@parser_classes must be applied before @api_view' in str(cm.exception)
|
||||||
|
|
||||||
|
def test_incorrect_decorator_order_authentication_classes(self):
|
||||||
|
"""
|
||||||
|
If @authentication_classes is applied after @api_view, we should raise a TypeError.
|
||||||
|
"""
|
||||||
|
with self.assertRaises(TypeError) as cm:
|
||||||
|
@authentication_classes([BasicAuthentication])
|
||||||
|
@api_view(['GET'])
|
||||||
|
def view(request):
|
||||||
|
return Response({})
|
||||||
|
|
||||||
|
assert '@authentication_classes must be applied before @api_view' in str(cm.exception)
|
||||||
|
|
||||||
|
def test_incorrect_decorator_order_throttle_classes(self):
|
||||||
|
"""
|
||||||
|
If @throttle_classes is applied after @api_view, we should raise a TypeError.
|
||||||
|
"""
|
||||||
|
class OncePerDayUserThrottle(UserRateThrottle):
|
||||||
|
rate = '1/day'
|
||||||
|
|
||||||
|
with self.assertRaises(TypeError) as cm:
|
||||||
|
@throttle_classes([OncePerDayUserThrottle])
|
||||||
|
@api_view(['GET'])
|
||||||
|
def view(request):
|
||||||
|
return Response({})
|
||||||
|
|
||||||
|
assert '@throttle_classes must be applied before @api_view' in str(cm.exception)
|
||||||
|
|
||||||
|
def test_incorrect_decorator_order_versioning_class(self):
|
||||||
|
"""
|
||||||
|
If @versioning_class is applied after @api_view, we should raise a TypeError.
|
||||||
|
"""
|
||||||
|
with self.assertRaises(TypeError) as cm:
|
||||||
|
@versioning_class(QueryParameterVersioning)
|
||||||
|
@api_view(['GET'])
|
||||||
|
def view(request):
|
||||||
|
return Response({})
|
||||||
|
|
||||||
|
assert '@versioning_class must be applied before @api_view' in str(cm.exception)
|
||||||
|
|
||||||
|
def test_incorrect_decorator_order_metadata_class(self):
|
||||||
|
"""
|
||||||
|
If @metadata_class is applied after @api_view, we should raise a TypeError.
|
||||||
|
"""
|
||||||
|
with self.assertRaises(TypeError) as cm:
|
||||||
|
@metadata_class(None)
|
||||||
|
@api_view(['GET'])
|
||||||
|
def view(request):
|
||||||
|
return Response({})
|
||||||
|
|
||||||
|
assert '@metadata_class must be applied before @api_view' in str(cm.exception)
|
||||||
|
|
||||||
|
def test_incorrect_decorator_order_content_negotiation_class(self):
|
||||||
|
"""
|
||||||
|
If @content_negotiation_class is applied after @api_view, we should raise a TypeError.
|
||||||
|
"""
|
||||||
|
class CustomContentNegotiation(BaseContentNegotiation):
|
||||||
|
def select_renderer(self, request, renderers, format_suffix):
|
||||||
|
return (renderers[0], renderers[0].media_type)
|
||||||
|
|
||||||
|
with self.assertRaises(TypeError) as cm:
|
||||||
|
@content_negotiation_class(CustomContentNegotiation)
|
||||||
|
@api_view(['GET'])
|
||||||
|
def view(request):
|
||||||
|
return Response({})
|
||||||
|
|
||||||
|
assert '@content_negotiation_class must be applied before @api_view' in str(cm.exception)
|
||||||
|
|
||||||
|
def test_incorrect_decorator_order_schema(self):
|
||||||
|
"""
|
||||||
|
If @schema is applied after @api_view, we should raise a TypeError.
|
||||||
|
"""
|
||||||
|
class CustomSchema(AutoSchema):
|
||||||
|
pass
|
||||||
|
|
||||||
|
with self.assertRaises(TypeError) as cm:
|
||||||
|
@schema(CustomSchema())
|
||||||
|
@api_view(['GET'])
|
||||||
|
def view(request):
|
||||||
|
return Response({})
|
||||||
|
|
||||||
|
assert '@schema must be applied before @api_view' in str(cm.exception)
|
||||||
|
|
||||||
|
|
||||||
class ActionDecoratorTestCase(TestCase):
|
class ActionDecoratorTestCase(TestCase):
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user