Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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 be applied before @api_view. "
f"The correct order is:\n\n"
f" @api_view(['GET'])\n"
f" @{decorator_name}(...)\n"
Copy link

Copilot AI Nov 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error message shows the decorators in the wrong order. When decorators are stacked, they are applied bottom-to-top, so @api_view should be on top and @{decorator_name} should be below it. The example currently shows:

@api_view(['GET'])
@{decorator_name}(...)
def my_view(request):
    ...

But this is the incorrect order being warned against. It should be:

@{decorator_name}(...)
@api_view(['GET'])
def my_view(request):
    ...
Suggested change
f" @api_view(['GET'])\n"
f" @{decorator_name}(...)\n"
f" @{decorator_name}(...)\n"
f" @api_view(['GET'])\n"

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The decorator needs to go below @api_view, just as the docs show—this way, DRF reads the attribute properly and permissions work.
Our tests confirm the documented order is correct; error is raised only if you swap them.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will get back to you on saturday morning

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, Copilot is wrong here, but that might be a symptom that "applied before" is a bit ambiguous in this case. The docs are actually using the term "after":

These must come after (below) the @api_view decorator

The docs refer to the order the decorators appear in the code: as we read top to bottom the order is swapped vs how they are applied by the code (bottom to top).

Understanding how decorators are applied by the language is important, but less experience people might not know it, and therefore won't understand the error message.

I'd rather make the language of the error message closer to what we have in the docs.

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 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 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):

Expand Down