Skip to content

Commit 17a2d44

Browse files
committed
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
1 parent 577bb3c commit 17a2d44

File tree

2 files changed

+144
-0
lines changed

2 files changed

+144
-0
lines changed

rest_framework/decorators.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,64 +87,90 @@ def handler(self, *args, **kwargs):
8787
return decorator
8888

8989

90+
def _check_decorator_order(func, decorator_name):
91+
"""
92+
Check if an API policy decorator is being applied after @api_view.
93+
"""
94+
# Check if func is actually a view function (result of APIView.as_view())
95+
if hasattr(func, 'cls') and issubclass(func.cls, APIView):
96+
raise TypeError(
97+
f"@{decorator_name} must be applied before @api_view. "
98+
f"The correct order is:\n\n"
99+
f" @api_view(['GET'])\n"
100+
f" @{decorator_name}(...)\n"
101+
f" def my_view(request):\n"
102+
f" ...\n\n"
103+
f"See https://www.django-rest-framework.org/api-guide/views/#api-policy-decorators"
104+
)
105+
106+
90107
def renderer_classes(renderer_classes):
91108
def decorator(func):
109+
_check_decorator_order(func, 'renderer_classes')
92110
func.renderer_classes = renderer_classes
93111
return func
94112
return decorator
95113

96114

97115
def parser_classes(parser_classes):
98116
def decorator(func):
117+
_check_decorator_order(func, 'parser_classes')
99118
func.parser_classes = parser_classes
100119
return func
101120
return decorator
102121

103122

104123
def authentication_classes(authentication_classes):
105124
def decorator(func):
125+
_check_decorator_order(func, 'authentication_classes')
106126
func.authentication_classes = authentication_classes
107127
return func
108128
return decorator
109129

110130

111131
def throttle_classes(throttle_classes):
112132
def decorator(func):
133+
_check_decorator_order(func, 'throttle_classes')
113134
func.throttle_classes = throttle_classes
114135
return func
115136
return decorator
116137

117138

118139
def permission_classes(permission_classes):
119140
def decorator(func):
141+
_check_decorator_order(func, 'permission_classes')
120142
func.permission_classes = permission_classes
121143
return func
122144
return decorator
123145

124146

125147
def content_negotiation_class(content_negotiation_class):
126148
def decorator(func):
149+
_check_decorator_order(func, 'content_negotiation_class')
127150
func.content_negotiation_class = content_negotiation_class
128151
return func
129152
return decorator
130153

131154

132155
def metadata_class(metadata_class):
133156
def decorator(func):
157+
_check_decorator_order(func, 'metadata_class')
134158
func.metadata_class = metadata_class
135159
return func
136160
return decorator
137161

138162

139163
def versioning_class(versioning_class):
140164
def decorator(func):
165+
_check_decorator_order(func, 'versioning_class')
141166
func.versioning_class = versioning_class
142167
return func
143168
return decorator
144169

145170

146171
def schema(view_inspector):
147172
def decorator(func):
173+
_check_decorator_order(func, 'schema')
148174
func.schema = view_inspector
149175
return func
150176
return decorator

tests/test_decorators.py

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,124 @@ def view(request):
204204

205205
assert isinstance(view.cls.schema, CustomSchema)
206206

