-
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
Conversation
Codecov Report
@@ Coverage Diff @@
## master #573 +/- ##
==========================================
- Coverage 98.80% 98.78% -0.03%
==========================================
Files 58 58
Lines 6702 6724 +22
==========================================
+ Hits 6622 6642 +20
- Misses 80 82 +2
Continue to review full report at Codecov.
|
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.
@tfranzel I've left some thoughts on some of this for you.
@@ -150,8 +150,7 @@ Parameter Location | |||
- :py:data:`~drf_yasg.openapi.IN_QUERY` is called :py:attr:`~drf_spectacular.utils.OpenApiParameter.QUERY` | |||
- :py:data:`~drf_yasg.openapi.IN_HEADER` is called :py:attr:`~drf_spectacular.utils.OpenApiParameter.HEADER` | |||
- :py:data:`~drf_yasg.openapi.IN_BODY` and :py:data:`~drf_yasg.openapi.IN_FORM` have no direct equivalent. | |||
Instead you can use ``@extend_schema(request={"<media-type>": ...})`` or | |||
``@extend_schema(request={("<status-code>", "<media-type"): ...})``. |
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! 🤦🏻
@@ -1019,7 +1020,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 comment
The reason will be displayed to describe this comment to others. Learn more.
By postponing the default values here, we allow OpenApiExample
to be unshackled such that it can be defined globally and reused for different status codes in multiple operations.
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 comment
The reason will be displayed to describe this comment to others. Learn more.
Casting to int
allows for http.HTTPStatus
to work.
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.
unfortunately it would be a regression
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 comment
The reason will be displayed to describe this comment to others. Learn more.
This could be removed if we wanted to pass back int
here too, but that would require updating all of the tests which currently have things like ...['responses']['200']
to have ...['responses'][200]
. You could choose to do this - it's not difficult - but I didn't want to introduce too much noise for the purposes of this PR.
if example.status_codes is None: | ||
example.status_codes = [status_code] |
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.
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 _get_examples()
to say that this is in an OpenApiResponse
so we can assume that all is well if status_codes
is None
...
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}' | ||
) |
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.
I didn't test this, but it struck me that if we're allowing status_codes
to be None
, or they can be pre-defined as the same value as the status code of the response, they could also have the wrong value.
Again, maybe this check should be pushed down into _get_examples()
to be done when in this context of an OpenApiResponse
to highlight that there is something wrong instead of silently ignoring it.
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):
...
@@ -1193,7 +1203,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 comment
The 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 comment
The reason will be displayed to describe this comment to others. Learn more.
this actually works quite well given that 2XX
is also a valid value. unless you go totally crazy there, this always does what it is supposed to do.
@@ -123,7 +124,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 comment
The reason will be displayed to describe this comment to others. Learn more.
Forcing list[int] | None
early makes everything much easier downstream.
92bc3bb
to
4ab934c
Compare
Rebased to resolve conflicts. |
I added the (status-code, media-type) version as requested in this comment: tfranzel#527 (comment) But, of course, status codes do not apply to requests, only responses!
Currently status codes are allowed to be integers in some places, but not others. It seems more natural that a status code be represented as an integer than a string.
When passing an example in `examples` for `OpenApiResponse`, assume that the `status_codes` of the `OpenApiExample` should match that of the response and don't default to `[200, 201]`.
4ab934c
to
8f28e40
Compare
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.
Hey @ngnpope, sry I had to manage my time a bit, but finally got around looking at this.
I was a bit surprised that the wildcard status codes were not covered by the tests at all. Initially the code was dealing in ints until I found out that status codes can also literally be 2XX
. This breaks a lot of changes you made there. I added a regression test for this with 36b970e.
I do like some aspect of this like delayed defaults for examples ['200', '201']
. HTTPStatus
seems nice but users should likely prefer using DRF's native definitions with rest_framwork.status
, which should work out of the box already.
A friendly remark: I'm thankful for your awesome contributions! However, mixing together several issues/refactorings in one large PR makes it harder for me to quickly review it in between my other work. It might also mean more work for you, if I need to reject some parts of it.
One more thing. If you're thinking about doing a bigger change that might be controversial, why not create a 1-2 sentence issue first, so that I can provide you with some preliminary feedback. It may save you some time in the future.
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 comment
The reason will be displayed to describe this comment to others. Learn more.
unfortunately it would be a regression
@@ -1193,7 +1203,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 comment
The reason will be displayed to describe this comment to others. Learn more.
this actually works quite well given that 2XX
is also a valid value. unless you go totally crazy there, this always does what it is supposed to do.
I'd completely forgotten that wildcard status codes was even a thing. (Mentioned here in the specification.)
Yes. One of the main things I was wanting to avoid was the need to specify the status codes where
Sorry - comes from time pressures and changes depending on each other. Will endeavour to split things up in the future. |
@ngnpope since this was kind of a feature down the wrong path, I think we can close this. I'm cleaning up, but if there is something still to consider here let me know. |
I've bumped into a number of things related to status codes that I think could work better.
OpenApiExample
'sstatus_codes
list.OpenApiExample
requiresstatus_codes
to be defined to something to work with non-200/201 statuses inOpenApiResponse
which doesn't make sense and seems unnecessary if the example can only be for that defined response and its status code.These changes do the following:
rest_framework.status.*
constants) or enum (e.g.HTTPStatus
) to work in:responses
dict of@extend_schema
.status_codes
list ofOpenApiExample
.status_codes
list to be left undefined so that theexamples
inOpenApiResponse
assume they work for the status code for that response.This probably isn't perfect and you'll want to fix some bits I'm sure, but I thought there was enough here to be getting on with.