From 43f9bd2f458d1b8113237edca15c8e75c7448276 Mon Sep 17 00:00:00 2001 From: Rizwan Shaikh Date: Mon, 12 Jun 2023 23:56:40 +0530 Subject: [PATCH 01/11] fix OpenAPIRenderer for timedelta --- rest_framework/renderers.py | 2 ++ rest_framework/utils/encoders.py | 11 +++++++++++ 2 files changed, 13 insertions(+) diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index 8e8c3a9b3c..23add173db 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -9,6 +9,7 @@ import base64 import contextlib +import datetime from urllib import parse from django import forms @@ -1056,6 +1057,7 @@ class Dumper(yaml.Dumper): def ignore_aliases(self, data): return True Dumper.add_representer(SafeString, Dumper.represent_str) + Dumper.add_representer(datetime.timedelta, encoders.CustomScalar.represent_timedelta) return yaml.dump(data, default_flow_style=False, sort_keys=False, Dumper=Dumper).encode('utf-8') diff --git a/rest_framework/utils/encoders.py b/rest_framework/utils/encoders.py index 35a89eb090..aa45422861 100644 --- a/rest_framework/utils/encoders.py +++ b/rest_framework/utils/encoders.py @@ -65,3 +65,14 @@ def default(self, obj): elif hasattr(obj, '__iter__'): return tuple(item for item in obj) return super().default(obj) + + +class CustomScalar: + """ + CustomScalar that knows how to encode timedelta that renderer + can understand. + """ + @classmethod + def represent_timedelta(cls, dumper, data): + value = str(data.total_seconds()) + return dumper.represent_scalar('tag:yaml.org,2002:str', value) From 98ce5bf274e7d8913e6af396b3b842c6f6665451 Mon Sep 17 00:00:00 2001 From: Rizwan Shaikh Date: Mon, 12 Jun 2023 23:57:50 +0530 Subject: [PATCH 02/11] added test for rendering openapi with timedelta --- tests/schemas/test_openapi.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/schemas/test_openapi.py b/tests/schemas/test_openapi.py index 0ea6d1ff92..e94bb91906 100644 --- a/tests/schemas/test_openapi.py +++ b/tests/schemas/test_openapi.py @@ -1162,6 +1162,18 @@ def test_schema_rendering_to_json(self): assert b'"openapi": "' in ret assert b'"default": "0.0"' in ret + def test_schema_rendering_to_yaml(self): + patterns = [ + path('example/', views.ExampleGenericAPIView.as_view()), + ] + generator = SchemaGenerator(patterns=patterns) + + request = create_request('/') + schema = generator.get_schema(request=request) + ret = OpenAPIRenderer().render(schema) + assert b"openapi: " in ret + assert b"default: '0.0'" in ret + def test_schema_with_no_paths(self): patterns = [] generator = SchemaGenerator(patterns=patterns) From d1680051b1960c1e38c50d924922b5e81d59e7c4 Mon Sep 17 00:00:00 2001 From: Rizwan Shaikh Date: Mon, 12 Jun 2023 23:56:40 +0530 Subject: [PATCH 03/11] fix OpenAPIRenderer for timedelta --- rest_framework/renderers.py | 2 ++ rest_framework/utils/encoders.py | 11 +++++++++++ 2 files changed, 13 insertions(+) diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index 0a3b03729d..db1fdd128b 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -9,6 +9,7 @@ import base64 import contextlib +import datetime from urllib import parse from django import forms @@ -1062,6 +1063,7 @@ class Dumper(yaml.Dumper): def ignore_aliases(self, data): return True Dumper.add_representer(SafeString, Dumper.represent_str) + Dumper.add_representer(datetime.timedelta, encoders.CustomScalar.represent_timedelta) return yaml.dump(data, default_flow_style=False, sort_keys=False, Dumper=Dumper).encode('utf-8') diff --git a/rest_framework/utils/encoders.py b/rest_framework/utils/encoders.py index 35a89eb090..aa45422861 100644 --- a/rest_framework/utils/encoders.py +++ b/rest_framework/utils/encoders.py @@ -65,3 +65,14 @@ def default(self, obj): elif hasattr(obj, '__iter__'): return tuple(item for item in obj) return super().default(obj) + + +class CustomScalar: + """ + CustomScalar that knows how to encode timedelta that renderer + can understand. + """ + @classmethod + def represent_timedelta(cls, dumper, data): + value = str(data.total_seconds()) + return dumper.represent_scalar('tag:yaml.org,2002:str', value) From 297f37ea62d1f828fbf40066f7ee432aa100f088 Mon Sep 17 00:00:00 2001 From: Rizwan Shaikh Date: Mon, 12 Jun 2023 23:57:50 +0530 Subject: [PATCH 04/11] added test for rendering openapi with timedelta --- tests/schemas/test_openapi.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/schemas/test_openapi.py b/tests/schemas/test_openapi.py index 0ea6d1ff92..e94bb91906 100644 --- a/tests/schemas/test_openapi.py +++ b/tests/schemas/test_openapi.py @@ -1162,6 +1162,18 @@ def test_schema_rendering_to_json(self): assert b'"openapi": "' in ret assert b'"default": "0.0"' in ret + def test_schema_rendering_to_yaml(self): + patterns = [ + path('example/', views.ExampleGenericAPIView.as_view()), + ] + generator = SchemaGenerator(patterns=patterns) + + request = create_request('/') + schema = generator.get_schema(request=request) + ret = OpenAPIRenderer().render(schema) + assert b"openapi: " in ret + assert b"default: '0.0'" in ret + def test_schema_with_no_paths(self): patterns = [] generator = SchemaGenerator(patterns=patterns) From 833313496c8ebbdc3509d87895764c822bfc5dc1 Mon Sep 17 00:00:00 2001 From: Lenno Nagel Date: Tue, 13 Jun 2023 07:27:37 +0300 Subject: [PATCH 05/11] Removed usage of field.choices that triggered full table load (#8950) Removed the `{{ field.choices|yesno:",disabled" }}` block because this triggers the loading of full database table worth of objects just to determine whether the multi-select widget should be set as disabled or not. Since this "disabled" marking feature is not present in the normal select field, then I propose to remove it also from the multi-select. --- .../templates/rest_framework/horizontal/select_multiple.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/templates/rest_framework/horizontal/select_multiple.html b/rest_framework/templates/rest_framework/horizontal/select_multiple.html index 36ff9fd0dc..12e781cc65 100644 --- a/rest_framework/templates/rest_framework/horizontal/select_multiple.html +++ b/rest_framework/templates/rest_framework/horizontal/select_multiple.html @@ -11,7 +11,7 @@ {% endif %}
- {% for select in field.iter_options %} {% if select.start_option_group %} From a16dbfd11018fa01ceaf6dee7df34ab0430282cf Mon Sep 17 00:00:00 2001 From: David Smith <39445562+smithdc1@users.noreply.github.com> Date: Tue, 13 Jun 2023 07:55:22 +0100 Subject: [PATCH 06/11] Added Deprecation Warnings for CoreAPI (#7519) * Added Deprecation Warnings for CoreAPI * Bumped removal to DRF315 * Update rest_framework/__init__.py * Update rest_framework/filters.py * Update rest_framework/filters.py * Update tests/schemas/test_coreapi.py * Update rest_framework/filters.py * Update rest_framework/filters.py * Update tests/schemas/test_coreapi.py * Update tests/schemas/test_coreapi.py * Update tests/schemas/test_coreapi.py * Update tests/schemas/test_coreapi.py * Update rest_framework/pagination.py * Update rest_framework/pagination.py * Update rest_framework/pagination.py * Update rest_framework/pagination.py * Update rest_framework/schemas/coreapi.py * Update rest_framework/schemas/coreapi.py * Update rest_framework/schemas/coreapi.py * Update rest_framework/schemas/coreapi.py * Update rest_framework/schemas/coreapi.py * Update tests/schemas/test_coreapi.py * Update setup.cfg * Update tests/schemas/test_coreapi.py * Update tests/schemas/test_coreapi.py * Update tests/schemas/test_coreapi.py * Update tests/schemas/test_coreapi.py * Update tests/schemas/test_coreapi.py * Update tests/schemas/test_coreapi.py * Update rest_framework/pagination.py --------- Co-authored-by: Asif Saif Uddin --- rest_framework/__init__.py | 4 +++ rest_framework/filters.py | 8 +++++ rest_framework/pagination.py | 11 +++++++ rest_framework/schemas/coreapi.py | 12 ++++++- setup.cfg | 2 ++ tests/schemas/test_coreapi.py | 55 +++++++++++++++++++++++++++++-- 6 files changed, 89 insertions(+), 3 deletions(-) diff --git a/rest_framework/__init__.py b/rest_framework/__init__.py index cc24ce46c5..da7b88dfa2 100644 --- a/rest_framework/__init__.py +++ b/rest_framework/__init__.py @@ -31,3 +31,7 @@ class RemovedInDRF315Warning(DeprecationWarning): pass + + +class RemovedInDRF317Warning(PendingDeprecationWarning): + pass diff --git a/rest_framework/filters.py b/rest_framework/filters.py index 1ffd9edc02..17e6975eb4 100644 --- a/rest_framework/filters.py +++ b/rest_framework/filters.py @@ -3,6 +3,7 @@ returned by list views. """ import operator +import warnings from functools import reduce from django.core.exceptions import ImproperlyConfigured @@ -12,6 +13,7 @@ from django.utils.encoding import force_str from django.utils.translation import gettext_lazy as _ +from rest_framework import RemovedInDRF317Warning from rest_framework.compat import coreapi, coreschema, distinct from rest_framework.settings import api_settings @@ -29,6 +31,8 @@ def filter_queryset(self, request, queryset, view): def get_schema_fields(self, view): assert coreapi is not None, 'coreapi must be installed to use `get_schema_fields()`' + if coreapi is not None: + warnings.warn('CoreAPI compatibility is deprecated and will be removed in DRF 3.17', RemovedInDRF317Warning) assert coreschema is not None, 'coreschema must be installed to use `get_schema_fields()`' return [] @@ -146,6 +150,8 @@ def to_html(self, request, queryset, view): def get_schema_fields(self, view): assert coreapi is not None, 'coreapi must be installed to use `get_schema_fields()`' + if coreapi is not None: + warnings.warn('CoreAPI compatibility is deprecated and will be removed in DRF 3.17', RemovedInDRF317Warning) assert coreschema is not None, 'coreschema must be installed to use `get_schema_fields()`' return [ coreapi.Field( @@ -306,6 +312,8 @@ def to_html(self, request, queryset, view): def get_schema_fields(self, view): assert coreapi is not None, 'coreapi must be installed to use `get_schema_fields()`' + if coreapi is not None: + warnings.warn('CoreAPI compatibility is deprecated and will be removed in DRF 3.17', RemovedInDRF317Warning) assert coreschema is not None, 'coreschema must be installed to use `get_schema_fields()`' return [ coreapi.Field( diff --git a/rest_framework/pagination.py b/rest_framework/pagination.py index af508bef6d..ce87785472 100644 --- a/rest_framework/pagination.py +++ b/rest_framework/pagination.py @@ -4,6 +4,8 @@ """ import contextlib +import warnings + from base64 import b64decode, b64encode from collections import namedtuple from urllib import parse @@ -15,6 +17,7 @@ from django.utils.encoding import force_str from django.utils.translation import gettext_lazy as _ +from rest_framework import RemovedInDRF317Warning from rest_framework.compat import coreapi, coreschema from rest_framework.exceptions import NotFound from rest_framework.response import Response @@ -152,6 +155,8 @@ def get_results(self, data): def get_schema_fields(self, view): assert coreapi is not None, 'coreapi must be installed to use `get_schema_fields()`' + if coreapi is not None: + warnings.warn('CoreAPI compatibility is deprecated and will be removed in DRF 3.17', RemovedInDRF317Warning) return [] def get_schema_operation_parameters(self, view): @@ -311,6 +316,8 @@ def to_html(self): def get_schema_fields(self, view): assert coreapi is not None, 'coreapi must be installed to use `get_schema_fields()`' + if coreapi is not None: + warnings.warn('CoreAPI compatibility is deprecated and will be removed in DRF 3.17', RemovedInDRF317Warning) assert coreschema is not None, 'coreschema must be installed to use `get_schema_fields()`' fields = [ coreapi.Field( @@ -525,6 +532,8 @@ def get_count(self, queryset): def get_schema_fields(self, view): assert coreapi is not None, 'coreapi must be installed to use `get_schema_fields()`' + if coreapi is not None: + warnings.warn('CoreAPI compatibility is deprecated and will be removed in DRF 3.17', RemovedInDRF317Warning) assert coreschema is not None, 'coreschema must be installed to use `get_schema_fields()`' return [ coreapi.Field( @@ -930,6 +939,8 @@ def to_html(self): def get_schema_fields(self, view): assert coreapi is not None, 'coreapi must be installed to use `get_schema_fields()`' + if coreapi is not None: + warnings.warn('CoreAPI compatibility is deprecated and will be removed in DRF 3.17', RemovedInDRF317Warning) assert coreschema is not None, 'coreschema must be installed to use `get_schema_fields()`' fields = [ coreapi.Field( diff --git a/rest_framework/schemas/coreapi.py b/rest_framework/schemas/coreapi.py index 0713e0cb80..582aba196e 100644 --- a/rest_framework/schemas/coreapi.py +++ b/rest_framework/schemas/coreapi.py @@ -5,7 +5,7 @@ from django.db import models from django.utils.encoding import force_str -from rest_framework import exceptions, serializers +from rest_framework import RemovedInDRF317Warning, exceptions, serializers from rest_framework.compat import coreapi, coreschema, uritemplate from rest_framework.settings import api_settings @@ -118,6 +118,8 @@ class SchemaGenerator(BaseSchemaGenerator): def __init__(self, title=None, url=None, description=None, patterns=None, urlconf=None, version=None): assert coreapi, '`coreapi` must be installed for schema support.' + if coreapi is not None: + warnings.warn('CoreAPI compatibility is deprecated and will be removed in DRF 3.17', RemovedInDRF317Warning) assert coreschema, '`coreschema` must be installed for schema support.' super().__init__(title, url, description, patterns, urlconf) @@ -351,6 +353,9 @@ def __init__(self, manual_fields=None): will be added to auto-generated fields, overwriting on `Field.name` """ super().__init__() + if coreapi is not None: + warnings.warn('CoreAPI compatibility is deprecated and will be removed in DRF 3.17', RemovedInDRF317Warning) + if manual_fields is None: manual_fields = [] self._manual_fields = manual_fields @@ -592,6 +597,9 @@ def __init__(self, fields, description='', encoding=None): * `description`: String description for view. Optional. """ super().__init__() + if coreapi is not None: + warnings.warn('CoreAPI compatibility is deprecated and will be removed in DRF 3.17', RemovedInDRF317Warning) + assert all(isinstance(f, coreapi.Field) for f in fields), "`fields` must be a list of coreapi.Field instances" self._fields = fields self._description = description @@ -613,4 +621,6 @@ def get_link(self, path, method, base_url): def is_enabled(): """Is CoreAPI Mode enabled?""" + if coreapi is not None: + warnings.warn('CoreAPI compatibility is deprecated and will be removed in DRF 3.17', RemovedInDRF317Warning) return issubclass(api_settings.DEFAULT_SCHEMA_CLASS, AutoSchema) diff --git a/setup.cfg b/setup.cfg index 294e9afdd6..487d99db91 100644 --- a/setup.cfg +++ b/setup.cfg @@ -3,6 +3,8 @@ license_files = LICENSE.md [tool:pytest] addopts=--tb=short --strict-markers -ra +testspath = tests +filterwarnings = ignore:CoreAPI compatibility is deprecated*:rest_framework.RemovedInDRF317Warning [flake8] ignore = E501,W503,W504 diff --git a/tests/schemas/test_coreapi.py b/tests/schemas/test_coreapi.py index eddc5243ec..98fd46f9fc 100644 --- a/tests/schemas/test_coreapi.py +++ b/tests/schemas/test_coreapi.py @@ -7,16 +7,24 @@ from django.urls import include, path from rest_framework import ( - filters, generics, pagination, permissions, serializers + RemovedInDRF317Warning, filters, generics, pagination, permissions, + serializers ) from rest_framework.compat import coreapi, coreschema from rest_framework.decorators import action, api_view, schema +from rest_framework.filters import ( + BaseFilterBackend, OrderingFilter, SearchFilter +) +from rest_framework.pagination import ( + BasePagination, CursorPagination, LimitOffsetPagination, + PageNumberPagination +) from rest_framework.request import Request from rest_framework.routers import DefaultRouter, SimpleRouter from rest_framework.schemas import ( AutoSchema, ManualSchema, SchemaGenerator, get_schema_view ) -from rest_framework.schemas.coreapi import field_to_schema +from rest_framework.schemas.coreapi import field_to_schema, is_enabled from rest_framework.schemas.generators import EndpointEnumerator from rest_framework.schemas.utils import is_list_view from rest_framework.test import APIClient, APIRequestFactory @@ -1433,3 +1441,46 @@ def test_schema_handles_exception(): response.render() assert response.status_code == 403 assert b"You do not have permission to perform this action." in response.content + + +@pytest.mark.skipif(not coreapi, reason='coreapi is not installed') +def test_coreapi_deprecation(): + with pytest.warns(RemovedInDRF317Warning): + SchemaGenerator() + + with pytest.warns(RemovedInDRF317Warning): + AutoSchema() + + with pytest.warns(RemovedInDRF317Warning): + ManualSchema({}) + + with pytest.warns(RemovedInDRF317Warning): + deprecated_filter = OrderingFilter() + deprecated_filter.get_schema_fields({}) + + with pytest.warns(RemovedInDRF317Warning): + deprecated_filter = BaseFilterBackend() + deprecated_filter.get_schema_fields({}) + + with pytest.warns(RemovedInDRF317Warning): + deprecated_filter = SearchFilter() + deprecated_filter.get_schema_fields({}) + + with pytest.warns(RemovedInDRF317Warning): + paginator = BasePagination() + paginator.get_schema_fields({}) + + with pytest.warns(RemovedInDRF317Warning): + paginator = PageNumberPagination() + paginator.get_schema_fields({}) + + with pytest.warns(RemovedInDRF317Warning): + paginator = LimitOffsetPagination() + paginator.get_schema_fields({}) + + with pytest.warns(RemovedInDRF317Warning): + paginator = CursorPagination() + paginator.get_schema_fields({}) + + with pytest.warns(RemovedInDRF317Warning): + is_enabled() From aed7761a8d7e1691a4f4bbf9c83a447dac44d92a Mon Sep 17 00:00:00 2001 From: Asif Saif Uddin Date: Tue, 13 Jun 2023 15:01:29 +0600 Subject: [PATCH 07/11] Update copy right timeline --- rest_framework/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/__init__.py b/rest_framework/__init__.py index da7b88dfa2..b9e3f9817c 100644 --- a/rest_framework/__init__.py +++ b/rest_framework/__init__.py @@ -13,7 +13,7 @@ __version__ = '3.14.0' __author__ = 'Tom Christie' __license__ = 'BSD 3-Clause' -__copyright__ = 'Copyright 2011-2019 Encode OSS Ltd' +__copyright__ = 'Copyright 2011-2023 Encode OSS Ltd' # Version synonym VERSION = __version__ From 71f87a586400074f1840276c5cf36fc7da1c2c4c Mon Sep 17 00:00:00 2001 From: Konstantin Kuchkov Date: Wed, 14 Jun 2023 06:24:09 -0700 Subject: [PATCH 08/11] Fix NamespaceVersioning ignoring DEFAULT_VERSION on non-None namespaces (#7278) * Fix the case where if the namespace is not None and there's no match, NamespaceVersioning always raises NotFound even if DEFAULT_VERSION is set or None is in ALLOWED_VERSIONS * Add test cases --- rest_framework/versioning.py | 19 ++++---- tests/test_versioning.py | 93 +++++++++++++++++++++++++++++++++++- 2 files changed, 102 insertions(+), 10 deletions(-) diff --git a/rest_framework/versioning.py b/rest_framework/versioning.py index c2764c7a40..a1c0ce4d7b 100644 --- a/rest_framework/versioning.py +++ b/rest_framework/versioning.py @@ -119,15 +119,16 @@ class NamespaceVersioning(BaseVersioning): def determine_version(self, request, *args, **kwargs): resolver_match = getattr(request, 'resolver_match', None) - if resolver_match is None or not resolver_match.namespace: - return self.default_version - - # Allow for possibly nested namespaces. - possible_versions = resolver_match.namespace.split(':') - for version in possible_versions: - if self.is_allowed_version(version): - return version - raise exceptions.NotFound(self.invalid_version_message) + if resolver_match is not None and resolver_match.namespace: + # Allow for possibly nested namespaces. + possible_versions = resolver_match.namespace.split(':') + for version in possible_versions: + if self.is_allowed_version(version): + return version + + if not self.is_allowed_version(self.default_version): + raise exceptions.NotFound(self.invalid_version_message) + return self.default_version def reverse(self, viewname, args=None, kwargs=None, request=None, format=None, **extra): if request.version is not None: diff --git a/tests/test_versioning.py b/tests/test_versioning.py index b216461840..1ccecae0bf 100644 --- a/tests/test_versioning.py +++ b/tests/test_versioning.py @@ -272,7 +272,7 @@ class FakeResolverMatch(ResolverMatch): assert response.status_code == status.HTTP_404_NOT_FOUND -class TestAllowedAndDefaultVersion: +class TestAcceptHeaderAllowedAndDefaultVersion: def test_missing_without_default(self): scheme = versioning.AcceptHeaderVersioning view = AllowedVersionsView.as_view(versioning_class=scheme) @@ -318,6 +318,97 @@ def test_missing_with_default_and_none_allowed(self): assert response.data == {'version': 'v2'} +class TestNamespaceAllowedAndDefaultVersion: + def test_no_namespace_without_default(self): + class FakeResolverMatch: + namespace = None + + scheme = versioning.NamespaceVersioning + view = AllowedVersionsView.as_view(versioning_class=scheme) + + request = factory.get('/endpoint/') + request.resolver_match = FakeResolverMatch + response = view(request) + assert response.status_code == status.HTTP_404_NOT_FOUND + + def test_no_namespace_with_default(self): + class FakeResolverMatch: + namespace = None + + scheme = versioning.NamespaceVersioning + view = AllowedAndDefaultVersionsView.as_view(versioning_class=scheme) + + request = factory.get('/endpoint/') + request.resolver_match = FakeResolverMatch + response = view(request) + assert response.status_code == status.HTTP_200_OK + assert response.data == {'version': 'v2'} + + def test_no_match_without_default(self): + class FakeResolverMatch: + namespace = 'no_match' + + scheme = versioning.NamespaceVersioning + view = AllowedVersionsView.as_view(versioning_class=scheme) + + request = factory.get('/endpoint/') + request.resolver_match = FakeResolverMatch + response = view(request) + assert response.status_code == status.HTTP_404_NOT_FOUND + + def test_no_match_with_default(self): + class FakeResolverMatch: + namespace = 'no_match' + + scheme = versioning.NamespaceVersioning + view = AllowedAndDefaultVersionsView.as_view(versioning_class=scheme) + + request = factory.get('/endpoint/') + request.resolver_match = FakeResolverMatch + response = view(request) + assert response.status_code == status.HTTP_200_OK + assert response.data == {'version': 'v2'} + + def test_with_default(self): + class FakeResolverMatch: + namespace = 'v1' + + scheme = versioning.NamespaceVersioning + view = AllowedAndDefaultVersionsView.as_view(versioning_class=scheme) + + request = factory.get('/endpoint/') + request.resolver_match = FakeResolverMatch + response = view(request) + assert response.status_code == status.HTTP_200_OK + assert response.data == {'version': 'v1'} + + def test_no_match_without_default_but_none_allowed(self): + class FakeResolverMatch: + namespace = 'no_match' + + scheme = versioning.NamespaceVersioning + view = AllowedWithNoneVersionsView.as_view(versioning_class=scheme) + + request = factory.get('/endpoint/') + request.resolver_match = FakeResolverMatch + response = view(request) + assert response.status_code == status.HTTP_200_OK + assert response.data == {'version': None} + + def test_no_match_with_default_and_none_allowed(self): + class FakeResolverMatch: + namespace = 'no_match' + + scheme = versioning.NamespaceVersioning + view = AllowedWithNoneAndDefaultVersionsView.as_view(versioning_class=scheme) + + request = factory.get('/endpoint/') + request.resolver_match = FakeResolverMatch + response = view(request) + assert response.status_code == status.HTTP_200_OK + assert response.data == {'version': 'v2'} + + class TestHyperlinkedRelatedField(URLPatternsTestCase, APITestCase): included = [ path('namespaced//', dummy_pk_view, name='namespaced'), From 214702c4d48860ecaa813bdb10382cd3c4f3f69a Mon Sep 17 00:00:00 2001 From: Rizwan Shaikh Date: Mon, 12 Jun 2023 23:56:40 +0530 Subject: [PATCH 09/11] fix OpenAPIRenderer for timedelta --- rest_framework/renderers.py | 2 ++ rest_framework/utils/encoders.py | 11 +++++++++++ 2 files changed, 13 insertions(+) diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index 0a3b03729d..db1fdd128b 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -9,6 +9,7 @@ import base64 import contextlib +import datetime from urllib import parse from django import forms @@ -1062,6 +1063,7 @@ class Dumper(yaml.Dumper): def ignore_aliases(self, data): return True Dumper.add_representer(SafeString, Dumper.represent_str) + Dumper.add_representer(datetime.timedelta, encoders.CustomScalar.represent_timedelta) return yaml.dump(data, default_flow_style=False, sort_keys=False, Dumper=Dumper).encode('utf-8') diff --git a/rest_framework/utils/encoders.py b/rest_framework/utils/encoders.py index 35a89eb090..aa45422861 100644 --- a/rest_framework/utils/encoders.py +++ b/rest_framework/utils/encoders.py @@ -65,3 +65,14 @@ def default(self, obj): elif hasattr(obj, '__iter__'): return tuple(item for item in obj) return super().default(obj) + + +class CustomScalar: + """ + CustomScalar that knows how to encode timedelta that renderer + can understand. + """ + @classmethod + def represent_timedelta(cls, dumper, data): + value = str(data.total_seconds()) + return dumper.represent_scalar('tag:yaml.org,2002:str', value) From 95aad32e0652a042055e0f3ab1fa27faf97f2ff7 Mon Sep 17 00:00:00 2001 From: Rizwan Shaikh Date: Mon, 12 Jun 2023 23:57:50 +0530 Subject: [PATCH 10/11] added test for rendering openapi with timedelta --- tests/schemas/test_openapi.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/schemas/test_openapi.py b/tests/schemas/test_openapi.py index 0ea6d1ff92..e94bb91906 100644 --- a/tests/schemas/test_openapi.py +++ b/tests/schemas/test_openapi.py @@ -1162,6 +1162,18 @@ def test_schema_rendering_to_json(self): assert b'"openapi": "' in ret assert b'"default": "0.0"' in ret + def test_schema_rendering_to_yaml(self): + patterns = [ + path('example/', views.ExampleGenericAPIView.as_view()), + ] + generator = SchemaGenerator(patterns=patterns) + + request = create_request('/') + schema = generator.get_schema(request=request) + ret = OpenAPIRenderer().render(schema) + assert b"openapi: " in ret + assert b"default: '0.0'" in ret + def test_schema_with_no_paths(self): patterns = [] generator = SchemaGenerator(patterns=patterns) From c5613a8ed415fed81da48ed13a0d52730dd6c66e Mon Sep 17 00:00:00 2001 From: Rizwan Shaikh Date: Thu, 15 Jun 2023 23:53:54 +0530 Subject: [PATCH 11/11] added testcase for rendering yaml with minvalidator for duration field (timedelta) --- tests/schemas/test_openapi.py | 13 +++++++++++++ tests/schemas/views.py | 5 +++++ 2 files changed, 18 insertions(+) diff --git a/tests/schemas/test_openapi.py b/tests/schemas/test_openapi.py index e94bb91906..1eb5b84b71 100644 --- a/tests/schemas/test_openapi.py +++ b/tests/schemas/test_openapi.py @@ -1174,6 +1174,19 @@ def test_schema_rendering_to_yaml(self): assert b"openapi: " in ret assert b"default: '0.0'" in ret + def test_schema_rendering_timedelta_to_yaml_with_validator(self): + + patterns = [ + path('example/', views.ExampleValidatedAPIView.as_view()), + ] + generator = SchemaGenerator(patterns=patterns) + + request = create_request('/') + schema = generator.get_schema(request=request) + ret = OpenAPIRenderer().render(schema) + assert b"openapi: " in ret + assert b"duration:\n type: string\n minimum: \'10.0\'\n" in ret + def test_schema_with_no_paths(self): patterns = [] generator = SchemaGenerator(patterns=patterns) diff --git a/tests/schemas/views.py b/tests/schemas/views.py index f1ed0bd4e3..c08208bf26 100644 --- a/tests/schemas/views.py +++ b/tests/schemas/views.py @@ -134,6 +134,11 @@ class ExampleValidatedSerializer(serializers.Serializer): ip4 = serializers.IPAddressField(protocol='ipv4') ip6 = serializers.IPAddressField(protocol='ipv6') ip = serializers.IPAddressField() + duration = serializers.DurationField( + validators=( + MinValueValidator(timedelta(seconds=10)), + ) + ) class ExampleValidatedAPIView(generics.GenericAPIView):