Skip to content

Commit

Permalink
Merge branch 'tfranzel:master' into allowed-method-names
Browse files Browse the repository at this point in the history
  • Loading branch information
jekel authored Nov 4, 2023
2 parents 5ca4fc4 + edce053 commit d8632a8
Show file tree
Hide file tree
Showing 13 changed files with 150 additions and 20 deletions.
23 changes: 23 additions & 0 deletions .readthedocs.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Read the Docs configuration file for Sphinx projects
# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details

# Required
version: 2

# Set the OS, Python version and other tools you might need
build:
os: ubuntu-22.04
tools:
python: "3.11"

# Build documentation in the "docs/" directory with Sphinx
sphinx:
configuration: docs/conf.py

# Optional but recommended, declare the Python requirements required
# to build your documentation
# See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html
python:
install:
- requirements: requirements/base.txt
- requirements: requirements/docs.txt
18 changes: 18 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,24 @@
Changelog
=========

0.26.5 (2023-09-23)
-------------------

- update FAQ entry on extension loading
- Fix (`#1079 <https://github.com/tfranzel/drf-spectacular/issues/1079>`_) crash when generating schema for field with UUID choices. [Pedro Borges]
- chore: fix typos [Heinz-Alexander Fuetterer]
- Use schema_url in SpectacularElementsView (`#1067 <https://github.com/tfranzel/drf-spectacular/issues/1067>`_) [q0w]
- add helper to disable viewset list detection `#1064 <https://github.com/tfranzel/drf-spectacular/issues/1064>`_
- pin django-allauth test dep due to breaking change with dj-rest-auth
- fix example building for pagination with basic list `#1055 <https://github.com/tfranzel/drf-spectacular/issues/1055>`_
- Fix discarded falsy examples values `#1049 <https://github.com/tfranzel/drf-spectacular/issues/1049>`_

Breaking changes / important additions:

- Added helper function ``forced_singular_serializer`` to disable a list detection on a endpoint, that has been quite difficult to properly
undo previously. This closes the functional gap for ``@extend_schema_serializer(many=False)`` in single-use (non-envelope) situations.
- Several small bugfixes

0.26.4 (2023-07-23)
-------------------

Expand Down
24 changes: 21 additions & 3 deletions docs/faq.rst
Original file line number Diff line number Diff line change
Expand Up @@ -247,9 +247,27 @@ call is proxied through the entrypoint.
Where should I put my extensions? / my extensions are not detected
------------------------------------------------------------------

The extensions register themselves automatically. Just be sure that the python interpreter sees them at least once.
To that end, we suggest creating a ``PROJECT/schema.py`` file and importing it in your ``PROJECT/__init__.py``
(same directory as ``settings.py`` and ``urls.py``) with ``import PROJECT.schema``.
The extensions register themselves automatically. Just be sure that the Python interpreter sees them at least once.
It is good practice to collect your extensions in ``YOUR_MAIN_APP_NAME/schema.py`` and to import that
file in your ``YOUR_MAIN_APP_NAME/apps.py``. Performing the import in the ``ready()`` method is the most robust
approach. It will make sure your environment (e.g. settings) is properly set up prior to loading.

.. code-block:: python
# your_main_app_name/apps.py
class YourMainAppNameConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "your_main_app_name"
def ready(self):
import your_main_app_name.schema # noqa: E402
While there are certainly other ways of loading your extensions, this is a battle-proven and robust way to do it.
Generally in Django/DRF, importing stuff in the wrong order often results in weird errors or circular
import issues, which this approach tries to carefully circumvent.


My ``@action`` is erroneously paginated or has filter parameters that I do not want
-----------------------------------------------------------------------------------
Expand Down
2 changes: 1 addition & 1 deletion drf_spectacular/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import django

__version__ = '0.26.4'
__version__ = '0.26.5'

