From a7a362813b63894a6243180dd8c3374ffc8330ea Mon Sep 17 00:00:00 2001 From: Rotzbua Date: Mon, 6 Apr 2020 15:54:30 +0200 Subject: [PATCH 1/9] Update optional dependencies list. (#7243) Co-authored-by: Ryan P Kilby --- docs/index.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/index.md b/docs/index.md index bccc1fb46..899118896 100644 --- a/docs/index.md +++ b/docs/index.md @@ -93,7 +93,7 @@ each Python and Django series. The following packages are optional: -* [coreapi][coreapi] (1.32.0+) - Schema generation support. +* [PyYAML][pyyaml], [uritemplate][uriteemplate] (5.1+, 3.0.0+) - Schema generation support. * [Markdown][markdown] (3.0.0+) - Markdown support for the browsable API. * [Pygments][pygments] (2.4.0+) - Add syntax highlighting to Markdown processing. * [django-filter][django-filter] (1.0.1+) - Filtering support. @@ -237,7 +237,8 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. [redhat]: https://www.redhat.com/ [heroku]: https://www.heroku.com/ [eventbrite]: https://www.eventbrite.co.uk/about/ -[coreapi]: https://pypi.org/project/coreapi/ +[pyyaml]: https://pypi.org/project/PyYAML/ +[uriteemplate]: https://pypi.org/project/uritemplate/ [markdown]: https://pypi.org/project/Markdown/ [pygments]: https://pypi.org/project/Pygments/ [django-filter]: https://pypi.org/project/django-filter/ From 0c8eb91737513029a7a12e719d7de8d68c8181f3 Mon Sep 17 00:00:00 2001 From: tsurutan Date: Tue, 7 Apr 2020 00:09:23 +0900 Subject: [PATCH 2/9] Fixed docs' custom render example. (#7171) --- docs/api-guide/renderers.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/api-guide/renderers.md b/docs/api-guide/renderers.md index a3321e860..a508a9ff9 100644 --- a/docs/api-guide/renderers.md +++ b/docs/api-guide/renderers.md @@ -273,7 +273,7 @@ By default this will include the following keys: `view`, `request`, `response`, The following is an example plaintext renderer that will return a response with the `data` parameter as the content of the response. - from django.utils.encoding import smart_unicode + from django.utils.encoding import smart_text from rest_framework import renderers @@ -282,7 +282,7 @@ The following is an example plaintext renderer that will return a response with format = 'txt' def render(self, data, media_type=None, renderer_context=None): - return data.encode(self.charset) + return smart_text(data, encoding=self.charset) ## Setting the character set From e6c1afbcf97e7080c0632ac9e2d60a6d10bd1a5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Freitag?= Date: Tue, 7 Apr 2020 10:28:09 +0000 Subject: [PATCH 3/9] Tighten checks for invalid field name in ordering (#7259) Django master removed the ORDER_PATTERN regex with commit https://github.com/django/django/commit/513948735b799239f3ef8c89397592445e1a0cd5 --- rest_framework/filters.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/rest_framework/filters.py b/rest_framework/filters.py index c15723ec3..8ef01743c 100644 --- a/rest_framework/filters.py +++ b/rest_framework/filters.py @@ -8,7 +8,6 @@ from functools import reduce from django.core.exceptions import ImproperlyConfigured from django.db import models from django.db.models.constants import LOOKUP_SEP -from django.db.models.sql.constants import ORDER_PATTERN from django.template import loader from django.utils.encoding import force_str from django.utils.translation import gettext_lazy as _ @@ -256,7 +255,13 @@ class OrderingFilter(BaseFilterBackend): def remove_invalid_fields(self, queryset, fields, view, request): valid_fields = [item[0] for item in self.get_valid_fields(queryset, view, {'request': request})] - return [term for term in fields if term.lstrip('-') in valid_fields and ORDER_PATTERN.match(term)] + + def term_valid(term): + if term.startswith("-"): + term = term[1:] + return term in valid_fields + + return [term for term in fields if term_valid(term)] def filter_queryset(self, request, queryset, view): ordering = self.get_ordering(request, queryset, view) From 41f27c3b43c16dd0dcc4cba387ec4450ac6ec10c Mon Sep 17 00:00:00 2001 From: Dhaval Mehta <20968146+dhaval-mehta@users.noreply.github.com> Date: Thu, 9 Apr 2020 22:40:50 +0530 Subject: [PATCH 4/9] Schemas: Don't generate component for DELETE method. (#7229) --- rest_framework/schemas/openapi.py | 4 ++++ tests/schemas/test_openapi.py | 12 ++++++++++++ 2 files changed, 16 insertions(+) diff --git a/rest_framework/schemas/openapi.py b/rest_framework/schemas/openapi.py index 1d0ec35d5..088f792c4 100644 --- a/rest_framework/schemas/openapi.py +++ b/rest_framework/schemas/openapi.py @@ -185,6 +185,10 @@ class AutoSchema(ViewInspector): """ Return components with their properties from the serializer. """ + + if method.lower() == 'delete': + return {} + serializer = self._get_serializer(path, method) if not isinstance(serializer, serializers.Serializer): diff --git a/tests/schemas/test_openapi.py b/tests/schemas/test_openapi.py index c9f6d967e..43101635d 100644 --- a/tests/schemas/test_openapi.py +++ b/tests/schemas/test_openapi.py @@ -1087,3 +1087,15 @@ class TestGenerator(TestCase): assert 'components' in schema assert 'schemas' in schema['components'] assert 'Duplicate' in schema['components']['schemas'] + + def test_component_should_not_be_generated_for_delete_method(self): + class ExampleView(generics.DestroyAPIView): + schema = AutoSchema(operation_id_base='example') + + url_patterns = [ + url(r'^example/?$', ExampleView.as_view()), + ] + generator = SchemaGenerator(patterns=url_patterns) + schema = generator.get_schema(request=create_request('/')) + assert 'components' not in schema + assert 'content' not in schema['paths']['/example/']['delete']['responses']['204'] From 603aac7db10671dea1975ea3023b5488815aa1ca Mon Sep 17 00:00:00 2001 From: Clinton Blackburn Date: Thu, 9 Apr 2020 10:16:17 -0700 Subject: [PATCH 5/9] Corrected OpenAPI schema type for DecimalField (#7254) --- rest_framework/schemas/openapi.py | 20 ++++++++++++++------ tests/schemas/test_openapi.py | 10 ++++++++++ tests/schemas/views.py | 8 ++++++-- 3 files changed, 30 insertions(+), 8 deletions(-) diff --git a/rest_framework/schemas/openapi.py b/rest_framework/schemas/openapi.py index 088f792c4..7af013444 100644 --- a/rest_framework/schemas/openapi.py +++ b/rest_framework/schemas/openapi.py @@ -15,6 +15,7 @@ from django.utils.encoding import force_str from rest_framework import exceptions, renderers, serializers from rest_framework.compat import uritemplate from rest_framework.fields import _UnvalidatedField, empty +from rest_framework.settings import api_settings from .generators import BaseSchemaGenerator from .inspectors import ViewInspector @@ -446,11 +447,17 @@ class AutoSchema(ViewInspector): content['format'] = field.protocol return content - # DecimalField has multipleOf based on decimal_places if isinstance(field, serializers.DecimalField): - content = { - 'type': 'number' - } + if getattr(field, 'coerce_to_string', api_settings.COERCE_DECIMAL_TO_STRING): + content = { + 'type': 'string', + 'format': 'decimal', + } + else: + content = { + 'type': 'number' + } + if field.decimal_places: content['multipleOf'] = float('.' + (field.decimal_places - 1) * '0' + '1') if field.max_whole_digits: @@ -461,7 +468,7 @@ class AutoSchema(ViewInspector): if isinstance(field, serializers.FloatField): content = { - 'type': 'number' + 'type': 'number', } self._map_min_max(field, content) return content @@ -560,7 +567,8 @@ class AutoSchema(ViewInspector): schema['maximum'] = v.limit_value elif isinstance(v, MinValueValidator): schema['minimum'] = v.limit_value - elif isinstance(v, DecimalValidator): + elif isinstance(v, DecimalValidator) and \ + not getattr(field, 'coerce_to_string', api_settings.COERCE_DECIMAL_TO_STRING): if v.decimal_places: schema['multipleOf'] = float('.' + (v.decimal_places - 1) * '0' + '1') if v.max_digits: diff --git a/tests/schemas/test_openapi.py b/tests/schemas/test_openapi.py index 43101635d..774636972 100644 --- a/tests/schemas/test_openapi.py +++ b/tests/schemas/test_openapi.py @@ -838,6 +838,16 @@ class TestOperationIntrospection(TestCase): assert properties['decimal2']['type'] == 'number' assert properties['decimal2']['multipleOf'] == .0001 + assert properties['decimal3'] == { + 'type': 'string', 'format': 'decimal', 'maximum': 1000000, 'minimum': -1000000, 'multipleOf': 0.01 + } + assert properties['decimal4'] == { + 'type': 'string', 'format': 'decimal', 'maximum': 1000000, 'minimum': -1000000, 'multipleOf': 0.01 + } + assert properties['decimal5'] == { + 'type': 'string', 'format': 'decimal', 'maximum': 10000, 'minimum': -10000, 'multipleOf': 0.01 + } + assert properties['email']['type'] == 'string' assert properties['email']['format'] == 'email' assert properties['email']['default'] == 'foo@bar.com' diff --git a/tests/schemas/views.py b/tests/schemas/views.py index 5645f59bf..18b3beae4 100644 --- a/tests/schemas/views.py +++ b/tests/schemas/views.py @@ -119,9 +119,13 @@ class ExampleValidatedSerializer(serializers.Serializer): MinLengthValidator(limit_value=2), ) ) - decimal1 = serializers.DecimalField(max_digits=6, decimal_places=2) - decimal2 = serializers.DecimalField(max_digits=5, decimal_places=0, + decimal1 = serializers.DecimalField(max_digits=6, decimal_places=2, coerce_to_string=False) + decimal2 = serializers.DecimalField(max_digits=5, decimal_places=0, coerce_to_string=False, validators=(DecimalValidator(max_digits=17, decimal_places=4),)) + decimal3 = serializers.DecimalField(max_digits=8, decimal_places=2, coerce_to_string=True) + decimal4 = serializers.DecimalField(max_digits=8, decimal_places=2, coerce_to_string=True, + validators=(DecimalValidator(max_digits=17, decimal_places=4),)) + decimal5 = serializers.DecimalField(max_digits=6, decimal_places=2) email = serializers.EmailField(default='foo@bar.com') url = serializers.URLField(default='http://www.example.com', allow_null=True) uuid = serializers.UUIDField() From 1872bde4625987202d23029d1f492253cbd43b58 Mon Sep 17 00:00:00 2001 From: Dhaval Mehta <20968146+dhaval-mehta@users.noreply.github.com> Date: Thu, 9 Apr 2020 22:48:00 +0530 Subject: [PATCH 6/9] Schemas: Improved decimal handling when mapping ChoiceField. (#7264) --- rest_framework/schemas/openapi.py | 35 +++++++++++++++---------------- tests/schemas/test_openapi.py | 13 ++++++++++++ 2 files changed, 30 insertions(+), 18 deletions(-) diff --git a/rest_framework/schemas/openapi.py b/rest_framework/schemas/openapi.py index 7af013444..8c948048e 100644 --- a/rest_framework/schemas/openapi.py +++ b/rest_framework/schemas/openapi.py @@ -330,30 +330,29 @@ class AutoSchema(ViewInspector): def _map_choicefield(self, field): choices = list(OrderedDict.fromkeys(field.choices)) # preserve order and remove duplicates - if all(isinstance(choice, bool) for choice in choices): - type = 'boolean' - elif all(isinstance(choice, int) for choice in choices): - type = 'integer' - elif all(isinstance(choice, (int, float, Decimal)) for choice in choices): # `number` includes `integer` - # Ref: https://tools.ietf.org/html/draft-wright-json-schema-validation-00#section-5.21 - type = 'number' - elif all(isinstance(choice, str) for choice in choices): - type = 'string' - else: - type = None - mapping = { # The value of `enum` keyword MUST be an array and SHOULD be unique. # Ref: https://tools.ietf.org/html/draft-wright-json-schema-validation-00#section-5.20 'enum': choices } - # If We figured out `type` then and only then we should set it. It must be a string. - # Ref: https://swagger.io/docs/specification/data-models/data-types/#mixed-type - # It is optional but it can not be null. - # Ref: https://tools.ietf.org/html/draft-wright-json-schema-validation-00#section-5.21 - if type: - mapping['type'] = type + if all(isinstance(choice, bool) for choice in choices): + mapping['type'] = 'boolean' + elif all(isinstance(choice, int) for choice in choices): + mapping['type'] = 'integer' + elif all(isinstance(choice, Decimal) for choice in choices): + mapping['format'] = 'decimal' + if api_settings.COERCE_DECIMAL_TO_STRING: + mapping['enum'] = [str(choice) for choice in mapping['enum']] + mapping['type'] = 'string' + else: + mapping['type'] = 'number' + elif all(isinstance(choice, (int, float, Decimal)) for choice in choices): # `number` includes `integer` + # Ref: https://tools.ietf.org/html/draft-wright-json-schema-validation-00#section-5.21 + mapping['type'] = 'number' + elif all(isinstance(choice, str) for choice in choices): + mapping['type'] = 'string' + return mapping def _map_field(self, field): diff --git a/tests/schemas/test_openapi.py b/tests/schemas/test_openapi.py index 774636972..1eef8fa2f 100644 --- a/tests/schemas/test_openapi.py +++ b/tests/schemas/test_openapi.py @@ -1,5 +1,6 @@ import uuid import warnings +from decimal import Decimal import pytest from django.conf.urls import url @@ -78,6 +79,9 @@ class TestFieldMapping(TestCase): (1, 'One'), (2, 'Two'), (3, 'Three'), (2, 'Two'), (3, 'Three'), (1, 'One'), ])), {'items': {'enum': [1, 2, 3], 'type': 'integer'}, 'type': 'array'}), + (serializers.ListField(child= + serializers.ChoiceField(choices=[(Decimal('1.111'), 'one'), (Decimal('2.222'), 'two')])), + {'items': {'enum': ['1.111', '2.222'], 'type': 'string', 'format': 'decimal'}, 'type': 'array'}), (serializers.IntegerField(min_value=2147483648), {'type': 'integer', 'minimum': 2147483648, 'format': 'int64'}), ] @@ -85,6 +89,15 @@ class TestFieldMapping(TestCase): with self.subTest(field=field): assert inspector._map_field(field) == mapping + @override_settings(REST_FRAMEWORK={'COERCE_DECIMAL_TO_STRING': False}) + def test_decimal_schema_for_choice_field(self): + inspector = AutoSchema() + field = serializers.ListField( + child=serializers.ChoiceField(choices=[(Decimal('1.111'), 'one'), (Decimal('2.222'), 'two')])) + mapping = {'items': {'enum': [Decimal('1.111'), Decimal('2.222')], 'type': 'number'}, 'type': 'array'} + assert inspector._map_field(field) == mapping + + def test_lazy_string_field(self): class ItemSerializer(serializers.Serializer): text = serializers.CharField(help_text=_('lazy string')) From b1bfff4f1c2ca3ad2ad844931747afec135b3fb8 Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Thu, 9 Apr 2020 19:35:46 +0200 Subject: [PATCH 7/9] Revert "Schemas: Improved decimal handling when mapping ChoiceField. (#7264)" This reverts commit 1872bde4625987202d23029d1f492253cbd43b58. --- rest_framework/schemas/openapi.py | 35 ++++++++++++++++--------------- tests/schemas/test_openapi.py | 13 ------------ 2 files changed, 18 insertions(+), 30 deletions(-) diff --git a/rest_framework/schemas/openapi.py b/rest_framework/schemas/openapi.py index 8c948048e..7af013444 100644 --- a/rest_framework/schemas/openapi.py +++ b/rest_framework/schemas/openapi.py @@ -330,29 +330,30 @@ class AutoSchema(ViewInspector): def _map_choicefield(self, field): choices = list(OrderedDict.fromkeys(field.choices)) # preserve order and remove duplicates + if all(isinstance(choice, bool) for choice in choices): + type = 'boolean' + elif all(isinstance(choice, int) for choice in choices): + type = 'integer' + elif all(isinstance(choice, (int, float, Decimal)) for choice in choices): # `number` includes `integer` + # Ref: https://tools.ietf.org/html/draft-wright-json-schema-validation-00#section-5.21 + type = 'number' + elif all(isinstance(choice, str) for choice in choices): + type = 'string' + else: + type = None + mapping = { # The value of `enum` keyword MUST be an array and SHOULD be unique. # Ref: https://tools.ietf.org/html/draft-wright-json-schema-validation-00#section-5.20 'enum': choices } - if all(isinstance(choice, bool) for choice in choices): - mapping['type'] = 'boolean' - elif all(isinstance(choice, int) for choice in choices): - mapping['type'] = 'integer' - elif all(isinstance(choice, Decimal) for choice in choices): - mapping['format'] = 'decimal' - if api_settings.COERCE_DECIMAL_TO_STRING: - mapping['enum'] = [str(choice) for choice in mapping['enum']] - mapping['type'] = 'string' - else: - mapping['type'] = 'number' - elif all(isinstance(choice, (int, float, Decimal)) for choice in choices): # `number` includes `integer` - # Ref: https://tools.ietf.org/html/draft-wright-json-schema-validation-00#section-5.21 - mapping['type'] = 'number' - elif all(isinstance(choice, str) for choice in choices): - mapping['type'] = 'string' - + # If We figured out `type` then and only then we should set it. It must be a string. + # Ref: https://swagger.io/docs/specification/data-models/data-types/#mixed-type + # It is optional but it can not be null. + # Ref: https://tools.ietf.org/html/draft-wright-json-schema-validation-00#section-5.21 + if type: + mapping['type'] = type return mapping def _map_field(self, field): diff --git a/tests/schemas/test_openapi.py b/tests/schemas/test_openapi.py index 1eef8fa2f..774636972 100644 --- a/tests/schemas/test_openapi.py +++ b/tests/schemas/test_openapi.py @@ -1,6 +1,5 @@ import uuid import warnings -from decimal import Decimal import pytest from django.conf.urls import url @@ -79,9 +78,6 @@ class TestFieldMapping(TestCase): (1, 'One'), (2, 'Two'), (3, 'Three'), (2, 'Two'), (3, 'Three'), (1, 'One'), ])), {'items': {'enum': [1, 2, 3], 'type': 'integer'}, 'type': 'array'}), - (serializers.ListField(child= - serializers.ChoiceField(choices=[(Decimal('1.111'), 'one'), (Decimal('2.222'), 'two')])), - {'items': {'enum': ['1.111', '2.222'], 'type': 'string', 'format': 'decimal'}, 'type': 'array'}), (serializers.IntegerField(min_value=2147483648), {'type': 'integer', 'minimum': 2147483648, 'format': 'int64'}), ] @@ -89,15 +85,6 @@ class TestFieldMapping(TestCase): with self.subTest(field=field): assert inspector._map_field(field) == mapping - @override_settings(REST_FRAMEWORK={'COERCE_DECIMAL_TO_STRING': False}) - def test_decimal_schema_for_choice_field(self): - inspector = AutoSchema() - field = serializers.ListField( - child=serializers.ChoiceField(choices=[(Decimal('1.111'), 'one'), (Decimal('2.222'), 'two')])) - mapping = {'items': {'enum': [Decimal('1.111'), Decimal('2.222')], 'type': 'number'}, 'type': 'array'} - assert inspector._map_field(field) == mapping - - def test_lazy_string_field(self): class ItemSerializer(serializers.Serializer): text = serializers.CharField(help_text=_('lazy string')) From d45e0005f32bc246a49b209836d233f3d23d77b0 Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Mon, 6 Apr 2020 16:30:49 +0200 Subject: [PATCH 8/9] Updated deprecation warnings for 3.12 --- rest_framework/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rest_framework/__init__.py b/rest_framework/__init__.py index b6f3f65ce..8f2bc4466 100644 --- a/rest_framework/__init__.py +++ b/rest_framework/__init__.py @@ -25,9 +25,9 @@ ISO_8601 = 'iso-8601' default_app_config = 'rest_framework.apps.RestFrameworkConfig' -class RemovedInDRF312Warning(DeprecationWarning): +class RemovedInDRF313Warning(DeprecationWarning): pass -class RemovedInDRF313Warning(PendingDeprecationWarning): +class RemovedInDRF314Warning(PendingDeprecationWarning): pass From b2497fc2456c607a3c639ed2355c28dac672a70f Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Mon, 6 Apr 2020 17:03:10 +0200 Subject: [PATCH 9/9] Convert openapi.AutoSchema methods to public API. --- rest_framework/schemas/openapi.py | 169 +++++++++++++++++++++++------- tests/schemas/test_openapi.py | 30 +++--- 2 files changed, 148 insertions(+), 51 deletions(-) diff --git a/rest_framework/schemas/openapi.py b/rest_framework/schemas/openapi.py index 7af013444..9b3082822 100644 --- a/rest_framework/schemas/openapi.py +++ b/rest_framework/schemas/openapi.py @@ -12,7 +12,9 @@ from django.core.validators import ( from django.db import models from django.utils.encoding import force_str -from rest_framework import exceptions, renderers, serializers +from rest_framework import ( + RemovedInDRF314Warning, exceptions, renderers, serializers +) from rest_framework.compat import uritemplate from rest_framework.fields import _UnvalidatedField, empty from rest_framework.settings import api_settings @@ -146,15 +148,15 @@ class AutoSchema(ViewInspector): operation['description'] = self.get_description(path, method) parameters = [] - parameters += self._get_path_parameters(path, method) - parameters += self._get_pagination_parameters(path, method) - parameters += self._get_filter_parameters(path, method) + parameters += self.get_path_parameters(path, method) + parameters += self.get_pagination_parameters(path, method) + parameters += self.get_filter_parameters(path, method) operation['parameters'] = parameters - request_body = self._get_request_body(path, method) + request_body = self.get_request_body(path, method) if request_body: operation['requestBody'] = request_body - operation['responses'] = self._get_responses(path, method) + operation['responses'] = self.get_responses(path, method) operation['tags'] = self.get_tags(path, method) return operation @@ -190,14 +192,14 @@ class AutoSchema(ViewInspector): if method.lower() == 'delete': return {} - serializer = self._get_serializer(path, method) + serializer = self.get_serializer(path, method) if not isinstance(serializer, serializers.Serializer): return {} component_name = self.get_component_name(serializer) - content = self._map_serializer(serializer) + content = self.map_serializer(serializer) return {component_name: content} def _to_camel_case(self, snake_str): @@ -220,8 +222,8 @@ class AutoSchema(ViewInspector): name = model.__name__ # Try with the serializer class name - elif self._get_serializer(path, method) is not None: - name = self._get_serializer(path, method).__class__.__name__ + elif self.get_serializer(path, method) is not None: + name = self.get_serializer(path, method).__class__.__name__ if name.endswith('Serializer'): name = name[:-10] @@ -259,7 +261,7 @@ class AutoSchema(ViewInspector): return action + name - def _get_path_parameters(self, path, method): + def get_path_parameters(self, path, method): """ Return a list of parameters from templated path variables. """ @@ -295,15 +297,15 @@ class AutoSchema(ViewInspector): return parameters - def _get_filter_parameters(self, path, method): - if not self._allows_filters(path, method): + def get_filter_parameters(self, path, method): + if not self.allows_filters(path, method): return [] parameters = [] for filter_backend in self.view.filter_backends: parameters += filter_backend().get_schema_operation_parameters(self.view) return parameters - def _allows_filters(self, path, method): + def allows_filters(self, path, method): """ Determine whether to include filter Fields in schema. @@ -316,19 +318,19 @@ class AutoSchema(ViewInspector): return self.view.action in ["list", "retrieve", "update", "partial_update", "destroy"] return method.lower() in ["get", "put", "patch", "delete"] - def _get_pagination_parameters(self, path, method): + def get_pagination_parameters(self, path, method): view = self.view if not is_list_view(path, method, view): return [] - paginator = self._get_paginator() + paginator = self.get_paginator() if not paginator: return [] return paginator.get_schema_operation_parameters(view) - def _map_choicefield(self, field): + def map_choicefield(self, field): choices = list(OrderedDict.fromkeys(field.choices)) # preserve order and remove duplicates if all(isinstance(choice, bool) for choice in choices): type = 'boolean' @@ -356,16 +358,16 @@ class AutoSchema(ViewInspector): mapping['type'] = type return mapping - def _map_field(self, field): + def map_field(self, field): # Nested Serializers, `many` or not. if isinstance(field, serializers.ListSerializer): return { 'type': 'array', - 'items': self._map_serializer(field.child) + 'items': self.map_serializer(field.child) } if isinstance(field, serializers.Serializer): - data = self._map_serializer(field) + data = self.map_serializer(field) data['type'] = 'object' return data @@ -373,7 +375,7 @@ class AutoSchema(ViewInspector): if isinstance(field, serializers.ManyRelatedField): return { 'type': 'array', - 'items': self._map_field(field.child_relation) + 'items': self.map_field(field.child_relation) } if isinstance(field, serializers.PrimaryKeyRelatedField): model = getattr(field.queryset, 'model', None) @@ -389,11 +391,11 @@ class AutoSchema(ViewInspector): if isinstance(field, serializers.MultipleChoiceField): return { 'type': 'array', - 'items': self._map_choicefield(field) + 'items': self.map_choicefield(field) } if isinstance(field, serializers.ChoiceField): - return self._map_choicefield(field) + return self.map_choicefield(field) # ListField. if isinstance(field, serializers.ListField): @@ -402,7 +404,7 @@ class AutoSchema(ViewInspector): 'items': {}, } if not isinstance(field.child, _UnvalidatedField): - mapping['items'] = self._map_field(field.child) + mapping['items'] = self.map_field(field.child) return mapping # DateField and DateTimeField type is string @@ -504,7 +506,7 @@ class AutoSchema(ViewInspector): if field.min_value: content['minimum'] = field.min_value - def _map_serializer(self, serializer): + def map_serializer(self, serializer): # Assuming we have a valid serializer instance. required = [] properties = {} @@ -516,7 +518,7 @@ class AutoSchema(ViewInspector): if field.required: required.append(field.field_name) - schema = self._map_field(field) + schema = self.map_field(field) if field.read_only: schema['readOnly'] = True if field.write_only: @@ -527,7 +529,7 @@ class AutoSchema(ViewInspector): schema['default'] = field.default if field.help_text: schema['description'] = str(field.help_text) - self._map_field_validators(field, schema) + self.map_field_validators(field, schema) properties[field.field_name] = schema @@ -540,7 +542,7 @@ class AutoSchema(ViewInspector): return result - def _map_field_validators(self, field, schema): + def map_field_validators(self, field, schema): """ map field validators """ @@ -578,7 +580,7 @@ class AutoSchema(ViewInspector): schema['maximum'] = int(digits * '9') + 1 schema['minimum'] = -schema['maximum'] - def _get_paginator(self): + def get_paginator(self): pagination_class = getattr(self.view, 'pagination_class', None) if pagination_class: return pagination_class() @@ -596,7 +598,7 @@ class AutoSchema(ViewInspector): media_types.append(renderer.media_type) return media_types - def _get_serializer(self, path, method): + def get_serializer(self, path, method): view = self.view if not hasattr(view, 'get_serializer'): @@ -614,13 +616,13 @@ class AutoSchema(ViewInspector): def _get_reference(self, serializer): return {'$ref': '#/components/schemas/{}'.format(self.get_component_name(serializer))} - def _get_request_body(self, path, method): + def get_request_body(self, path, method): if method not in ('PUT', 'PATCH', 'POST'): return {} self.request_media_types = self.map_parsers(path, method) - serializer = self._get_serializer(path, method) + serializer = self.get_serializer(path, method) if not isinstance(serializer, serializers.Serializer): item_schema = {} @@ -634,8 +636,7 @@ class AutoSchema(ViewInspector): } } - def _get_responses(self, path, method): - # TODO: Handle multiple codes and pagination classes. + def get_responses(self, path, method): if method == 'DELETE': return { '204': { @@ -645,7 +646,7 @@ class AutoSchema(ViewInspector): self.response_media_types = self.map_renderers(path, method) - serializer = self._get_serializer(path, method) + serializer = self.get_serializer(path, method) if not isinstance(serializer, serializers.Serializer): item_schema = {} @@ -657,7 +658,7 @@ class AutoSchema(ViewInspector): 'type': 'array', 'items': item_schema, } - paginator = self._get_paginator() + paginator = self.get_paginator() if paginator: response_schema = paginator.get_paginated_response_schema(response_schema) else: @@ -688,3 +689,99 @@ class AutoSchema(ViewInspector): path = path[1:] return [path.split('/')[0].replace('_', '-')] + + def _get_path_parameters(self, path, method): + warnings.warn( + "Method `_get_path_parameters()` has been renamed to `get_path_parameters()`. " + "The old name will be removed in DRF v3.14.", + RemovedInDRF314Warning, stacklevel=2 + ) + return self.get_path_parameters(path, method) + + def _get_filter_parameters(self, path, method): + warnings.warn( + "Method `_get_filter_parameters()` has been renamed to `get_filter_parameters()`. " + "The old name will be removed in DRF v3.14.", + RemovedInDRF314Warning, stacklevel=2 + ) + return self.get_filter_parameters(path, method) + + def _get_responses(self, path, method): + warnings.warn( + "Method `_get_responses()` has been renamed to `get_responses()`. " + "The old name will be removed in DRF v3.14.", + RemovedInDRF314Warning, stacklevel=2 + ) + return self.get_responses(path, method) + + def _get_request_body(self, path, method): + warnings.warn( + "Method `_get_request_body()` has been renamed to `get_request_body()`. " + "The old name will be removed in DRF v3.14.", + RemovedInDRF314Warning, stacklevel=2 + ) + return self.get_request_body(path, method) + + def _get_serializer(self, path, method): + warnings.warn( + "Method `_get_serializer()` has been renamed to `get_serializer()`. " + "The old name will be removed in DRF v3.14.", + RemovedInDRF314Warning, stacklevel=2 + ) + return self.get_serializer(path, method) + + def _get_paginator(self): + warnings.warn( + "Method `_get_paginator()` has been renamed to `get_paginator()`. " + "The old name will be removed in DRF v3.14.", + RemovedInDRF314Warning, stacklevel=2 + ) + return self.get_paginator() + + def _map_field_validators(self, field, schema): + warnings.warn( + "Method `_map_field_validators()` has been renamed to `map_field_validators()`. " + "The old name will be removed in DRF v3.14.", + RemovedInDRF314Warning, stacklevel=2 + ) + return self.map_field_validators(field, schema) + + def _map_serializer(self, serializer): + warnings.warn( + "Method `_map_serializer()` has been renamed to `map_serializer()`. " + "The old name will be removed in DRF v3.14.", + RemovedInDRF314Warning, stacklevel=2 + ) + return self.map_serializer(serializer) + + def _map_field(self, field): + warnings.warn( + "Method `_map_field()` has been renamed to `map_field()`. " + "The old name will be removed in DRF v3.14.", + RemovedInDRF314Warning, stacklevel=2 + ) + return self.map_field(field) + + def _map_choicefield(self, field): + warnings.warn( + "Method `_map_choicefield()` has been renamed to `map_choicefield()`. " + "The old name will be removed in DRF v3.14.", + RemovedInDRF314Warning, stacklevel=2 + ) + return self.map_choicefield(field) + + def _get_pagination_parameters(self, path, method): + warnings.warn( + "Method `_get_pagination_parameters()` has been renamed to `get_pagination_parameters()`. " + "The old name will be removed in DRF v3.14.", + RemovedInDRF314Warning, stacklevel=2 + ) + return self.get_pagination_parameters(path, method) + + def _allows_filters(self, path, method): + warnings.warn( + "Method `_allows_filters()` has been renamed to `allows_filters()`. " + "The old name will be removed in DRF v3.14.", + RemovedInDRF314Warning, stacklevel=2 + ) + return self.allows_filters(path, method) diff --git a/tests/schemas/test_openapi.py b/tests/schemas/test_openapi.py index 774636972..0e86a7f50 100644 --- a/tests/schemas/test_openapi.py +++ b/tests/schemas/test_openapi.py @@ -83,7 +83,7 @@ class TestFieldMapping(TestCase): ] for field, mapping in cases: with self.subTest(field=field): - assert inspector._map_field(field) == mapping + assert inspector.map_field(field) == mapping def test_lazy_string_field(self): class ItemSerializer(serializers.Serializer): @@ -91,7 +91,7 @@ class TestFieldMapping(TestCase): inspector = AutoSchema() - data = inspector._map_serializer(ItemSerializer()) + data = inspector.map_serializer(ItemSerializer()) assert isinstance(data['properties']['text']['description'], str), "description must be str" def test_boolean_default_field(self): @@ -102,7 +102,7 @@ class TestFieldMapping(TestCase): inspector = AutoSchema() - data = inspector._map_serializer(Serializer()) + data = inspector.map_serializer(Serializer()) assert data['properties']['default_true']['default'] is True, "default must be true" assert data['properties']['default_false']['default'] is False, "default must be false" assert 'default' not in data['properties']['without_default'], "default must not be defined" @@ -202,7 +202,7 @@ class TestOperationIntrospection(TestCase): inspector = AutoSchema() inspector.view = view - request_body = inspector._get_request_body(path, method) + request_body = inspector.get_request_body(path, method) print(request_body) assert request_body['content']['application/json']['schema']['$ref'] == '#/components/schemas/Item' @@ -229,7 +229,7 @@ class TestOperationIntrospection(TestCase): inspector = AutoSchema() inspector.view = view - serializer = inspector._get_serializer(path, method) + serializer = inspector.get_serializer(path, method) with pytest.raises(Exception) as exc: inspector.get_component_name(serializer) @@ -259,7 +259,7 @@ class TestOperationIntrospection(TestCase): # there should be no empty 'required' property, see #6834 assert 'required' not in component - for response in inspector._get_responses(path, method).values(): + for response in inspector.get_responses(path, method).values(): assert 'required' not in component def test_empty_required_with_patch_method(self): @@ -285,7 +285,7 @@ class TestOperationIntrospection(TestCase): component = components['Item'] # there should be no empty 'required' property, see #6834 assert 'required' not in component - for response in inspector._get_responses(path, method).values(): + for response in inspector.get_responses(path, method).values(): assert 'required' not in component def test_response_body_generation(self): @@ -307,7 +307,7 @@ class TestOperationIntrospection(TestCase): inspector = AutoSchema() inspector.view = view - responses = inspector._get_responses(path, method) + responses = inspector.get_responses(path, method) assert responses['201']['content']['application/json']['schema']['$ref'] == '#/components/schemas/Item' components = inspector.get_components(path, method) @@ -337,7 +337,7 @@ class TestOperationIntrospection(TestCase): inspector = AutoSchema() inspector.view = view - responses = inspector._get_responses(path, method) + responses = inspector.get_responses(path, method) assert responses['201']['content']['application/json']['schema']['$ref'] == '#/components/schemas/Item' components = inspector.get_components(path, method) assert components['Item'] @@ -368,7 +368,7 @@ class TestOperationIntrospection(TestCase): inspector = AutoSchema() inspector.view = view - responses = inspector._get_responses(path, method) + responses = inspector.get_responses(path, method) assert responses == { '200': { 'description': '', @@ -424,7 +424,7 @@ class TestOperationIntrospection(TestCase): inspector = AutoSchema() inspector.view = view - responses = inspector._get_responses(path, method) + responses = inspector.get_responses(path, method) assert responses == { '200': { 'description': '', @@ -472,7 +472,7 @@ class TestOperationIntrospection(TestCase): inspector = AutoSchema() inspector.view = view - responses = inspector._get_responses(path, method) + responses = inspector.get_responses(path, method) assert responses == { '204': { 'description': '', @@ -496,7 +496,7 @@ class TestOperationIntrospection(TestCase): inspector = AutoSchema() inspector.view = view - request_body = inspector._get_request_body(path, method) + request_body = inspector.get_request_body(path, method) assert len(request_body['content'].keys()) == 2 assert 'multipart/form-data' in request_body['content'] @@ -519,7 +519,7 @@ class TestOperationIntrospection(TestCase): inspector = AutoSchema() inspector.view = view - responses = inspector._get_responses(path, method) + responses = inspector.get_responses(path, method) # TODO this should be changed once the multiple response # schema support is there success_response = responses['200'] @@ -594,7 +594,7 @@ class TestOperationIntrospection(TestCase): inspector = AutoSchema() inspector.view = view - responses = inspector._get_responses(path, method) + responses = inspector.get_responses(path, method) assert responses == { '200': { 'description': '',