Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/master' into pr125
Browse files Browse the repository at this point in the history
  • Loading branch information
tfranzel committed Oct 11, 2020
2 parents 5970984 + 507cf19 commit afb9424
Show file tree
Hide file tree
Showing 32 changed files with 1,275 additions and 162 deletions.
10 changes: 8 additions & 2 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ jobs:
env: TOXENV=py36-django3.0-drf3.10
- python: 3.6
env: TOXENV=py36-django3.0-drf3.11
- python: 3.6
env: TOXENV=py36-django3.1-drf3.11

- python: 3.7
env: TOXENV=py37-django2.2-drf3.10
Expand All @@ -31,6 +33,8 @@ jobs:
env: TOXENV=py37-django3.0-drf3.10
- python: 3.7
env: TOXENV=py37-django3.0-drf3.11
- python: 3.7
env: TOXENV=py37-django3.1-drf3.11

- python: 3.8
env: TOXENV=py38-django2.2-drf3.10
Expand All @@ -40,9 +44,11 @@ jobs:
env: TOXENV=py38-django3.0-drf3.10
- python: 3.8
env: TOXENV=py38-django3.0-drf3.11
- python: 3.8
env: TOXENV=py38-django3.1-drf3.11

- python: 3.8
env: TOXENV=py38-django3.0-drfmaster
env: TOXENV=py38-django3.1-drfmaster
- python: 3.8
env: TOXENV=py38-djangomaster-drf3.11
- python: 3.8
Expand All @@ -51,7 +57,7 @@ jobs:
env: TOXENV=py38-drfmaster-djangomaster-allowcontribfail

allow_failures:
- env: TOXENV=py38-django3.0-drfmaster
- env: TOXENV=py38-django3.1-drfmaster
- env: TOXENV=py38-djangomaster-drf3.11
- env: TOXENV=py38-drfmaster-djangomaster
- env: TOXENV=py38-drfmaster-djangomaster-allowcontribfail
Expand Down
62 changes: 62 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,68 @@
Changelog
=========

0.9.14 (2020-10-04)
-------------------

- improve client generation for paginated listings
- update pinned swagger-ui version `#160 <https://github.com/tfranzel/drf-spectacular/issues/160>`_
- Hot fix for AcceptVersioningHeader support [Nicolas Delaby]
- bugfix module string includes with urlpatterns `#157 <https://github.com/tfranzel/drf-spectacular/issues/157>`_
- add expressive error in case of misconfiguration `#156 <https://github.com/tfranzel/drf-spectacular/issues/156>`_
- fix django-filter related resolution. improve test `#150 <https://github.com/tfranzel/drf-spectacular/issues/150>`_ `#151 <https://github.com/tfranzel/drf-spectacular/issues/151>`_
- improve follow_field_source for reverse resolution and model leafs `#150 <https://github.com/tfranzel/drf-spectacular/issues/150>`_
- add ref if list field child is serializer [Matt Shirley]
- add customization option for mock request generation `#135 <https://github.com/tfranzel/drf-spectacular/issues/135>`_

Breaking changes:

- paginated list response is now wrapped in its own component

0.9.13 (2020-09-13)
-------------------

- bugfix filter parameter application on non-list views `#147 <https://github.com/tfranzel/drf-spectacular/issues/147>`_
- improved support for django-filter
- add mocked request for view processing. `#81 <https://github.com/tfranzel/drf-spectacular/issues/81>`_ `#141 <https://github.com/tfranzel/drf-spectacular/issues/141>`_
- Use sha256 to hash lists [David Davis]
- change empty operation name on API prefix-cut to "root"
- bugfix lost "missing hint" warning and incorrect empty fallback
- add operationId collision resolution `#137 <https://github.com/tfranzel/drf-spectacular/issues/137>`_
- bugfix leaking path var names in operationId `#137 <https://github.com/tfranzel/drf-spectacular/issues/137>`_
- add config for camelizing names `#138 <https://github.com/tfranzel/drf-spectacular/issues/138>`_
- bugfix parameterized patterns for namespace versioning `#145 <https://github.com/tfranzel/drf-spectacular/issues/145>`_
- Add support for Accept header versioning [Krzysztof Socha]
- support for DictField child type (`#142 <https://github.com/tfranzel/drf-spectacular/issues/142>`_) and models.JSONField (Django>=3.1)
- add convenience inline_serializer for extend_schema `#139 <https://github.com/tfranzel/drf-spectacular/issues/139>`_
- remove multipleOf due to schema violation `#131 <https://github.com/tfranzel/drf-spectacular/issues/131>`_