if django.VERSION < (3, 2):
default_app_config = 'drf_spectacular.apps.SpectacularConfig'
4 changes: 2 additions & 2 deletions drf_spectacular/openapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -583,7 +583,7 @@ def _map_model_field(self, model_field, direction):
return self._map_model_field(model_field.target_field, direction)
elif hasattr(models, 'JSONField') and isinstance(model_field, models.JSONField):
# fix for DRF==3.11 with django>=3.1 as it is not yet represented in the field_mapping
return build_basic_type(OpenApiTypes.OBJECT)
return build_basic_type(OpenApiTypes.ANY)
elif isinstance(model_field, models.BinaryField):
return build_basic_type(OpenApiTypes.BYTE)
elif hasattr(models, model_field.get_internal_type()):
Expand Down Expand Up @@ -847,7 +847,7 @@ def _map_serializer_field(self, field, direction, bypass_extensions=False):
return append_meta(build_basic_type(OpenApiTypes.BOOL), meta)

if isinstance(field, serializers.JSONField):
return append_meta(build_basic_type(OpenApiTypes.OBJECT), meta)
return append_meta(build_basic_type(OpenApiTypes.ANY), meta)

if isinstance(field, (serializers.DictField, serializers.HStoreField)):
content = build_basic_type(OpenApiTypes.OBJECT)
Expand Down
7 changes: 4 additions & 3 deletions drf_spectacular/plumbing.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
from rest_framework.fields import empty
from rest_framework.settings import api_settings
from rest_framework.test import APIRequestFactory
from rest_framework.utils.encoders import JSONEncoder
from rest_framework.utils.mediatypes import _MediaType
from rest_framework.utils.serializer_helpers import ReturnDict, ReturnList
from uritemplate import URITemplate
Expand Down Expand Up @@ -409,9 +410,9 @@ def build_choice_field(field):
else:
type = None

if field.allow_blank:
if field.allow_blank and '' not in choices:
choices.append('')
if field.allow_null:
if field.allow_null and None not in choices:
choices.append(None)

schema = {
Expand Down Expand Up @@ -839,7 +840,7 @@ def load_enum_name_overrides():


def list_hash(lst):
return hashlib.sha256(json.dumps(list(lst), sort_keys=True).encode()).hexdigest()
return hashlib.sha256(json.dumps(list(lst), sort_keys=True, cls=JSONEncoder).encode()).hexdigest()


def anchor_pattern(pattern: str) -> str:
Expand Down
2 changes: 0 additions & 2 deletions drf_spectacular/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,6 @@ class SpectacularSwaggerView(APIView):
renderer_classes = [TemplateHTMLRenderer]
permission_classes = spectacular_settings.SERVE_PERMISSIONS
authentication_classes = AUTHENTICATION_CLASSES
versioning_class = None
url_name = 'schema'
url = None
template_name = 'drf_spectacular/swagger_ui.html'
Expand Down Expand Up @@ -232,7 +231,6 @@ class SpectacularRedocView(APIView):
renderer_classes = [TemplateHTMLRenderer]
permission_classes = spectacular_settings.SERVE_PERMISSIONS
authentication_classes = AUTHENTICATION_CLASSES
versioning_class = None
url_name = 'schema'
url = None
template_name = 'drf_spectacular/redoc.html'
Expand Down
19 changes: 18 additions & 1 deletion tests/contrib/test_django_filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from django.db import models
from django.db.models import F
from django.urls import include, path
from rest_framework import routers, serializers, viewsets
from rest_framework import generics, routers, serializers, viewsets
from rest_framework.test import APIClient

from drf_spectacular.types import OpenApiTypes
Expand Down Expand Up @@ -419,3 +419,20 @@ class XViewSet(viewsets.ModelViewSet):
assert schema['paths']['/x/']['get']['parameters'][0]['description'] == (
'* `one` - One\n* `two` - Two\n* `three` - Three'
)


@pytest.mark.contrib('django_filter')
def test_filter_on_listapiview(no_warnings):
class XListView(generics.ListAPIView):
queryset = Product.objects.all()
serializer_class = ProductSerializer
filter_backends = (DjangoFilterBackend,)
filterset_class = ProductFilter

def get_queryset(self):
return Product.objects.all().annotate(
price_vat=F('price') * 1.19
)

schema = generate_schema('/x/', view=XListView)
assert len(schema['paths']['/x/']['get']['parameters']) > 1
4 changes: 1 addition & 3 deletions tests/test_extend_schema.yml
Original file line number Diff line number Diff line change
Expand Up @@ -320,9 +320,7 @@ components:
type: string
field_b:
type: integer
field_c:
type: object
additionalProperties: {}
field_c: {}
required:
- field_a
- field_b
Expand Down
4 changes: 1 addition & 3 deletions tests/test_fields.yml
Original file line number Diff line number Diff line change
Expand Up @@ -190,9 +190,7 @@ components:
type: object
additionalProperties:
type: integer
field_json:
type: object
additionalProperties: {}
field_json: {}
field_sub_object_calculated:
type: integer
description: My calculated property
Expand Down
23 changes: 21 additions & 2 deletions tests/test_plumbing.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,9 @@

from drf_spectacular.openapi import AutoSchema
from drf_spectacular.plumbing import (
analyze_named_regex_pattern, build_basic_type, detype_pattern, follow_field_source,
force_instance, get_list_serializer, is_field, is_serializer, resolve_type_hint,
analyze_named_regex_pattern, build_basic_type, build_choice_field, detype_pattern,
follow_field_source, force_instance, get_list_serializer, is_field, is_serializer,
resolve_type_hint,
)
from drf_spectacular.validation import validate_schema
from tests import generate_schema
Expand Down Expand Up @@ -358,3 +359,21 @@ def test_analyze_named_regex_pattern(no_warnings, pattern, output):
def test_unknown_basic_type(capsys):
build_basic_type(object)
assert 'could not resolve type for "<class \'object\'>' in capsys.readouterr().err


def test_choicefield_choices_enum():
schema = build_choice_field(serializers.ChoiceField(['bluepill', 'redpill']))
assert schema['enum'] == ['bluepill', 'redpill']
assert schema['type'] == 'string'

schema = build_choice_field(serializers.ChoiceField(
['bluepill', 'redpill'], allow_null=True, allow_blank=True
))
assert schema['enum'] == ['bluepill', 'redpill', '', None]
assert schema['type'] == 'string'

schema = build_choice_field(serializers.ChoiceField(
choices=['bluepill', 'redpill', '', None], allow_null=True, allow_blank=True
))
assert schema['enum'] == ['bluepill', 'redpill', '', None]
assert 'type' not in schema
26 changes: 26 additions & 0 deletions tests/test_postprocessing.py
Original file line number Diff line number Diff line change
Expand Up @@ -314,3 +314,29 @@ def get(self, request):
'items': {'$ref': '#/components/schemas/QualityLevelsEnum'},
'readOnly': True
}


