Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions rest_framework/decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,64 +87,90 @@ def handler(self, *args, **kwargs):
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 come after (below) the @api_view decorator. "
"The correct order is:\n\n"
" @api_view(['GET'])\n"
f" @{decorator_name}(...)\n"
" def my_view(request):\n"
" ...\n\n"
"See https://www.django-rest-framework.org/api-guide/views/#api-policy-decorators"
)


def renderer_classes(renderer_classes):
def decorator(func):
_check_decorator_order(func, 'renderer_classes')
func.renderer_classes = renderer_classes
return func
return decorator


def parser_classes(parser_classes):
def decorator(func):
_check_decorator_order(func, 'parser_classes')
func.parser_classes = parser_classes
return func
return decorator


def authentication_classes(authentication_classes):
def decorator(func):
_check_decorator_order(func, 'authentication_classes')
func.authentication_classes = authentication_classes
return func
return decorator


def throttle_classes(throttle_classes):
def decorator(func):
_check_decorator_order(func, 'throttle_classes')
func.throttle_classes = throttle_classes
return func
return decorator


def permission_classes(permission_classes):
def decorator(func):
_check_decorator_order(func, 'permission_classes')
func.permission_classes = permission_classes
return func
return decorator


def content_negotiation_class(content_negotiation_class):
def decorator(func):
_check_decorator_order(func, 'content_negotiation_class')
func.content_negotiation_class = content_negotiation_class
return func
return decorator


def metadata_class(metadata_class):
def decorator(func):
_check_decorator_order(func, 'metadata_class')
func.metadata_class = metadata_class
return func
return decorator


def versioning_class(versioning_class):
def decorator(func):
_check_decorator_order(func, 'versioning_class')
func.versioning_class = versioning_class
return func
return decorator


def schema(view_inspector):
def decorator(func):
_check_decorator_order(func, 'schema')
func.schema = view_inspector
return func
return decorator
Expand Down
118 changes: 118 additions & 0 deletions tests/test_decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,124 @@ def view(request):

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 come after (below) the @api_view decorator' 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 come after (below) the @api_view decorator' 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 come after (below) the @api_view decorator' 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 come after (below) the @api_view decorator' 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 come after (below) the @api_view decorator' 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 come after (below) the @api_view decorator' 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 come after (below) the @api_view decorator' 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 come after (below) the @api_view decorator' 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 come after (below) the @api_view decorator' in str(cm.exception)


class ActionDecoratorTestCase(TestCase):

Expand Down