207+
def test_incorrect_decorator_order_permission_classes(self):
208+
"""
209+
If @permission_classes is applied after @api_view, we should raise a TypeError.
210+
"""
211+
with self.assertRaises(TypeError) as cm:
212+
@permission_classes([IsAuthenticated])
213+
@api_view(['GET'])
214+
def view(request):
215+
return Response({})
216+
217+
assert '@permission_classes must be applied before @api_view' in str(cm.exception)
218+
219+
def test_incorrect_decorator_order_renderer_classes(self):
220+
"""
221+
If @renderer_classes is applied after @api_view, we should raise a TypeError.
222+
"""
223+
with self.assertRaises(TypeError) as cm:
224+
@renderer_classes([JSONRenderer])
225+
@api_view(['GET'])
226+
def view(request):
227+
return Response({})
228+
229+
assert '@renderer_classes must be applied before @api_view' in str(cm.exception)
230+
231+
def test_incorrect_decorator_order_parser_classes(self):
232+
"""
233+
If @parser_classes is applied after @api_view, we should raise a TypeError.
234+
"""
235+
with self.assertRaises(TypeError) as cm:
236+
@parser_classes([JSONParser])
237+
@api_view(['GET'])
238+
def view(request):
239+
return Response({})
240+
241+
assert '@parser_classes must be applied before @api_view' in str(cm.exception)
242+
243+
def test_incorrect_decorator_order_authentication_classes(self):
244+
"""
245+
If @authentication_classes is applied after @api_view, we should raise a TypeError.
246+
"""
247+
with self.assertRaises(TypeError) as cm:
248+
@authentication_classes([BasicAuthentication])
249+
@api_view(['GET'])
250+
def view(request):
251+
return Response({})
252+
253+
assert '@authentication_classes must be applied before @api_view' in str(cm.exception)
254+
255+
def test_incorrect_decorator_order_throttle_classes(self):
256+
"""
257+
If @throttle_classes is applied after @api_view, we should raise a TypeError.
258+
"""
259+
class OncePerDayUserThrottle(UserRateThrottle):
260+
rate = '1/day'
261+
262+
with self.assertRaises(TypeError) as cm:
263+
@throttle_classes([OncePerDayUserThrottle])
264+
@api_view(['GET'])
265+
def view(request):
266+
return Response({})
267+
268+
assert '@throttle_classes must be applied before @api_view' in str(cm.exception)
269+
270+
def test_incorrect_decorator_order_versioning_class(self):
271+
"""
272+
If @versioning_class is applied after @api_view, we should raise a TypeError.
273+
"""
274+
with self.assertRaises(TypeError) as cm:
275+
@versioning_class(QueryParameterVersioning)
276+
@api_view(['GET'])
277+
def view(request):
278+
return Response({})
279+
280+
assert '@versioning_class must be applied before @api_view' in str(cm.exception)
281+
282+
def test_incorrect_decorator_order_metadata_class(self):
283+
"""
284+
If @metadata_class is applied after @api_view, we should raise a TypeError.
285+
"""
286+
with self.assertRaises(TypeError) as cm:
287+
@metadata_class(None)
288+
@api_view(['GET'])
289+
def view(request):
290+
return Response({})
291+
292+
assert '@metadata_class must be applied before @api_view' in str(cm.exception)
293+
294+
def test_incorrect_decorator_order_content_negotiation_class(self):
295+
"""
296+
If @content_negotiation_class is applied after @api_view, we should raise a TypeError.
297+
"""
298+
class CustomContentNegotiation(BaseContentNegotiation):
299+
def select_renderer(self, request, renderers, format_suffix):
300+
return (renderers[0], renderers[0].media_type)
301+
302+
with self.assertRaises(TypeError) as cm:
303+
@content_negotiation_class(CustomContentNegotiation)
304+
@api_view(['GET'])
305+
def view(request):
306+
return Response({})
307+
308+
assert '@content_negotiation_class must be applied before @api_view' in str(cm.exception)
309+
310+
def test_incorrect_decorator_order_schema(self):
311+
"""
312+
If @schema is applied after @api_view, we should raise a TypeError.
313+
"""
314+
class CustomSchema(AutoSchema):
315+
pass
316+
317+
with self.assertRaises(TypeError) as cm:
318+
@schema(CustomSchema())
319+
@api_view(['GET'])
320+
def view(request):
321+
return Response({})
322+
323+
assert '@schema must be applied before @api_view' in str(cm.exception)
324+
207325

208326
class ActionDecoratorTestCase(TestCase):
209327

0 commit comments

Comments
 (0)