def test_uuid_choices(no_warnings):

import uuid

class XSerializer(serializers.Serializer):
foo = serializers.ChoiceField(
choices=[
(uuid.UUID('93d7527f-de3c-4a76-9cc2-5578675630d4'), 'baz'),
(uuid.UUID('47a4b873-409e-4e43-81d5-fafc3faeb849'), 'bar')
]
)

class XAPIView(APIView):
@extend_schema(responses=XSerializer)
def get(self, request):
pass # pragma: no cover

schema = generate_schema('x', view=XAPIView)

assert 'FooEnum' in schema['components']['schemas']
assert schema['components']['schemas']['FooEnum']['enum'] == [
uuid.UUID('93d7527f-de3c-4a76-9cc2-5578675630d4'),
uuid.UUID('47a4b873-409e-4e43-81d5-fafc3faeb849')
]
14 changes: 14 additions & 0 deletions tests/test_regressions.py
Original file line number Diff line number Diff line change
Expand Up @@ -2287,6 +2287,20 @@ class XViewset(viewsets.ReadOnlyModelViewSet):
assert schema['paths']['/x/']['get']['security'] == [{'tokenAuth': []}, {}]


@mock.patch(
'drf_spectacular.settings.spectacular_settings.AUTHENTICATION_WHITELIST', []
)
def test_authentication_empty_whitelist(no_warnings):
class XViewset(viewsets.ReadOnlyModelViewSet):
serializer_class = SimpleSerializer
queryset = SimpleModel.objects.none()
authentication_classes = [BasicAuthentication, TokenAuthentication]

schema = generate_schema('/x', XViewset)
assert 'securitySchemes' not in schema['components']
assert schema['paths']['/x/']['get']['security'] == [{}]


def test_request_response_raw_schema_annotation(no_warnings):
@extend_schema(
request={'application/pdf': {'type': 'string', 'format': 'binary'}},
Expand Down

0 comments on commit d8632a8

Please sign in to comment.