Breaking changes:

- ``operationId`` changed for endpoints using the DRF's ``FORMAT`` path feature.
- ``operationId`` changed where there were path variables leaking into the name.

0.9.12 (2020-07-22)
-------------------

- Temporarily pin the swagger-ui unpkg URL to 3.30.0 [Mohamed Abdulaziz]
- Add `deepLinking` parameter [p.alekseev]
- added preprocessing hooks for operation list modification/filtering `#93 <https://github.com/tfranzel/drf-spectacular/issues/93>`_
- Document effective DRF settings [John Vandenberg]
- add format query parameter `#110 <https://github.com/tfranzel/drf-spectacular/issues/110>`_
- improve assert messages `#126 <https://github.com/tfranzel/drf-spectacular/issues/126>`_
- more graceful handling of magic fields `#126 <https://github.com/tfranzel/drf-spectacular/issues/126>`_
- allow for field child on ListSerializer. `#120 <https://github.com/tfranzel/drf-spectacular/issues/120>`_
- Fix sorting of endpoints with params [John Vandenberg]
- Emit enum of possible format suffixes [John Vandenberg]
- i18n `#109 <https://github.com/tfranzel/drf-spectacular/issues/109>`_
- bugfix INSTALLED_APP retrieval `#114 <https://github.com/tfranzel/drf-spectacular/issues/114>`_
- emit import warning for extensions with installed apps `#114 <https://github.com/tfranzel/drf-spectacular/issues/114>`_

Breaking changes:

- ``drf_spectacular.hooks.postprocess_schema_enums`` moved from ``blumbing`` to ``hooks`` for consistency. Only relevant if ``POSTPROCESSING_HOOKS`` is explicitly set by user.
- preprocessing hooks are currently experimental and may change on the next release.

0.9.11 (2020-07-08)
-------------------

Expand Down
1 change: 1 addition & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ Features
- `DjangoOAuthToolkit <https://github.com/jazzband/django-oauth-toolkit>`_
- `djangorestframework-jwt <https://github.com/jpadilla/django-rest-framework-jwt>`_ (tested fork `drf-jwt <https://github.com/Styria-Digital/django-rest-framework-jwt>`_)
- `djangorestframework-camel-case <https://github.com/vbabiy/djangorestframework-camel-case>`_ (via postprocessing hook ``camelize_serializer_fields``)
- `django-filter <https://github.com/carltongibson/django-filter>`_ (basic support out-of-the-box; improved types either with ``SpectacularDjangoFilterBackendMixin`` or drf-spectacular's ``DjangoFilterBackend``)


For more information visit the `documentation <https://drf-spectacular.readthedocs.io>`_.
Expand Down
6 changes: 6 additions & 0 deletions docs/customization.rst
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@ discovered in the introspection.
def retrieve(self, request, *args, **kwargs)
# your code
.. note:: For simple responses, you might not go through the hassle of writing an explicit serializer class.
In those cases, you can simply specify the request/response with a call to
:py:func:`inline_serializer <drf_spectacular.utils.inline_serializer>`.
This lets you conveniently define the endpoint's schema inline without actually writing a serializer class.


Step 3: :py:class:`@extend_schema_field <drf_spectacular.utils.extend_schema_field>` and type hints
---------------------------------------------------------------------------------------------------
A custom ``SerializerField`` might not get picked up properly. You can inform `drf-spectacular`
Expand Down
49 changes: 26 additions & 23 deletions docs/settings.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,32 @@ You can override any setting, otherwise the defaults below are used.
:start-after: APISettings
:end-before: IMPORT_STRINGS


Django Rest Framework settings
------------------------------

Some of the `Django Rest Framework settings <https://www.django-rest-framework.org/api-guide/settings/>`_
also impact the schema generation. Refer to the documentation for the version that you are using.

Settings which effect the processing of requests and data types of responses will usually be effective.

There is explicit use of these settings:

- ``DEFAULT_SCHEMA_CLASS``
- ``COERCE_DECIMAL_TO_STRING``
- ``UPLOADED_FILES_USE_URL``
- ``URL_FORMAT_OVERRIDE``
- ``FORMAT_SUFFIX_KWARG``

The following settings are ignored:

- ``SCHEMA_COERCE_METHOD_NAMES``

The following are known to be effective:

- ``SCHEMA_COERCE_PATH_PK``


Example: API Key securitySchemes & security setting
---------------------------------------------------------------------

Expand All @@ -35,26 +61,3 @@ This can be done in the following way:
},
"SECURITY": [{"ApiKeyAuth": [], }],
}
Django Rest Framework settings
------------------------------

Some of the `Django Rest Framework settings <https://www.django-rest-framework.org/api-guide/settings/>`_
also impact the schema generation. Refer to the documentation for the version that you are using.

Settings which effect the processing of requests and data types of responses will usually be effective.

There is explicit use of these settings:

- `COERCE_DECIMAL_TO_STRING`
- `UPLOADED_FILES_USE_URL`

The following settings are ignored:

- `SCHEMA_COERCE_METHOD_NAMES`

The following are known to be effective:

- `SCHEMA_COERCE_PATH_PK`
- `FORMAT_SUFFIX_KWARG`
- `URL_FORMAT_OVERRIDE`
2 changes: 1 addition & 1 deletion drf_spectacular/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = '0.9.11'
__version__ = '0.9.14'
39 changes: 39 additions & 0 deletions drf_spectacular/contrib/django_filters.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
from drf_spectacular.plumbing import build_parameter_type, follow_field_source, get_view_model
from drf_spectacular.utils import OpenApiParameter

try:
from django_filters.rest_framework import DjangoFilterBackend as OriginalDjangoFilterBackend
except ImportError:
class OriginalDjangoFilterBackend: # type: ignore
pass


class SpectacularDjangoFilterBackendMixin:
def get_schema_operation_parameters(self, view):
model = get_view_model(view)
if not model:
return []

filterset_class = self.get_filterset_class(view, model.objects.none())
if not filterset_class:
return []

parameters = []
for field_name, field in filterset_class.base_filters.items():
path = field.field_name.split('__')
model_field = follow_field_source(model, path)

parameters.append(build_parameter_type(
name=field_name,
required=field.extra['required'],
location=OpenApiParameter.QUERY,
description=field.label if field.label is not None else field_name,
schema=view.schema._map_model_field(model_field, direction=None),
enum=[c for c, _ in field.extra.get('choices', [])],
))

return parameters


class DjangoFilterBackend(SpectacularDjangoFilterBackendMixin, OriginalDjangoFilterBackend):
pass
30 changes: 24 additions & 6 deletions drf_spectacular/generators.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,11 @@
from rest_framework.schemas.generators import EndpointEnumerator as BaseEndpointEnumerator

from drf_spectacular.extensions import OpenApiViewExtension
from drf_spectacular.openapi import AutoSchema
from drf_spectacular.plumbing import (
ComponentRegistry, alpha_operation_sorter, build_root_object, error, is_versioning_supported,
modify_for_versioning, normalize_result_object, operation_matches_version,
reset_generator_stats, warn,
ComponentRegistry, alpha_operation_sorter, build_root_object, camelize_operation, error,
is_versioning_supported, modify_for_versioning, normalize_result_object,
operation_matches_version, reset_generator_stats, sanitize_result_object, warn,
)
from drf_spectacular.settings import spectacular_settings

Expand Down Expand Up @@ -127,6 +128,13 @@ def parse(self, request, public):
if not self.has_view_permissions(path, method, view):
continue

# mocked request to allow certain operations in get_queryset and get_serializer[_class]
# without exceptions being raised due to no request.
if not request:
request = spectacular_settings.GET_MOCK_REQUEST(method, path, view, request)

view.request = request

if view.versioning_class and not is_versioning_supported(view.versioning_class):
warn(
f'using unsupported versioning class "{view.versioning_class}". view will be '
Expand All @@ -138,11 +146,17 @@ def parse(self, request, public):
or getattr(request, 'version', None) # incoming request was versioned
or view.versioning_class.default_version # fallback
)
if not version:
continue
path = modify_for_versioning(self.inspector.patterns, method, path, view, version)
if not version or not operation_matches_version(view, version):
if not operation_matches_version(view, version):
continue

# beware that every access to schema yields a fresh object (descriptor pattern)
assert isinstance(view.schema, AutoSchema), (
'Incompatible AutoSchema used on View. Is DRF\'s DEFAULT_SCHEMA_CLASS '
'pointing to "drf_spectacular.openapi.AutoSchema" or any other drf-spectacular '
'compatible AutoSchema?'
)
operation = view.schema.get_operation(path, path_regex, method, self.registry)

# operation was manually removed via @extend_schema
Expand All @@ -154,6 +168,9 @@ def parse(self, request, public):
path = path[1:]
path = urljoin(self.url or '/', path)

if spectacular_settings.CAMELIZE_NAMES:
path, operation = camelize_operation(path, operation)

result.setdefault(path, {})
result[path][method.lower()] = operation

Expand All @@ -168,4 +185,5 @@ def get_schema(self, request=None, public=False):
)
for hook in spectacular_settings.POSTPROCESSING_HOOKS:
result = hook(result=result, generator=self, request=request, public=public)
return normalize_result_object(result)

return sanitize_result_object(normalize_result_object(result))
39 changes: 29 additions & 10 deletions drf_spectacular/hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,16 @@ def iter_prop_containers(schema):
yield from iter_prop_containers(schema.get('oneOf', []))
yield from iter_prop_containers(schema.get('allOf', []))

def create_enum_component(name, schema):
component = ResolvedComponent(
name=name,
type=ResolvedComponent.SCHEMA,
schema=schema,
object=name,
)
generator.registry.register_on_missing(component)
return component

schemas = result.get('components', {}).get('schemas', {})

overrides = load_enum_name_overrides()
Expand All @@ -36,7 +46,9 @@ def iter_prop_containers(schema):
for prop_name, prop_schema in props.items():
if 'enum' not in prop_schema:
continue
hash_mapping[prop_name].add(list_hash(prop_schema['enum']))
# remove blank/null entry for hashing. will be reconstructed in the last step
prop_enum_cleaned_list = [i for i in prop_schema['enum'] if i]
hash_mapping[prop_name].add(list_hash(prop_enum_cleaned_list))

# traverse all enum properties and generate a name for the choice set. naming collisions
# are resolved and a warning is emitted. giving a choice set multiple names is technically
Expand Down Expand Up @@ -73,23 +85,30 @@ def iter_prop_containers(schema):
if 'enum' not in prop_schema:
continue

prop_enum_original_list = prop_schema['enum']
prop_schema['enum'] = [i for i in prop_schema['enum'] if i]
prop_hash = list_hash(prop_schema['enum'])
# when choice sets are reused under multiple names, the generated name cannot be
# resolved from the hash alone. fall back to prop_name and hash for resolution.
enum_name = enum_name_mapping.get(prop_hash) or enum_name_mapping[prop_hash, prop_name]

# split property into remaining property and enum component parts
enum_schema = {k: v for k, v in prop_schema.items() if k in ['type', 'enum']}
prop_schema = {k: v for k, v in prop_schema.items() if k not in ['type', 'enum']}

component = ResolvedComponent(
name=enum_name,
type=ResolvedComponent.SCHEMA,
schema=enum_schema,
object=enum_name,
)
if component not in generator.registry:
generator.registry.register(component)
prop_schema.update(component.ref)
components = [
create_enum_component(enum_name, schema=enum_schema)
]
if '' in prop_enum_original_list:
components.append(create_enum_component('BlankEnum', schema={'enum': ['']}))
if None in prop_enum_original_list:
components.append(create_enum_component('NullEnum', schema={'enum': [None]}))

if len(components) == 1:
prop_schema.update(components[0].ref)
else:
prop_schema.update({'oneOf': [c.ref for c in components]})

props[prop_name] = safe_ref(prop_schema)

# sort again with additional components
Expand Down
Loading

0 comments on commit afb9424

Please sign in to comment.