Compare commits

..

15 Commits

Author SHA1 Message Date
Pietro Brenna 728c02356c fix #559: recursively resolve type of one2one using target_field 2020-03-18 19:15:18 +01:00
Cristi Vîjdea 9ccf24c27a Add 1.17.1 changelog 2020-02-17 03:40:09 +02:00
Cristi Vîjdea 8aa255cf56 Drop Python 2.7 tests 2020-02-17 03:40:00 +02:00
Cristi Vîjdea 7491d330a8 Fix ugettext_lazy warning 2020-02-17 03:12:59 +02:00
Cristi Vîjdea ebe21b77c6 Fix tox.ini 2020-02-17 03:12:07 +02:00
Cristi Vîjdea 17da098940 Fix lint errors 2020-02-17 03:06:37 +02:00
Cristi Vîjdea a872eb66d6 Add Django 3.0, DRF 3.11, drop Python 3.5, Django 2.1 2020-02-17 02:58:55 +02:00
johnthagen 6a1166deb5 Add example for using swagger_schema_fields for a Field (#494)
* Add example for using swagger_schema_fields for a Field
* Mention that Meta class can be added to fields as well
* Reference the DRF docs on how to add validation to serializers
2019-11-16 18:27:03 +02:00
Yannick Chabbert b700191f46 add comments on why we returns non form media types by default (#436) 2019-11-14 15:03:11 +02:00
Jethro Lee 5c25ecd8f2 Edit type check for swagger_auto_schema (#490) 2019-11-14 14:57:49 +02:00
johnthagen 8fd27664f1 Fix typo in docstring (#479) 2019-11-14 14:17:53 +02:00
yurihs 456b697ca2 Apply dedent to descriptions (#416) (#464) 2019-11-14 14:13:40 +02:00
johnthagen 9966297f87 Support Python 3.8 (#477)
* Support Python 3.8
* Add Python 3.8 trove classifier
* Add Python 3.8 support to README
2019-11-14 14:01:55 +02:00
Carlos Martinez 27007a9cf4 Fix #485 (#486) 2019-11-14 00:13:29 +02:00
Ned Batchelder a72e5b2899 Write multi-line strings in block style (#466)
Closes  #439
2019-10-03 02:06:05 +03:00
20 changed files with 176 additions and 61 deletions

View File

@ -1,10 +1,8 @@
language: python language: python
python: python:
- '2.7'
- '3.5'
- '3.6' - '3.6'
- '3.7' - '3.7'
- '3.8-dev' - '3.8'
dist: xenial dist: xenial
@ -39,7 +37,6 @@ matrix:
allow_failures: allow_failures:
- env: TOXENV=lint - env: TOXENV=lint
- env: TOXENV=djmaster - env: TOXENV=djmaster
- python: '3.8-dev'
fast_finish: true fast_finish: true

View File

@ -57,7 +57,7 @@ You want to contribute some code? Great! Here are a few steps to get you started
.. code:: console .. code:: console
(venv) $ python testproj/manage.py generate_swagger ../tests/reference.yaml --overwrite --user admin --url http://test.local:8002/ (venv) $ python testproj/manage.py generate_swagger tests/reference.yaml --overwrite --user admin --url http://test.local:8002/
After checking the git diff to verify that no unexpected changes appeared, you should commit the new After checking the git diff to verify that no unexpected changes appeared, you should commit the new
``reference.yaml`` together with your changes. ``reference.yaml`` together with your changes.

View File

@ -13,9 +13,9 @@ Generate **real** Swagger/OpenAPI 2.0 specifications from a Django Rest Framewor
Compatible with Compatible with
- **Django Rest Framework**: 3.8, 3.9, 3.10 - **Django Rest Framework**: 3.8, 3.9, 3.10, 3.11
- **Django**: 1.11, 2.1, 2.2 - **Django**: 1.11, 2.2, 3.0
- **Python**: 2.7, 3.5, 3.6, 3.7 - **Python**: 2.7, 3.6, 3.7, 3.8
Only the latest patch version of each ``major.minor`` series of Python, Django and Django REST Framework is supported. Only the latest patch version of each ``major.minor`` series of Python, Django and Django REST Framework is supported.

View File

@ -2,6 +2,18 @@
Changelog Changelog
######### #########
**********
**1.17.1**
**********
*Release date: Feb 17, 2020*
- **FIXED:** fixed compatibility issue with CurrentUserDefault in Django Rest Framework 3.11
- **FIXED:** respect `USERNAME_FIELD` in `generate_swagger` command (:pr:`486`)
**Support was dropped for Python 3.5, Django 2.0, Django 2.1, DRF 3.7**
********** **********
**1.17.0** **1.17.0**
********** **********

View File

@ -212,7 +212,8 @@ Schema generation of ``serializers.SerializerMethodField`` is supported in two w
Serializer ``Meta`` nested class Serializer ``Meta`` nested class
******************************** ********************************
You can define some per-serializer options by adding a ``Meta`` class to your serializer, e.g.: You can define some per-serializer or per-field options by adding a ``Meta`` class to your ``Serializer`` or
serializer ``Field``, e.g.:
.. code-block:: python .. code-block:: python
@ -236,6 +237,64 @@ The available options are:
which are converted to Swagger ``Schema`` attribute names according to :func:`.make_swagger_name`. which are converted to Swagger ``Schema`` attribute names according to :func:`.make_swagger_name`.
Attribute names and values must conform to the `OpenAPI 2.0 specification <https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#schemaObject>`_. Attribute names and values must conform to the `OpenAPI 2.0 specification <https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#schemaObject>`_.
Suppose you wanted to model an email using a `JSONField` to store the subject and body for performance reasons:
.. code-block:: python
from django.contrib.postgres.fields import JSONField
class Email(models.Model):
# Store data as JSON, but the data should be made up of
# an object that has two properties, "subject" and "body"
# Example:
# {
# "subject": "My Title",
# "body": "The body of the message.",
# }
message = JSONField()
To instruct ``drf-yasg`` to output an OpenAPI schema that matches this, create a custom ``JSONField``:
.. code-block:: python
class EmailMessageField(serializers.JSONField):
class Meta:
swagger_schema_fields = {
"type": openapi.TYPE_OBJECT,
"title": "Email",
"properties": {
"subject": openapi.Schema(
title="Email subject",
type=openapi.TYPE_STRING,
),
"body": openapi.Schema(
title="Email body",
type=openapi.TYPE_STRING,
),
},
"required": ["subject", "body"],
}
class EmailSerializer(ModelSerializer):
class Meta:
model = Email
fields = "__all__"
message = EmailMessageField()
.. Warning::
Overriding a default ``Field`` generated by a ``ModelSerializer`` will also override automatically
generated validators for that ``Field``. To add ``Serializer`` validation back in manually, see the relevant
`DRF Validators`_ and `DRF Fields`_ documentation.
One example way to do this is to set the ``default_validators`` attribute on a field.
.. code-block:: python
class EmailMessageField(serializers.JSONField):
default_validators = [my_custom_email_validator]
...
************************* *************************
Subclassing and extending Subclassing and extending
@ -389,3 +448,5 @@ A second example, of a :class:`~.inspectors.FieldInspector` that removes the ``t
.. _Python 3 type hinting: https://docs.python.org/3/library/typing.html .. _Python 3 type hinting: https://docs.python.org/3/library/typing.html
.. _DRF Validators: https://www.django-rest-framework.org/api-guide/validators/
.. _DRF Fields: https://www.django-rest-framework.org/api-guide/fields/#validators

View File

@ -4,3 +4,4 @@
-r lint.txt -r lint.txt
tox-battery>=0.5 tox-battery>=0.5
django-oauth-toolkit

View File

@ -2,9 +2,8 @@
Pillow>=4.3.0 Pillow>=4.3.0
django-filter>=1.1.0,<2.0; python_version == "2.7" django-filter>=1.1.0,<2.0; python_version == "2.7"
django-filter>=1.1.0; python_version >= "3.5" django-filter>=1.1.0; python_version >= "3.5"
#djangorestframework-camel-case>=0.2.0 djangorestframework-camel-case>=1.1.2
# tempory replacement of broken lib
-e git+https://github.com/tfranzel/djangorestframework-camel-case.git@bd556d38fa7382acadfe91d93d92d99c663248a9#egg=djangorestframework_camel_case
djangorestframework-recursive>=0.1.2 djangorestframework-recursive>=0.1.2
dj-database-url>=0.4.2 dj-database-url>=0.4.2
user_agents>=1.1.0 user_agents>=1.1.0
django-cors-headers

View File

@ -19,7 +19,7 @@ with io.open('README.rst', encoding='utf-8') as readme:
requirements = read_req('base.txt') requirements = read_req('base.txt')
requirements_validation = read_req('validation.txt') requirements_validation = read_req('validation.txt')
py3_supported_range = (5, 7) py3_supported_range = (5, 8)
# convert inclusive range to exclusive range # convert inclusive range to exclusive range
py3_supported_range = (py3_supported_range[0], py3_supported_range[1] + 1) py3_supported_range = (py3_supported_range[0], py3_supported_range[1] + 1)

View File

@ -1,4 +1,4 @@
from six import raise_from from six import binary_type, raise_from, text_type
import copy import copy
import json import json
@ -176,7 +176,14 @@ class SaneYamlDumper(yaml.SafeDumper):
node.flow_style = best_style node.flow_style = best_style
return node return node
def represent_text(self, text):
if "\n" in text:
return self.represent_scalar('tag:yaml.org,2002:str', text, style='|')
return self.represent_scalar('tag:yaml.org,2002:str', text)
SaneYamlDumper.add_representer(binary_type, SaneYamlDumper.represent_text)
SaneYamlDumper.add_representer(text_type, SaneYamlDumper.represent_text)
SaneYamlDumper.add_representer(OrderedDict, SaneYamlDumper.represent_odict) SaneYamlDumper.add_representer(OrderedDict, SaneYamlDumper.represent_odict)
SaneYamlDumper.add_multi_representer(OrderedDict, SaneYamlDumper.represent_odict) SaneYamlDumper.add_multi_representer(OrderedDict, SaneYamlDumper.represent_odict)

View File

@ -382,8 +382,21 @@ def find_limits(field):
def decimal_field_type(field): def decimal_field_type(field):
return openapi.TYPE_NUMBER if decimal_as_float(field) else openapi.TYPE_STRING return openapi.TYPE_NUMBER if decimal_as_float(field) else openapi.TYPE_STRING
def recurse_one_to_one(field, visited_set=None):
if visited_set is None:
visited_set = set()
if field in visited_set:
return None #cycle?
if isinstance(field, models.OneToOneField):
tgt = field.target_field
visited_set.add(field)
return recurse_one_to_one(tgt, visited_set=visited_set)
else:
tmp = get_basic_type_info(field)
return tmp['type']
model_field_to_basic_type = [ model_field_to_basic_type = [
(models.OneToOneField, (recurse_one_to_one, None)),
(models.AutoField, (openapi.TYPE_INTEGER, None)), (models.AutoField, (openapi.TYPE_INTEGER, None)),
(models.BinaryField, (openapi.TYPE_STRING, openapi.FORMAT_BINARY)), (models.BinaryField, (openapi.TYPE_STRING, openapi.FORMAT_BINARY)),
(models.BooleanField, (openapi.TYPE_BOOLEAN, None)), (models.BooleanField, (openapi.TYPE_BOOLEAN, None)),

View File

@ -131,7 +131,7 @@ class Command(BaseCommand):
if user: if user:
# Only call get_user_model if --user was passed in order to # Only call get_user_model if --user was passed in order to
# avoid crashing if auth is not configured in the project # avoid crashing if auth is not configured in the project
user = get_user_model().objects.get(username=user) user = get_user_model().objects.get(**{get_user_model().USERNAME_FIELD: user})
mock = mock or private or (user is not None) or (api_version is not None) mock = mock or private or (user is not None) or (api_version is not None)
if mock and not api_url: if mock and not api_url:

View File

@ -470,7 +470,7 @@ class Schema(SwaggerDict):
:type properties: dict[str,Schema or SchemaRef] :type properties: dict[str,Schema or SchemaRef]
:param additional_properties: allow wildcard properties not listed in `properties` :param additional_properties: allow wildcard properties not listed in `properties`
:type additional_properties: bool or Schema or SchemaRef :type additional_properties: bool or Schema or SchemaRef
:param list[str] required: list of requried property names :param list[str] required: list of required property names
:param items: type of array items, only valid if `type` is ``array`` :param items: type of array items, only valid if `type` is ``array``
:type items: Schema or SchemaRef :type items: Schema or SchemaRef
:param default: only valid when insider another ``Schema``\\ 's ``properties``; :param default: only valid when insider another ``Schema``\\ 's ``properties``;

View File

@ -1,6 +1,7 @@
import inspect import inspect
import logging import logging
import sys import sys
import textwrap
from collections import OrderedDict from collections import OrderedDict
from decimal import Decimal from decimal import Decimal
@ -95,8 +96,8 @@ def swagger_auto_schema(method=None, methods=None, auto_schema=unset, request_bo
* a ``Serializer`` class or instance will be converted into a :class:`.Schema` and treated as above * a ``Serializer`` class or instance will be converted into a :class:`.Schema` and treated as above
* a :class:`.Response` object will be used as-is; however if its ``schema`` attribute is a ``Serializer``, * a :class:`.Response` object will be used as-is; however if its ``schema`` attribute is a ``Serializer``,
it will automatically be converted into a :class:`.Schema` it will automatically be converted into a :class:`.Schema`
:type responses: dict[str,(drf_yasg.openapi.Schema or drf_yasg.openapi.SchemaRef or drf_yasg.openapi.Response or :type responses: dict[int or str, (drf_yasg.openapi.Schema or drf_yasg.openapi.SchemaRef or
str or rest_framework.serializers.Serializer)] drf_yasg.openapi.Response or str or rest_framework.serializers.Serializer)]
:param list[type[drf_yasg.inspectors.FieldInspector]] field_inspectors: extra serializer and field inspectors; these :param list[type[drf_yasg.inspectors.FieldInspector]] field_inspectors: extra serializer and field inspectors; these
will be tried before :attr:`.ViewInspector.field_inspectors` on the :class:`.inspectors.SwaggerAutoSchema` will be tried before :attr:`.ViewInspector.field_inspectors` on the :class:`.inspectors.SwaggerAutoSchema`
@ -374,10 +375,16 @@ def get_consumes(parser_classes):
parser_classes = [pc for pc in parser_classes if not issubclass(pc, FileUploadParser)] parser_classes = [pc for pc in parser_classes if not issubclass(pc, FileUploadParser)]
media_types = [parser.media_type for parser in parser_classes or []] media_types = [parser.media_type for parser in parser_classes or []]
non_form_media_types = [encoding for encoding in media_types if not is_form_media_type(encoding)] non_form_media_types = [encoding for encoding in media_types if not is_form_media_type(encoding)]
# Because swagger Parameter objects don't support complex data types (nested objects, arrays),
# we can't use those unless we are sure the view *only* accepts form data
# This means that a view won't support file upload in swagger unless it explicitly
# sets its parser classes to include only form parsers
if len(non_form_media_types) == 0: if len(non_form_media_types) == 0:
return media_types return media_types
else:
return non_form_media_types # If the form accepts both form data and another type, like json (which is the default config),
# we will render its input as a Schema and thus it file parameters will be read-only
return non_form_media_types
def get_produces(renderer_classes): def get_produces(renderer_classes):
@ -438,6 +445,9 @@ def force_real_str(s, encoding='utf-8', strings_only=False, errors='strict'):
if type(s) != str: if type(s) != str:
s = '' + s s = '' + s
# Remove common indentation to get the correct Markdown rendering
s = textwrap.dedent(s)
return s return s
@ -473,7 +483,10 @@ def get_field_default(field):
try: try:
if hasattr(default, 'set_context'): if hasattr(default, 'set_context'):
default.set_context(field) default.set_context(field)
default = default() if getattr(default, 'requires_context', False):
default = default(field)
else:
default = default()
except Exception: # pragma: no cover except Exception: # pragma: no cover
logger.warning("default for %s is callable but it raised an exception when " logger.warning("default for %s is callable but it raised an exception when "
"called; 'default' will not be set on schema", field, exc_info=True) "called; 'default' will not be set on schema", field, exc_info=True)

View File

@ -1,4 +1,4 @@
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import gettext_lazy as _
from rest_framework import serializers from rest_framework import serializers
from articles.models import Article, ArticleGroup from articles.models import Article, ArticleGroup

View File

@ -193,16 +193,6 @@ LOGGING = {
'propagate': False, 'propagate': False,
}, },
'django': { 'django': {
'handlers': ['console_log'],
'level': 'DEBUG',
'propagate': False,
},
'django.db.backends': {
'handlers': ['console_log'],
'level': 'INFO',
'propagate': False,
},
'django.template': {
'handlers': ['console_log'], 'handlers': ['console_log'],
'level': 'INFO', 'level': 'INFO',
'propagate': False, 'propagate': False,

View File

@ -13,9 +13,9 @@ swagger_info = openapi.Info(
default_version='v1', default_version='v1',
description="""This is a demo project for the [drf-yasg](https://github.com/axnsan12/drf-yasg) Django Rest Framework library. description="""This is a demo project for the [drf-yasg](https://github.com/axnsan12/drf-yasg) Django Rest Framework library.
The `swagger-ui` view can be found [here](/cached/swagger). The `swagger-ui` view can be found [here](/cached/swagger).
The `ReDoc` view can be found [here](/cached/redoc). The `ReDoc` view can be found [here](/cached/redoc).
The swagger YAML document can be found [here](/cached/swagger.yaml). The swagger YAML document can be found [here](/cached/swagger.yaml).
You can log in using the pre-existing `admin` user with password `passwordadmin`.""", # noqa You can log in using the pre-existing `admin` user with password `passwordadmin`.""", # noqa
terms_of_service="https://www.google.com/policies/terms/", terms_of_service="https://www.google.com/policies/terms/",

View File

@ -3,7 +3,7 @@ import sys
from django.conf import settings from django.conf import settings
from django.contrib.auth.hashers import make_password from django.contrib.auth.hashers import make_password
from django.db import migrations, IntegrityError from django.db import migrations, IntegrityError, transaction
def add_default_user(apps, schema_editor): def add_default_user(apps, schema_editor):
@ -13,14 +13,15 @@ def add_default_user(apps, schema_editor):
User = apps.get_model(settings.AUTH_USER_MODEL) User = apps.get_model(settings.AUTH_USER_MODEL)
try: try:
admin = User( with transaction.atomic():
username=username, admin = User(
email=email, username=username,
password=make_password(password), email=email,
is_superuser=True, password=make_password(password),
is_staff=True is_superuser=True,
) is_staff=True
admin.save() )
admin.save()
except IntegrityError: except IntegrityError:
sys.stdout.write(" User '%s <%s>' already exists..." % (username, email)) sys.stdout.write(" User '%s <%s>' already exists..." % (username, email))
else: else:

View File

@ -1,11 +1,14 @@
swagger: '2.0' swagger: '2.0'
info: info:
title: Snippets API title: Snippets API
description: "This is a demo project for the [drf-yasg](https://github.com/axnsan12/drf-yasg)\ description: |-
\ Django Rest Framework library.\n\nThe `swagger-ui` view can be found [here](/cached/swagger).\ This is a demo project for the [drf-yasg](https://github.com/axnsan12/drf-yasg) Django Rest Framework library.
\ \nThe `ReDoc` view can be found [here](/cached/redoc). \nThe swagger YAML\
\ document can be found [here](/cached/swagger.yaml). \n\nYou can log in using\ The `swagger-ui` view can be found [here](/cached/swagger).
\ the pre-existing `admin` user with password `passwordadmin`." The `ReDoc` view can be found [here](/cached/redoc).
The swagger YAML document can be found [here](/cached/swagger.yaml).
You can log in using the pre-existing `admin` user with password `passwordadmin`.
termsOfService: https://www.google.com/policies/terms/ termsOfService: https://www.google.com/policies/terms/
contact: contact:
email: contact@snippets.local email: contact@snippets.local
@ -1502,7 +1505,9 @@ definitions:
readOnly: true readOnly: true
help_text_example_3: help_text_example_3:
title: Help text example 3 title: Help text example 3
description: "\n docstring is set so should appear in swagger as fallback\n\ description: |2
\ :return:\n "
docstring is set so should appear in swagger as fallback
:return:
type: integer type: integer
readOnly: true readOnly: true

View File

@ -7,11 +7,11 @@ from django.conf.urls import url
from django.contrib.postgres import fields as postgres_fields from django.contrib.postgres import fields as postgres_fields
from django.db import models from django.db import models
from django.utils.inspect import get_func_args from django.utils.inspect import get_func_args
from django_fake_model import models as fake_models
from rest_framework import routers, serializers, viewsets from rest_framework import routers, serializers, viewsets
from rest_framework.decorators import api_view from rest_framework.decorators import api_view
from rest_framework.response import Response from rest_framework.response import Response
from django_fake_model import models as fake_models
from drf_yasg import codecs, openapi from drf_yasg import codecs, openapi
from drf_yasg.codecs import yaml_sane_load from drf_yasg.codecs import yaml_sane_load
from drf_yasg.errors import SwaggerGenerationError from drf_yasg.errors import SwaggerGenerationError
@ -334,3 +334,21 @@ def test_optional_return_type(py_type, expected_type):
swagger = generator.get_schema(None, True) swagger = generator.get_schema(None, True)
property_schema = swagger["definitions"]["OptionalMethod"]["properties"]["x"] property_schema = swagger["definitions"]["OptionalMethod"]["properties"]["x"]
assert property_schema == openapi.Schema(title='X', type=expected_type, readOnly=True) assert property_schema == openapi.Schema(title='X', type=expected_type, readOnly=True)
EXPECTED_DESCRIPTION = """\
description: |-
This is a demo project for the [drf-yasg](https://github.com/axnsan12/drf-yasg) Django Rest Framework library.
The `swagger-ui` view can be found [here](/cached/swagger).
The `ReDoc` view can be found [here](/cached/redoc).
The swagger YAML document can be found [here](/cached/swagger.yaml).
You can log in using the pre-existing `admin` user with password `passwordadmin`.
"""
def test_multiline_strings(call_generate_swagger):
output = call_generate_swagger(format='yaml')
print("|\n|".join(output.splitlines()[:20]))
assert EXPECTED_DESCRIPTION in output

16
tox.ini
View File

@ -5,11 +5,9 @@ isolated_build_env = .package
# https://docs.djangoproject.com/en/dev/faq/install/#what-python-version-can-i-use-with-django # https://docs.djangoproject.com/en/dev/faq/install/#what-python-version-can-i-use-with-django
envlist = envlist =
py27-django111-drf39-typing, py36-django{111,22}-drf{38,39},
py27-django111-drf{38,39}, py37-django22-drf{38,39,310,311},
py{35,36}-django{111,21,22}-drf{38,39}, py38-django{22,3}-drf{310,311},
py37-django{21,22}-drf{38,39,310},
py38-django22-drf310,
djmaster, lint, docs djmaster, lint, docs
skip_missing_interpreters = true skip_missing_interpreters = true
@ -20,21 +18,21 @@ deps =
[testenv] [testenv]
deps = deps =
django111: Django>=1.11,<2.0 django111: Django>=1.11,<2.0
django111: django-cors-headers>=2.1.0
django111: django-oauth-toolkit>=1.1.0,<1.2.0 django111: django-oauth-toolkit>=1.1.0,<1.2.0
django21: Django>=2.1,<2.2 django21: Django>=2.1,<2.2
django21: django-cors-headers>=2.1.0
django21: django-oauth-toolkit>=1.2.0 django21: django-oauth-toolkit>=1.2.0
django22: Django>=2.2,<2.3 django22: Django>=2.2,<2.3
django22: django-cors-headers>=2.1.0
django22: django-oauth-toolkit>=1.2.0 django22: django-oauth-toolkit>=1.2.0
django3: Django>=2.2,<2.3
django3: django-oauth-toolkit>=1.2.0
drf38: djangorestframework>=3.8,<3.9 drf38: djangorestframework>=3.8,<3.9
drf39: djangorestframework>=3.9,<3.10 drf39: djangorestframework>=3.9,<3.10
drf310: djangorestframework>=3.10 drf310: djangorestframework>=3.10,<3.11
drf311: djangorestframework>=3.11,<3.12
typing: typing>=3.6.6 typing: typing>=3.6.6