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): pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy