-
Notifications
You must be signed in to change notification settings - Fork 273
New issue
Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? # to your account
Improve handling of status codes. #573
Changes from all commits
9d4ba5f
1e9fd0f
51a9867
60b4f07
e9f1ec1
8f28e40
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -15,6 +15,7 @@ | |
from rest_framework.schemas.inspectors import ViewInspector | ||
from rest_framework.schemas.utils import get_pk_description # type: ignore | ||
from rest_framework.settings import api_settings | ||
from rest_framework.status import is_success | ||
from rest_framework.utils.model_meta import get_field_info | ||
from rest_framework.views import APIView | ||
|
||
|
@@ -1010,7 +1011,7 @@ def get_examples(self): | |
""" override this for custom behaviour """ | ||
return [] | ||
|
||
def _get_examples(self, serializer, direction, media_type, status_code=None, extras=None): | ||
def _get_examples(self, serializer, direction, media_type, status_code: typing.Optional[int] = None, extras=None): | ||
""" Handles examples for request/response. purposefully ignores parameter examples """ | ||
|
||
# don't let the parameter examples influence the serializer example retrieval | ||
|
@@ -1033,7 +1034,7 @@ def _get_examples(self, serializer, direction, media_type, status_code=None, ext | |
continue | ||
if media_type and media_type != example.media_type: | ||
continue | ||
if status_code and status_code not in example.status_codes: | ||
if status_code and status_code not in (example.status_codes or [200, 201]): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. By postponing the default values here, we allow |
||
continue | ||
filtered_examples.append(example) | ||
|
||
|
@@ -1127,22 +1128,23 @@ def _get_response_bodies(self): | |
if self.method == 'DELETE': | ||
return {'204': {'description': _('No response body')}} | ||
if self._is_create_operation(): | ||
return {'201': self._get_response_for_code(response_serializers, '201')} | ||
return {'200': self._get_response_for_code(response_serializers, '200')} | ||
return {'201': self._get_response_for_code(response_serializers, 201)} | ||
return {'200': self._get_response_for_code(response_serializers, 200)} | ||
elif isinstance(response_serializers, dict): | ||
# custom handling for overriding default return codes with @extend_schema | ||
responses = {} | ||
for code, serializer in response_serializers.items(): | ||
if isinstance(code, tuple): | ||
code, media_types = str(code[0]), code[1:] | ||
for status_code, serializer in response_serializers.items(): | ||
if isinstance(status_code, tuple): | ||
status_code, *media_types = status_code | ||
else: | ||
code, media_types = str(code), None | ||
content_response = self._get_response_for_code(serializer, code, media_types) | ||
if code in responses: | ||
responses[code]['content'].update(content_response['content']) | ||
media_types = None | ||
status_code = int(status_code) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Casting to There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. unfortunately it would be a regression |
||
content_response = self._get_response_for_code(serializer, status_code, media_types) | ||
if status_code in responses: | ||
responses[status_code]['content'].update(content_response['content']) | ||
else: | ||
responses[code] = content_response | ||
return responses | ||
responses[status_code] = content_response | ||
return {str(k): v for k, v in responses.items()} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This could be removed if we wanted to pass back |
||
else: | ||
warn( | ||
f'could not resolve "{response_serializers}" for {self.method} {self.path}. ' | ||
|
@@ -1151,7 +1153,7 @@ def _get_response_bodies(self): | |
) | ||
schema = build_basic_type(OpenApiTypes.OBJECT) | ||
schema['description'] = _('Unspecified response body') | ||
return {'200': self._get_response_for_code(schema, '200')} | ||
return {'200': self._get_response_for_code(schema, 200)} | ||
|
||
def _unwrap_list_serializer(self, serializer, direction) -> typing.Optional[dict]: | ||
if is_field(serializer): | ||
|
@@ -1170,6 +1172,14 @@ def _get_response_for_code(self, serializer, status_code, media_types=None): | |
serializer, description, examples = ( | ||
serializer.response, serializer.description, serializer.examples | ||
) | ||
for example in examples: | ||
if example.status_codes is None: | ||
example.status_codes = [status_code] | ||
Comment on lines
+1176
to
+1177
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. My main concern here is isolation as we're modifying the example. We might need to clone the object or otherwise pass through a special flag to |
||
elif status_code not in example.status_codes: | ||
warn( | ||
f'example in response with status code {status_code} had' | ||
f'status_codes set to {example.status_codes!r}' | ||
) | ||
Comment on lines
+1178
to
+1182
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I didn't test this, but it struck me that if we're allowing Again, maybe this check should be pushed down into This check would be especially handy for the following (untested, hypothetical) situation: unauthenticated_example = OpenApiExample(
"UnauthenticatedExample",
value={"error": "You are not authenticated."},
status_codes=[401, 403],
)
@extend_schema(
...,
responses={
200: OpenApiResponse(description="...", examples=[unauthenticated_example]), # Should raise warning...
401: OpenApiResponse(description="...", examples=[unauthenticated_example]),
403: OpenApiResponse(description="...", examples=[unauthenticated_example]),
)
@api_view(["POST"])
def view(request):
... |
||
else: | ||
description, examples = '', [] | ||
|
||
|
@@ -1207,7 +1217,7 @@ def _get_response_for_code(self, serializer, status_code, media_types=None): | |
if ( | ||
self._is_list_view(serializer) | ||
and get_override(serializer, 'many') is not False | ||
and ('200' <= status_code < '300' or spectacular_settings.ENABLE_LIST_MECHANICS_ON_NON_2XX) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I mean, this worked, but it's sort of a fluke. 😄 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this actually works quite well given that |
||
and (is_success(status_code) or spectacular_settings.ENABLE_LIST_MECHANICS_ON_NON_2XX) | ||
): | ||
schema = build_array_type(schema) | ||
paginator = self._get_paginator() | ||
|
@@ -1244,7 +1254,7 @@ def _get_response_for_code(self, serializer, status_code, media_types=None): | |
'description': description | ||
} | ||
|
||
def _get_response_headers_for_code(self, status_code) -> dict: | ||
def _get_response_headers_for_code(self, status_code: int) -> dict: | ||
result = {} | ||
for parameter in self.get_override_parameters(): | ||
if not isinstance(parameter, OpenApiParameter): | ||
|
@@ -1253,7 +1263,7 @@ def _get_response_headers_for_code(self, status_code) -> dict: | |
continue | ||
if ( | ||
isinstance(parameter.response, list) | ||
and status_code not in [str(code) for code in parameter.response] | ||
and status_code not in parameter.response | ||
): | ||
continue | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,6 @@ | ||
import inspect | ||
import sys | ||
from http import HTTPStatus | ||
from typing import Any, Callable, Dict, List, Optional, Tuple, Type, TypeVar, Union | ||
|
||
from rest_framework.fields import Field, empty | ||
|
@@ -113,7 +114,7 @@ def __init__( | |
response_only: bool = False, | ||
parameter_only: Optional[Tuple[str, _ParameterLocationType]] = None, | ||
media_type: str = 'application/json', | ||
status_codes: Optional[List[str]] = None, | ||
status_codes: Optional[List[Union[HTTPStatus, int, str]]] = None, | ||
): | ||
self.name = name | ||
self.summary = summary | ||
|
@@ -124,7 +125,7 @@ def __init__( | |
self.response_only = response_only | ||
self.parameter_only = parameter_only | ||
self.media_type = media_type | ||
self.status_codes = status_codes or ['200', '201'] | ||
self.status_codes = list(map(int, status_codes)) if status_codes else None | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Forcing |
||
|
||
|
||
class OpenApiParameter(OpenApiSchemaBase): | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We both missed that this didn't make sense for requests! 🤦🏻