Compare commits

..

111 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
Cristi Vîjdea 13311582ea Add 1.17.0 changelog
Closes #412
2019-10-03 02:02:02 +03:00
Aliaksei Urbanski 9a89d8ccb0 Improve testing (#415)
These changes:

 - Fix the lint and the djmaster jobs
 - Fix compatibility with upcoming Django 3.0
 - Replace jobs with matrix in .travis.yml
 - Add a test job for Python 3.8
 - Allow running tests on any branch
2019-10-03 01:38:02 +03:00
Cristi Vîjdea 16f67cd8c2 Fix CHANGELOG.rst syntax
Fixes #425
2019-09-29 19:15:42 +03:00
Cristi Vîjdea 99fa7c25ca Add missing newline for #444 2019-09-29 19:08:37 +03:00
grumbling-tom 97e70d9d16 custom_spec.rst: Update to indicate swagger_auto_schema import location. (#444) 2019-09-29 19:07:08 +03:00
Myungseo Kang ee086a6eec Fix typo in docs (#460) 2019-09-29 19:03:05 +03:00
johnthagen 4af38c970a Fix typo (#456) 2019-09-29 19:02:50 +03:00
Ilya Stepin 95337f85ad custom_spec: fix typo in docs (#447) 2019-09-29 19:02:37 +03:00
Étienne Noss 1352c2a23b fix Optional typing hint for SerializerMethodField (#428) 2019-09-29 19:01:28 +03:00
Cristi Vîjdea 8578b93eba Update swagger-ui to 3.23.11 and ReDoc to 2.0.0-rc.14
Supersedes #434
Fixes #398
2019-09-29 18:57:54 +03:00
johnthagen 212891b1b8 Update README to support DRF 3.10 (#424) 2019-09-29 18:51:54 +03:00
Étienne Noss ab6444a32e inspectors: add support for JSONField (#417) 2019-07-19 14:13:26 +03:00
Cristi Vijdea 2e0f9a19a9 Update swagger-ui to 3.23.1 2019-07-16 21:22:44 +03:00
Cristi Vijdea cda808fe11 Fix isort errors 2019-07-16 21:20:46 +03:00
Cristi Vijdea e6219ab8b7 Add packaging to requirements 2019-07-16 21:19:19 +03:00
Cristi Vijdea bc931677dc Add 1.16.1 changelog 2019-07-16 20:59:36 +03:00
tfranzel 1904b0499e Fix imports for DRF 3.10 (#408)
Fixes #410
Fixes #411
2019-07-16 20:39:44 +03:00
johnthagen 1e380fe68b Fix variable formatting (#406) 2019-07-15 12:14:37 +03:00
Étienne Noss 6417bb3770 Handle enum type for nested ChoiceFields (#400) 2019-07-15 12:14:23 +03:00
Cristi Vîjdea e9f27442fc Remove unused import 2019-06-13 16:58:41 +03:00
Cristi Vîjdea cf8b912c10 Align tox matrix to python version 2019-06-13 16:57:15 +03:00
Cristi Vîjdea 3a37c4a019 Drop Django 2.0 and DRF 3.7 support 2019-06-13 16:49:46 +03:00
Cristi Vîjdea 8acab171ea Fix isort
[ci skip]
2019-06-13 16:43:16 +03:00
Cristi Vîjdea acc204e4ea Add test for #382 2019-06-13 16:34:23 +03:00
Cristi Vîjdea 1635e5e095 Add 1.16.0 changelog 2019-06-13 13:48:45 +03:00
Cristi Vîjdea 753be1a8bd Fix potential issubclass crash 2019-06-13 13:43:16 +03:00
Pavel White 2656696a0f generators: support custom ReferenceResolver class (#350) 2019-06-13 03:38:17 +03:00
Hugo Duroux a083d3cf7c Add operation_keys to SwaggerAutoSchema constructor (#355) 2019-06-13 03:37:06 +03:00
Cristi Vîjdea eed8a8d3ec Add 1.15.1 changelog 2019-06-13 03:23:25 +03:00
Cristi Vîjdea d04f27f40f Add test for serializer field label
Closes #353.
2019-06-13 03:15:24 +03:00
Cristi Vîjdea db154d196a Drop Python 3.4 support 2019-06-13 02:37:04 +03:00
Cristi Vîjdea 60e1346150 Refactor setup.py python_requires 2019-06-13 02:36:43 +03:00
Cristi Vîjdea 69b628a7af Fix python 3.7 typing
Closes #371
2019-06-13 02:26:36 +03:00
Cristi Vîjdea 64d9d42aa9 Remove pygments test dependency 2019-06-13 01:54:22 +03:00
Cristi Vîjdea e9d5344de3 Update swagger-ui to 3.22.3 and ReDoc to 2.0.0-rc.8-1 2019-06-13 01:28:31 +03:00
elliott-omosheye b5aba7243d Fix unicode attribute error if typing installed on py2.7 (#363) 2019-06-13 00:51:47 +03:00
Terence Honles 91ef83e830 update reference.yaml based on pygments lexers & styles changes (#364)
Snippet model in testproj/snippets/models.py has field choices which depend
on ``pygments.lexers`` and ``pygments.styles``
2019-06-13 00:51:23 +03:00
raksa 0991c806c7 Fixed typo (#361) 2019-05-16 00:10:12 +03:00
johnthagen 59e86ff72f Support Django 2.2 (#346) 2019-04-05 23:32:33 +03:00
Cristi Vijdea 548489a539 Add missing migration for test 2019-04-01 03:29:10 +03:00
Cristi Vijdea 887b53300a Add 1.15.0 changelog 2019-04-01 03:29:03 +03:00
Joe Korbel f692fe7c98 Update openapi.rst (#327)
Minor typo on "because"
2019-04-01 03:21:16 +03:00
Cristi Vijdea 017ae3d240 Update swagger-ui to 3.22.0 and ReDoc to 2.0.0-rc.4 2019-04-01 03:19:26 +03:00
Cristi Vijdea b57413023b Fix isort 2019-04-01 02:58:42 +03:00
Cristi Vijdea 4014c69689 Add test for #340 2019-04-01 02:58:07 +03:00
Cristi Vijdea 7bb4700003 Apply to_representation on choices
Fixes #340
2019-04-01 02:58:07 +03:00
Dimas Ari 86c1675c58 make sure all ListModelMixin views considered as list view (#306)
* make sure all ListModelMixin views considered as list view
* test to make sure all ListModelMixin views considered as list view added
2019-04-01 02:57:14 +03:00
AndreaFox93 81f0b1a2ea Use getattr for 'help_text' (#342) 2019-04-01 02:24:00 +03:00
Cristi Vijdea 298a9745df Add `is_list_view` and `has_list_response` methods 2019-03-22 14:27:15 +02:00
Cristi Vijdea 652a33a54d Fix lint errors 2019-03-04 00:49:28 +02:00
Cristi Vijdea 340a60324c Add special exclusion for FileUploadParser
Fixes #288
2019-03-04 00:48:55 +02:00
Cristi Vijdea f348084d85 Fix Python 2.7 compat 2019-03-04 00:46:00 +02:00
Cristi Vijdea 904c43a167 Add 1.14.0 changelog 2019-03-04 00:08:19 +02:00
Cristi Vijdea 4c78a683f4 Remove Background section from readme 2019-03-03 23:59:12 +02:00
Cristi Vijdea 4da09830ac Add BMaC button (#323) 2019-03-03 23:59:05 +02:00
Cristi Vijdea 34ed1e20a2 Update swagger-ui to 3.21.0 2019-03-03 18:08:25 +02:00
Cristi Vijdea 62d97a80bc Fix _serializer pickle bugs 2019-03-03 17:23:04 +02:00
Cristi Vijdea e108ddbb48 Add force_init argument to ReferenceResolver constructor
Closes #325
2019-03-03 16:40:19 +02:00
Cristi Vijdea 75a5d866be Run makemigrations 2019-03-02 05:51:15 +02:00
Cristi Vijdea f189426901 Update swagger-ui to 3.20.9 2019-03-02 05:42:39 +02:00
Cristi Vijdea b4900ebd6a Add tests for #310 2019-03-02 05:35:10 +02:00
Cristi Vijdea c593b3fcfb Remove _summary_and_description_compat 2019-02-27 21:51:01 +02:00
Cristi Vijdea 9caeed781e Fix duplicate ref_name detection and promote to error 2019-02-27 21:47:31 +02:00
Cristi Vijdea 3377ef08ea Fix Schema limits for ListField and ListSerializer
Fixes #324
2019-02-27 21:23:25 +02:00
Daniel Hahler 5c2c39c82d Travis: use Ubuntu Xenial (#302)
Closes #302
2019-02-27 21:23:09 +02:00
Roman Sichny e538e0713a Support RecursiveField with 'many' option (#321) 2019-02-25 20:59:35 +02:00
Roman Sichny 76c8fe0646 Fix union type hint checks (#318)
Fix some obscure edge cases related to typing.Union type args.

Fixes #304.
2019-02-22 01:00:14 +02:00
Paul Wayper 3d43ee6748 Fix `items` field conflict with dict.items() (#308)
Closes #307
2019-02-22 00:57:52 +02:00
Cristi Vijdea 583e404ed8 Add xenial hacks back
Travis is still retarded
2019-01-29 09:43:09 +02:00
Cristi Vijdea d62243599b Remove xenial incantations 2019-01-29 09:28:22 +02:00
Cristi Vijdea 6df3523675 Use python 3.7 for lint and Django master tests 2019-01-29 09:26:56 +02:00
Cristi Vijdea d2bd838325 Add 1.13.0 changelog 2019-01-29 09:11:28 +02:00
Cristi Vijdea bacab20f0b Update swagger-ui to 3.20.5 and ReDoc to 2.0.0-rc.2 2019-01-29 09:05:26 +02:00
Cristi Vijdea df82fe59d7 Remove recommandations for unmaintained flex library
Fixes #285
2019-01-29 09:04:10 +02:00
Cristi Vijdea 7c5a0b7176 Promote deprecation warning to exception 2019-01-29 08:51:02 +02:00
Cristi Vijdea 69a1e62ed3 Add x-nullable to Optional 2019-01-29 08:46:33 +02:00
Alexander Egorov 3806d6efd5 Add support for custom and collection type hint classes (#272) 2019-01-29 08:39:29 +02:00
Dima Boger 58e6dae548 Fix typo with quotes in security doc (#300) 2019-01-28 23:14:33 +02:00
Cristi Vîjdea 8e2228fe5f Fix py37 test 2019-01-14 14:58:16 +02:00
Cristi Vîjdea 762467285c Keep dict key order for Python 3.7 2019-01-14 14:25:10 +02:00
Cristi Vîjdea 0e62fd6f2b Update copyright year 2019-01-14 14:25:10 +02:00
Daniel Hahler e266eeda60 Fi doc typo (#286) 2019-01-08 12:38:08 +02:00
Cristi Vîjdea 4b1098369c Add check to prevent build of docs with bad version 2019-01-03 20:36:12 +02:00
Cristi Vîjdea bda545e85f Inline coverage.sh 2018-12-30 16:14:05 +02:00
Cristi Vîjdea c1d3d4fe3c Add .readthedocs.yml 2018-12-29 17:44:29 +02:00
Cristi Vîjdea 7f3ffe80a9 Use call_view_method for get_serializer_class 2018-12-29 17:42:05 +02:00
Cristi Vîjdea 1fe8c2c03c Add 1.12.1 changelog 2018-12-28 16:26:17 +02:00
Cristi Vîjdea b8512bda8e Fix call_view_method warnings for view classes 2018-12-28 16:20:37 +02:00
Cristi Vîjdea 2bc9addc99 Fix crash with empty generator class name 2018-12-28 09:34:54 +02:00
Cristi Vîjdea 6df2362156 Use DEFAULT_VERSION in management command 2018-12-28 01:37:17 +02:00
Cristi Vîjdea e5a569ebf7 Add extension points to management command 2018-12-28 01:37:13 +02:00
Cristi Vîjdea 470c993b98 Update ReDoc to 2.0.0-rc.0 2018-12-25 18:23:19 +02:00
johnthagen a5e4386f38 Fix monospace formatting typo (#283) 2018-12-24 20:34:12 +02:00
johnthagen 6bd91faa5d Fix minor spelling typo (#282) 2018-12-24 20:33:24 +02:00
Cristi Vîjdea 667c9c1002 Add supported versions note 2018-12-24 07:57:50 +02:00
Cristi Vîjdea 8b0da2607f Add pretty and media_type options to codecs 2018-12-24 07:55:17 +02:00
69 changed files with 2290 additions and 1016 deletions

1
.gitignore vendored
View File

@ -3,6 +3,7 @@ testproj/db.sqlite3
testproj/staticfiles
\.pytest_cache/
docs/\.doctrees/
pip-wheel-metadata/
# Created by .ignore support plugin (hsz.mobi)
### Python template

View File

@ -19,6 +19,7 @@
<excludeFolder url="file://$MODULE_DIR$/.cache" />
<excludeFolder url="file://$MODULE_DIR$/.eggs" />
<excludeFolder url="file://$MODULE_DIR$/.pytest_cache" />
<excludeFolder url="file://$MODULE_DIR$/.tox" />
<excludeFolder url="file://$MODULE_DIR$/build" />
<excludeFolder url="file://$MODULE_DIR$/dist" />
<excludeFolder url="file://$MODULE_DIR$/docs/.doctrees" />

15
.readthedocs.yml 100644
View File

@ -0,0 +1,15 @@
requirements_file: requirements/docs.txt
build:
image: latest
python:
version: 3.6
setup_py_install: false
pip_install: true # need this for correct pyproject.toml handling
extra_requirements:
- validation
# extra formats in addition to the default HTML web docs
formats:
- pdf

View File

@ -1,26 +1,20 @@
language: python
sudo: false
python:
- '2.7'
- '3.4'
- '3.5'
- '3.6'
- '3.7'
- '3.8'
dist: xenial
cache: pip
jobs:
matrix:
include:
- # workaround for python 3.7 on travis https://github.com/travis-ci/travis-ci/issues/9815#issuecomment-401756442
stage: test
python: '3.7'
dist: xenial
sudo: required
- python: '3.6'
env: TOXENV=djmaster
- # readthedocs uses python 3.5 for building
python: '3.5'
env: TOXENV=docs
- python: '3.6'
- python: '3.7'
env: TOXENV=djmaster
- python: '3.7'
env: TOXENV=lint
- stage: publish
@ -59,22 +53,16 @@ script:
after_success:
- |
if [[ -z "$TOXENV" && -z "$PYPI_DEPLOY" ]]; then
chmod +x coverage.sh
./coverage.sh
coverage combine || true
coverage report
codecov
fi
branches:
only:
- master
- /^release\/.*$/
- /^v?\d+\.\d+(\.\d+)?(-?\S+)?$/
stages:
- test
- name: publish
if: tag IS present
notifications:
email:
on_success: always

View File

@ -6,7 +6,7 @@
Contributing
############
Contributions are always welcome and appreciated! Here are some ways you can contribut.
Contributions are always welcome and appreciated! Here are some ways you can contribute.
******
Issues
@ -57,7 +57,7 @@ You want to contribute some code? Great! Here are a few steps to get you started
.. 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
``reference.yaml`` together with your changes.
@ -95,7 +95,7 @@ You want to contribute some code? Great! Here are a few steps to get you started
#. **Your code must pass all the required travis jobs before it is merged**
As of now, this consists of running on Python 2.7, 3.4, 3.5 and 3.6, and building the docs succesfully.
As of now, this consists of running on Python 2.7, 3.5, 3.6 and 3.7, and building the docs succesfully.
******************
Maintainer's notes

View File

@ -10,7 +10,7 @@ License
BSD 3-Clause License
********************
Copyright (c) 2018, Cristian V. <cristi@cvjd.me> |br|\ All rights reserved.
Copyright (c) 2017 - 2019, Cristian V. <cristi@cvjd.me> |br|\ All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:

View File

@ -7,13 +7,22 @@ drf-yasg - Yet another Swagger generator
|travis| |nbsp| |codecov| |nbsp| |rtd-badge| |nbsp| |pypi-version|
|bmac-button|
Generate **real** Swagger/OpenAPI 2.0 specifications from a Django Rest Framework API.
Compatible with
- **Django Rest Framework**: 3.7.7, 3.8
- **Django**: 1.11, 2.0, 2.1
- **Python**: 2.7, 3.4, 3.5, 3.6, 3.7
- **Django Rest Framework**: 3.8, 3.9, 3.10, 3.11
- **Django**: 1.11, 2.2, 3.0
- **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 version of drf-yasg is supported.** Support of old versions is dropped immediately with the release
of a new version. Please do not create issues before upgrading to the latest release available at the time. Regression
reports are accepted and will be resolved with a new release as quickly as possible. Removed features will usually go
through a deprecation cycle of a few minor releases.
Resources:
@ -22,9 +31,7 @@ Resources:
* **Changelog**: https://drf-yasg.readthedocs.io/en/stable/changelog.html
* **Live demo**: https://drf-yasg-demo.herokuapp.com/
.. image:: https://www.herokucdn.com/deploy/button.svg
:target: https://heroku.com/deploy?template=https://github.com/axnsan12/drf-yasg
:alt: heroku deploy button
|heroku-button|
********
Features
@ -40,8 +47,7 @@ Features
`redoc <https://github.com/Rebilly/ReDoc>`_ for viewing the generated documentation
- schema view is cacheable out of the box
- generated Swagger schema can be automatically validated by
`swagger-spec-validator <https://github.com/Yelp/swagger_spec_validator>`_ or
`flex <https://github.com/pipermerriam/flex>`_
`swagger-spec-validator <https://github.com/Yelp/swagger_spec_validator>`_
- supports Django REST Framework API versioning with ``URLPathVersioning`` and ``NamespaceVersioning``; other DRF
or custom versioning schemes are not currently supported
@ -159,7 +165,7 @@ a. ``get_schema_view`` parameters
- ``patterns`` - passed to SchemaGenerator
- ``urlconf`` - passed to SchemaGenerator
- ``public`` - if False, includes only endpoints the current user has access to
- ``validators`` - a list of validator names to apply on the generated schema; allowed values are ``flex``, ``ssv``
- ``validators`` - a list of validator names to apply on the generated schema; only ``ssv`` is currently supported
- ``generator_class`` - schema generator class to use; should be a subclass of ``OpenAPISchemaGenerator``
- ``authentication_classes`` - authentication classes for the schema view itself
- ``permission_classes`` - permission classes for the schema view itself
@ -204,9 +210,9 @@ caching the schema view in-memory, with some sane defaults:
4. Validation
=============
Given the numerous methods to manually customzie the generated schema, it makes sense to validate the result to ensure
Given the numerous methods to manually customize the generated schema, it makes sense to validate the result to ensure
it still conforms to OpenAPI 2.0. To this end, validation is provided at the generation point using python swagger
libraries, and can be activated by passing :python:`validators=['flex', 'ssv']` to ``get_schema_view``; if the generated
libraries, and can be activated by passing :python:`validators=['ssv']` to ``get_schema_view``; if the generated
schema is not valid, a :python:`SwaggerValidationError` is raised by the handling codec.
**Warning:** This internal validation can slow down your server.
@ -229,7 +235,7 @@ Offline
^^^^^^^
If your schema is not accessible from the internet, you can run a local copy of
`swagger-validator <https://hub.docker.com/r/swaggerapi/swagger-validator/>`_ and set the `VALIDATOR_URL` accordingly:
`swagger-validator <https://hub.docker.com/r/swaggerapi/swagger-validator/>`_ and set the ``VALIDATOR_URL`` accordingly:
.. code:: python
@ -300,45 +306,6 @@ For additional usage examples, you can take a look at the test project in the ``
(venv) $ python manage.py runserver
(venv) $ firefox localhost:8000/swagger/
**********
Background
**********
``OpenAPI 2.0``/``Swagger`` is a format designed to encode information about a Web API into an easily parsable schema
that can then be used for rendering documentation, generating code, etc.
More details are available on `swagger.io <https://swagger.io/>`__ and on the `OpenAPI 2.0 specification
page <https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md>`__.
From here on, the terms “OpenAPI” and “Swagger” are used interchangeably.
Swagger in Django Rest Framework
================================
Since Django Rest Framework 3.7, there is now `built in support <http://www.django-rest-framework.org/api-guide/schemas/>`__
for automatic OpenAPI 2.0 schema generation. However, this generation is based on the `coreapi <http://www.coreapi.org/>`__
standard, which for the moment is vastly inferior to OpenAPI in both features and tooling support. In particular,
the OpenAPI codec/compatibility layer provided has a few major problems:
* there is no support for documenting response schemas and status codes
* nested schemas do not work properly
* does not handle more complex fields such as ``FileField``, ``ChoiceField``, …
In short this makes the generated schema unusable for code generation, and mediocre at best for documentation.
Other libraries
===============
There are currently two decent Swagger schema generators that I could find for django-rest-framework:
* `django-rest-swagger <https://github.com/marcgibbons/django-rest-swagger>`__
* `drf-openapi <https://github.com/limdauto/drf_openapi>`__
``django-rest-swagger`` is just a wrapper around DRF 3.7 schema generation with an added UI, and
thus presents the same problems, while also being unmaintained. ``drf-openapi`` was
`discontinued by the author <https://github.com/limdauto/drf_openapi/commit/1673c6e039eec7f089336a83bdc31613f32f7e21>`_
on April 3rd, 2018.
************************
Third-party integrations
************************
@ -372,5 +339,13 @@ provided out of the box - if you have ``djangorestframework-recursive`` installe
:target: https://drf-yasg.readthedocs.io/
:alt: ReadTheDocs
.. |bmac-button| image:: https://www.buymeacoffee.com/assets/img/custom_images/yellow_img.png
:target: https://www.buymeacoffee.com/cvijdea
:alt: Buy Me A Coffee
.. |heroku-button| image:: https://www.herokucdn.com/deploy/button.svg
:target: https://heroku.com/deploy?template=https://github.com/axnsan12/drf-yasg
:alt: Heroku deploy button
.. |nbsp| unicode:: 0xA0
:trim:

View File

@ -1,6 +0,0 @@
#!/usr/bin/env bash
set -e
coverage combine || true
coverage report
codecov

View File

@ -2,6 +2,110 @@
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**
**********
*Release date: Oct 03, 2019*
- **ADDED:** added `JSONFieldInspector` for `JSONField` support (:pr:`417`)
- **IMPROVED:** updated ``swagger-ui`` to version 3.23.11
- **IMPROVED:** updated ``ReDoc`` to version 2.0.0-rc.14 (:issue:`398`)
- **FIXED:** fixed a type hint support issue (:pr:`428`, :issue:`450`)
- **FIXED:** fixed packaging issue caused by a missing requirement (:issue:`412`)
**********
**1.16.1**
**********
*Release date: Jul 16, 2019*
- **IMPROVED:** better enum type detection for nested `ChoiceField`\ s (:pr:`400`)
- **FIXED:** fixed DRF 3.10 compatibility (:pr:`408`, :issue:`410`, :issue:`411`)
**********
**1.16.0**
**********
*Release date: Jun 13, 2019*
- **ADDED:** added `reference_resolver_class` attribute hook to `SwaggerAutoSchema` (:pr:`350`)
- **ADDED:** added `operation_keys` attribute to `SwaggerAutoSchema`, along with `__init__` parameter (:pr:`355`)
- **FIXED:** fixed potential crash on `issubclass` check without `isclass` check
**********
**1.15.1**
**********
*Release date: Jun 13, 2019*
- **IMPROVED:** updated ``swagger-ui`` to version 3.22.3
- **IMPROVED:** updated ``ReDoc`` to version 2.0.0-rc.8-1
- **FIXED:** fixed an issue with inspection of typing hints on Python 2.7 (:issue:`363`)
- **FIXED:** fixed an issue with inspection of typing hints on Python 3.7 (:issue:`371`)
**Python 3.4 support has been dropped!**
**********
**1.15.0**
**********
*Release date: Apr 01, 2019*
- **ADDED:** added ``is_list_view`` and ``has_list_response`` extension points to ``SwaggerAutoSchema`` (:issue:`331`)
- **IMPROVED:** updated ``swagger-ui`` to version 3.22.0
- **IMPROVED:** updated ``ReDoc`` to version 2.0.0-rc.4
- **FIXED:** ``ListModelMixin`` will now always be treated as a list view (:issue:`306`)
- **FIXED:** non-primtive values in field ``choices`` will now be handled properly (:issue:`340`)
**********
**1.14.0**
**********
*Release date: Mar 04, 2019*
- **IMPROVED:** updated ``swagger-ui`` to version 3.21.0
- **FIXED:** implicit ``ref_name`` collisions will now throw an exception
- **FIXED:** ``RecursiveField`` will now also work as a child of ``ListSerializer`` (:pr:`321`)
- **FIXED:** fixed ``minLength`` and ``maxLength`` for ``ListSerializer`` and ``ListField``
- **FIXED:** the ``items`` property of ``Schema``, ``Parameter`` and ``Items`` objects was renamed to ``items_``; this
is a *mildly breaking change* and was needed to fix the collision with the ``items`` method of ``dict`` (:pr:`308`)
- **REMOVED:** the ``get_summary`` and ``get_description`` methods have been removed (previously deprecated in 1.12.0)
**********
**1.13.0**
**********
*Release date: Jan 29, 2019*
- **IMPROVED:** type hint inspection is now supported for collections and ``Optional`` (:pr:`272`)
- **IMPROVED:** updated ``swagger-ui`` to version 3.20.5
- **IMPROVED:** updated ``ReDoc`` to version 2.0.0-rc.2
- **DEPRECATED:** quietly dropped support for the ``flex`` validator; it will still work if the library is installed,
but the setup.py requirement was removed and the validator will be silently skipped if not installed (:issue:`285`)
**********
**1.12.1**
**********
*Release date: Dec 28, 2018*
- **IMPROVED:** updated ``ReDoc`` to version 2.0.0-rc.0
- **FIXED:** management command will now correctly fall back to ``DEFAULT_VERSION`` for mock request
- **FIXED:** fixed bad "raised exception during schema generation" warnings caused by missing ``self`` parameter
**********
**1.12.0**
**********
@ -30,7 +134,7 @@ Changelog
Starting with this version, the ``setup_requires`` argument was dropped from ``setup.py`` in favor of
``build-system.requires`` in ``pyproject.toml`` . This means that for correctly building or installing from sdist,
you will need to use a PEP517/PEP518 compliant tool (tox>=3.3.0, setuptools>=40, pip>=10.0, pep517.build) or manually
install the build requirements yourself (just ```setuptools`` and ``setuptools-scm``, for now).
install the build requirements yourself (just ``setuptools`` and ``setuptools-scm``, for now).
Additionally, for correct package version detection, a full git checkout is required when building (this was always the
case). Building without ``.git`` or without ``setuptools-scm`` will result in a distribution with a version like

View File

@ -48,6 +48,11 @@ author = 'Cristi V.'
# The full version, including alpha/beta/rc tags.
release = get_distribution('drf_yasg').version
if 'noscm' in release:
raise AssertionError('Invalid package version string: %s. \n'
'The documentation must be built with drf_yasg installed from a distribution package, '
'which must have been built with a proper version number (i.e. from a full source checkout).'
% (release,))
# The short X.Y.Z version.
version = '.'.join(release.split('.')[:3])

View File

@ -45,6 +45,8 @@ some properties of the generated :class:`.Operation`. For example, in a ``ViewSe
.. code-block:: python
from drf_yasg.utils import swagger_auto_schema
@swagger_auto_schema(operation_description="partial_update description override", responses={404: 'slug not found'})
def partial_update(self, request, *args, **kwargs):
"""partial_update method docstring"""
@ -87,14 +89,14 @@ Where you can use the :func:`@swagger_auto_schema <.swagger_auto_schema>` decora
* for ``ViewSet``, ``GenericViewSet``, ``ModelViewSet``, because each viewset corresponds to multiple **paths**, you have
to decorate the *action methods*, i.e. ``list``, ``create``, ``retrieve``, etc. |br|
Additionally, ``@action``\ s, ``@list_route``\ s or ``@detail_route``\ s defined on the viewset, like function based
api views, can respond to multiple HTTP methods and thus have multiple operations that must be decorated separately:
Additionally, ``@action``\ s defined on the viewset, like function based api views, can respond to multiple HTTP
methods and thus have multiple operations that must be decorated separately:
.. code-block:: python
class ArticleViewSet(viewsets.ModelViewSet):
# method or 'methods' can be skipped because the list_route only handles a single method (GET)
# method or 'methods' can be skipped because the action only handles a single method (GET)
@swagger_auto_schema(operation_description='GET /articles/today/')
@action(detail=False, methods=['get'])
def today(self, request):
@ -210,7 +212,8 @@ Schema generation of ``serializers.SerializerMethodField`` is supported in two w
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
@ -234,6 +237,64 @@ The available options are:
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>`_.
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
@ -376,7 +437,7 @@ A second example, of a :class:`~.inspectors.FieldInspector` that removes the ``t
class AnotherSerializer(serializers.ModelSerializer):
chilf = OneSerializer()
child = OneSerializer()
class Meta:
model = SomeParentModel
@ -387,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
.. _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

@ -14,7 +14,7 @@ This library generates OpenAPI 2.0 documents. The authoritative specification fo
be the official documentation over at `swagger.io <https://swagger.io/>`__ and the `OpenAPI 2.0 specification
page <https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md>`__.
Beause the above specifications are a bit heavy and convoluted, here is a general overview of how the specification
Because the above specifications are a bit heavy and convoluted, here is a general overview of how the specification
is structured, starting from the root ``Swagger`` object.
* :class:`.Swagger` object

View File

@ -81,7 +81,7 @@ A very simple working configuration was provided by :ghuser:`Vigrond`, originall
'type': 'oauth2',
'authorizationUrl': '/yourapp/o/authorize',
'tokenUrl': '/yourapp/o/token/',
'flow": "accessCode',
'flow': 'accessCode',
'scopes': {
'read:groups': 'read groups',
}

View File

@ -85,6 +85,7 @@ to this list.
:class:`'drf_yasg.inspectors.ChoiceFieldInspector' <.inspectors.ChoiceFieldInspector>`, |br| \
:class:`'drf_yasg.inspectors.FileFieldInspector' <.inspectors.FileFieldInspector>`, |br| \
:class:`'drf_yasg.inspectors.DictFieldInspector' <.inspectors.DictFieldInspector>`, |br| \
:class:`'drf_yasg.inspectors.JSONFieldInspector' <.inspectors.JSONFieldInspector>`, |br| \
:class:`'drf_yasg.inspectors.HiddenFieldInspector' <.inspectors.HiddenFieldInspector>`, |br| \
:class:`'drf_yasg.inspectors.RecursiveFieldInspector' <.inspectors.RecursiveFieldInspector>`, |br| \
:class:`'drf_yasg.inspectors.SerializerMethodFieldInspector' <.inspectors.SerializerMethodFieldInspector>`, |br| \
@ -340,7 +341,7 @@ values for Parameters.
OAUTH2_REDIRECT_URL
-------------------
Used when OAuth2 authenitcation of API requests via swagger-ui is desired. If ``None`` is passed, the
Used when OAuth2 authentication of API requests via swagger-ui is desired. If ``None`` is passed, the
``oauth2RedirectUrl`` parameter will be set to ``{% static 'drf-yasg/swagger-ui-dist/oauth2-redirect.html' %}``. This
is the default `https://github.com/swagger-api/swagger-ui/blob/master/dist/oauth2-redirect.html <oauth2-redirect>`_
file provided by ``swagger-ui``.
@ -351,7 +352,7 @@ file provided by ``swagger-ui``.
OAUTH2_CONFIG
-------------
Used when OAuth2 authenitcation of API requests via swagger-ui is desired. Provides OAuth2 configuration parameters
Used when OAuth2 authentication of API requests via swagger-ui is desired. Provides OAuth2 configuration parameters
to the ``SwaggerUIBundle#initOAuth`` method, and must be a dictionary. See
`OAuth2 configuration <https://github.com/swagger-api/swagger-ui/blob/master/docs/usage/oauth2.md>`_.
@ -454,7 +455,7 @@ FETCH_SCHEMA_WITH_QUERY
Fetch the OpenAPI document using the query parameters passed to the ReDoc page request.
**Default**: :python:`'True` |br|
**Default**: :python:`True` |br|
*Maps to parameter*: -

1005
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,8 +1,8 @@
{
"name": "drf-yasg",
"dependencies": {
"redoc": "^2.0.0-alpha.41",
"swagger-ui-dist": "^3.20.4"
"redoc": "^2.0.0-rc.14",
"swagger-ui-dist": "^3.23.11"
},
"repository": {
"type": "git",

View File

@ -4,6 +4,7 @@ ruamel.yaml>=0.15.34
inflection>=0.3.1
six>=1.10.0
uritemplate>=3.0.0
packaging
djangorestframework>=3.7.7
djangorestframework>=3.8
Django>=1.11.7

View File

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

View File

@ -5,5 +5,7 @@ pytest-cov>=2.6.0
pytest-xdist>=1.25.0
pytest-django>=3.4.4
datadiff==2.0.0
psycopg2-binary==2.8.3
django-fake-model==0.1.4
-r testproj.txt

View File

@ -1,12 +1,9 @@
# test project requirements
Pillow>=4.3.0
pygments>=2.2.0
django-cors-headers>=2.1.0
django-filter>=1.1.0,<2.0; python_version == "2.7"
django-filter>=1.1.0; python_version >= "3.4"
djangorestframework-camel-case>=0.2.0
django-filter>=1.1.0; python_version >= "3.5"
djangorestframework-camel-case>=1.1.2
djangorestframework-recursive>=0.1.2
dj-database-url>=0.4.2
user_agents>=1.1.0
# django-oauth-toolkit 1.2 does not support Django 1.11
django-oauth-toolkit>=1.1.0,<1.2.0
django-cors-headers

View File

@ -1,3 +1,2 @@
# requirements for the validation feature
flex>=6.11.1
swagger-spec-validator>=2.1.0

View File

@ -1 +1 @@
python-3.7.1
python-3.7.3

View File

@ -19,6 +19,18 @@ with io.open('README.rst', encoding='utf-8') as readme:
requirements = read_req('base.txt')
requirements_validation = read_req('validation.txt')
py3_supported_range = (5, 8)
# convert inclusive range to exclusive range
py3_supported_range = (py3_supported_range[0], py3_supported_range[1] + 1)
python_requires = ", ".join([">=2.7"] + ["!=3.{}.*".format(v) for v in range(0, py3_supported_range[0])])
python_classifiers = [
'Programming Language :: Python',
'Programming Language :: Python :: 2',
'Programming Language :: Python :: 2.7',
'Programming Language :: Python :: 3',
] + ['Programming Language :: Python :: 3.{}'.format(v) for v in range(*py3_supported_range)]
def drf_yasg_setup(**kwargs):
setup(
@ -38,28 +50,21 @@ def drf_yasg_setup(**kwargs):
author_email='cristi@cvjd.me',
keywords='drf django django-rest-framework schema swagger openapi codegen swagger-codegen '
'documentation drf-yasg django-rest-swagger drf-openapi',
python_requires=">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*",
python_requires=python_requires,
classifiers=[
'Intended Audience :: Developers',
'License :: OSI Approved :: BSD License',
'Development Status :: 5 - Production/Stable',
'Operating System :: OS Independent',
'Environment :: Web Environment',
'Programming Language :: Python',
'Programming Language :: Python :: 2',
'Programming Language :: Python :: 2.7',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.4',
'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6',
'Programming Language :: Python :: 3.7',
'Framework :: Django',
'Framework :: Django :: 1.11',
'Framework :: Django :: 2.0',
'Framework :: Django :: 2.1',
'Framework :: Django :: 2.2',
'Topic :: Documentation',
'Topic :: Software Development :: Code Generators',
],
] + python_classifiers,
**kwargs
)

View File

@ -12,6 +12,7 @@ SWAGGER_DEFAULTS = {
'drf_yasg.inspectors.ChoiceFieldInspector',
'drf_yasg.inspectors.FileFieldInspector',
'drf_yasg.inspectors.DictFieldInspector',
'drf_yasg.inspectors.JSONFieldInspector',
'drf_yasg.inspectors.HiddenFieldInspector',
'drf_yasg.inspectors.RelatedFieldInspector',
'drf_yasg.inspectors.SerializerMethodFieldInspector',

View File

@ -1,4 +1,4 @@
from six import raise_from
from six import binary_type, raise_from, text_type
import copy
import json
@ -15,8 +15,12 @@ logger = logging.getLogger(__name__)
def _validate_flex(spec):
try:
from flex.core import parse as validate_flex
from flex.exceptions import ValidationError
except ImportError:
return
try:
validate_flex(spec)
except ValidationError as ex:
@ -88,7 +92,7 @@ class _OpenAPICodec(object):
:param dict spec: a python dict
:return: string representation of ``spec``
:rtype: str
:rtype: str or bytes
"""
raise NotImplementedError("override this method")
@ -105,8 +109,21 @@ class _OpenAPICodec(object):
class OpenAPICodecJson(_OpenAPICodec):
media_type = 'application/json'
def __init__(self, validators, pretty=False, media_type='application/json'):
super(OpenAPICodecJson, self).__init__(validators)
self.pretty = pretty
self.media_type = media_type
def _dump_dict(self, spec):
"""Dump ``spec`` into JSON."""
"""Dump ``spec`` into JSON.
:rtype: str"""
if self.pretty:
out = json.dumps(spec, indent=4, separators=(',', ': '))
if out[-1] != '\n':
out += '\n'
return out
else:
return json.dumps(spec)
@ -159,7 +176,14 @@ class SaneYamlDumper(yaml.SafeDumper):
node.flow_style = best_style
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_multi_representer(OrderedDict, SaneYamlDumper.represent_odict)
@ -201,6 +225,12 @@ def yaml_sane_load(stream):
class OpenAPICodecYaml(_OpenAPICodec):
media_type = 'application/yaml'
def __init__(self, validators, media_type='application/yaml'):
super(OpenAPICodecYaml, self).__init__(validators)
self.media_type = media_type
def _dump_dict(self, spec):
"""Dump ``spec`` into YAML."""
"""Dump ``spec`` into YAML.
:rtype: bytes"""
return yaml_sane_dump(spec, binary=True)

View File

@ -3,13 +3,14 @@ import logging
import re
from collections import OrderedDict, defaultdict
import rest_framework
import uritemplate
from coreapi.compat import urlparse
from packaging.version import Version
from rest_framework import versioning
from rest_framework.compat import URLPattern, URLResolver, get_original_route
from rest_framework.schemas.generators import EndpointEnumerator as _EndpointEnumerator
from rest_framework.schemas.generators import SchemaGenerator, endpoint_ordering, get_pk_name
from rest_framework.schemas.inspectors import get_pk_description
from rest_framework.schemas.generators import endpoint_ordering, get_pk_name
from rest_framework.settings import api_settings
from . import openapi
@ -19,6 +20,14 @@ from .inspectors.field import get_basic_type_info, get_queryset_field, get_query
from .openapi import ReferenceResolver, SwaggerDict
from .utils import force_real_str, get_consumes, get_produces
if Version(rest_framework.__version__) < Version('3.10'):
from rest_framework.schemas.generators import SchemaGenerator
from rest_framework.schemas.inspectors import get_pk_description
else:
from rest_framework.schemas import SchemaGenerator
from rest_framework.schemas.utils import get_pk_description
logger = logging.getLogger(__name__)
PATH_PARAMETER_RE = re.compile(r'{(?P<parameter>\w+)}')
@ -160,6 +169,7 @@ class OpenAPISchemaGenerator(object):
Method implementations shamelessly stolen and adapted from rest-framework ``SchemaGenerator``.
"""
endpoint_enumerator_class = EndpointEnumerator
reference_resolver_class = ReferenceResolver
def __init__(self, info, version='', url=None, patterns=None, urlconf=None):
"""
@ -230,7 +240,7 @@ class OpenAPISchemaGenerator(object):
def get_schema(self, request=None, public=False):
"""Generate a :class:`.Swagger` object representing the API schema.
:param request: the request used for filtering accesible endpoints and finding the spec URI
:param request: the request used for filtering accessible endpoints and finding the spec URI
:type request: rest_framework.request.Request or None
:param bool public: if True, all endpoints are included regardless of access through `request`
@ -238,7 +248,7 @@ class OpenAPISchemaGenerator(object):
:rtype: openapi.Swagger
"""
endpoints = self.get_endpoints(request)
components = ReferenceResolver(openapi.SCHEMA_DEFINITIONS)
components = self.reference_resolver_class(openapi.SCHEMA_DEFINITIONS, force_init=True)
self.consumes = get_consumes(api_settings.DEFAULT_PARSER_CLASSES)
self.produces = get_produces(api_settings.DEFAULT_RENDERER_CLASSES)
paths, prefix = self.get_paths(endpoints, components, request, public)
@ -440,7 +450,7 @@ class OpenAPISchemaGenerator(object):
if view_inspector_cls is None:
return None
view_inspector = view_inspector_cls(view, path, method, components, request, overrides)
view_inspector = view_inspector_cls(view, path, method, components, request, overrides, operation_keys)
operation = view_inspector.get_operation(operation_keys)
if operation is None:
return None

View File

@ -4,8 +4,8 @@ from .base import (
)
from .field import (
CamelCaseJSONFilter, ChoiceFieldInspector, DictFieldInspector, FileFieldInspector, HiddenFieldInspector,
InlineSerializerInspector, RecursiveFieldInspector, ReferencingSerializerInspector, RelatedFieldInspector,
SerializerMethodFieldInspector, SimpleFieldInspector, StringDefaultFieldInspector
InlineSerializerInspector, JSONFieldInspector, RecursiveFieldInspector, ReferencingSerializerInspector,
RelatedFieldInspector, SerializerMethodFieldInspector, SimpleFieldInspector, StringDefaultFieldInspector
)
from .query import CoreAPICompatInspector, DjangoRestResponsePagination
from .view import SwaggerAutoSchema
@ -24,7 +24,7 @@ __all__ = [
# field inspectors
'InlineSerializerInspector', 'RecursiveFieldInspector', 'ReferencingSerializerInspector', 'RelatedFieldInspector',
'SimpleFieldInspector', 'FileFieldInspector', 'ChoiceFieldInspector', 'DictFieldInspector',
'SimpleFieldInspector', 'FileFieldInspector', 'ChoiceFieldInspector', 'DictFieldInspector', 'JSONFieldInspector',
'StringDefaultFieldInspector', 'CamelCaseJSONFilter', 'HiddenFieldInspector', 'SerializerMethodFieldInspector',
# view inspectors

View File

@ -12,21 +12,44 @@ NotHandled = object()
logger = logging.getLogger(__name__)
def is_callable_method(cls_or_instance, method_name):
method = getattr(cls_or_instance, method_name)
if inspect.ismethod(method) and getattr(method, '__self__', None):
# bound classmethod or instance method
return method, True
try:
# inspect.getattr_static was added in python 3.2
from inspect import getattr_static
# on python 3, both unbound instance methods (i.e. getattr(cls, mth)) and static methods are plain functions
# getattr_static allows us to check the type of the method descriptor; for `@staticmethod` this is staticmethod
return method, isinstance(getattr_static(cls_or_instance, method_name, None), staticmethod)
except ImportError:
# python 2 still has unbound methods, so ismethod <=> !staticmethod TODO: remove when dropping python 2.7
return method, not inspect.ismethod(method)
def call_view_method(view, method_name, fallback_attr=None, default=None):
"""Call a view method which might throw an exception. If an exception is thrown, log an informative error message
and return the value of fallback_attr, or default if not present.
and return the value of fallback_attr, or default if not present. The method must be callable without any arguments
except cls or self.
:param rest_framework.views.APIView view:
:param view: view class or instance; if a class is passed, instance methods won't be called
:type view: rest_framework.views.APIView or type[rest_framework.views.APIView]
:param str method_name: name of a method on the view
:param str fallback_attr: name of an attribute on the view to fall back on, if calling the method fails
:param default: default value if all else fails
:return: view method's return value, or value of view's fallback_attr, or default
:rtype: any or None
"""
if hasattr(view, method_name):
try:
return getattr(view, method_name)()
view_method, is_callabale = is_callable_method(view, method_name)
if is_callabale:
return view_method()
except Exception: # pragma: no cover
logger.warning("view's %s.get_parsers raised exception during schema generation; use "
logger.warning("view's %s raised exception during schema generation; use "
"`getattr(self, 'swagger_fake_view', False)` to detect and short-circuit this",
type(view).__name__, exc_info=True)
@ -249,7 +272,8 @@ class FieldInspector(BaseInspector):
assert not isinstance(field, openapi.SwaggerDict), "passed field is already a SwaggerDict object"
title = force_real_str(field.label) if field.label else None
title = title if swagger_object_type == openapi.Schema else None # only Schema has title
description = force_real_str(field.help_text) if field.help_text else None
help_text = getattr(field, 'help_text', None)
description = force_real_str(help_text) if help_text else None
description = description if swagger_object_type != openapi.Items else None # Items has no description either
def SwaggerType(existing_object=None, **instance_kwargs):
@ -318,6 +342,9 @@ class ViewInspector(BaseInspector):
#: methods that are assumed to require a request body determined by the view's ``serializer_class``
implicit_body_methods = ('PUT', 'PATCH', 'POST')
#: methods which are assumed to return a list of objects when present on non-detail endpoints
implicit_list_response_methods = ('GET',)
# real values set in __init__ to prevent import errors
field_inspectors = [] #:
filter_inspectors = [] #:
@ -351,20 +378,30 @@ class ViewInspector(BaseInspector):
"""
raise NotImplementedError("ViewInspector must implement get_operation()!")
# methods below provided as default implementations for probing inspectors
def is_list_view(self):
"""Determine whether this view is a list or a detail view. The difference between the two is that
detail views depend on a pk/id path parameter. Note that a non-detail view does not necessarily imply a list
reponse (:meth:`.has_list_response`), nor are list responses limited to non-detail views.
For example, one might have a `/topic/<pk>/posts` endpoint which is a detail view that has a list response.
:rtype: bool"""
return is_list_view(self.path, self.method, self.view)
def has_list_response(self):
"""Determine whether this view returns multiple objects. By default this is any non-detail view
(see :meth:`.is_list_view`) whose request method is one of :attr:`.implicit_list_response_methods`.
:rtype: bool
"""
return self.is_list_view() and (self.method.upper() in self.implicit_list_response_methods)
def should_filter(self):
"""Determine whether filter backend parameters should be included for this request.
:rtype: bool
"""
if not getattr(self.view, 'filter_backends', None):
return False
if self.method.lower() not in ["get", "delete"]:
return False
return is_list_view(self.path, self.method, self.view)
return getattr(self.view, 'filter_backends', None) and self.has_list_response()
def get_filter_parameters(self):
"""Return the parameters added to the view by its filter backends.
@ -385,13 +422,7 @@ class ViewInspector(BaseInspector):
:rtype: bool
"""
if not getattr(self.view, 'paginator', None):
return False
if self.method.lower() != 'get':
return False
return is_list_view(self.path, self.method, self.view)
return getattr(self.view, 'paginator', None) and self.has_list_response()
def get_pagination_parameters(self):
"""Return the parameters added to the view by its paginator.

View File

@ -2,6 +2,7 @@ import datetime
import inspect
import logging
import operator
import sys
import uuid
from collections import OrderedDict
from decimal import Decimal
@ -13,7 +14,9 @@ from rest_framework.settings import api_settings as rest_framework_settings
from .. import openapi
from ..errors import SwaggerGenerationError
from ..utils import decimal_as_float, filter_none, get_serializer_class, get_serializer_ref_name
from ..utils import (
decimal_as_float, field_value_to_representation, filter_none, get_serializer_class, get_serializer_ref_name
)
from .base import FieldInspector, NotHandled, SerializerInspector, call_view_method
try:
@ -72,14 +75,20 @@ class InlineSerializerInspector(SerializerInspector):
def get_serializer_ref_name(self, serializer):
return get_serializer_ref_name(serializer)
def _has_ref_name(self, serializer):
serializer_meta = getattr(serializer, 'Meta', None)
return hasattr(serializer_meta, 'ref_name')
def field_to_swagger_object(self, field, swagger_object_type, use_references, **kwargs):
SwaggerType, ChildSwaggerType = self._get_partial_types(field, swagger_object_type, use_references, **kwargs)
if isinstance(field, (serializers.ListSerializer, serializers.ListField)):
child_schema = self.probe_field_inspectors(field.child, ChildSwaggerType, use_references)
limits = find_limits(field) or {}
return SwaggerType(
type=openapi.TYPE_ARRAY,
items=child_schema,
**limits
)
elif isinstance(field, serializers.Serializer):
if swagger_object_type != openapi.Schema:
@ -116,6 +125,7 @@ class InlineSerializerInspector(SerializerInspector):
# it is better to just remove title from inline models
del result.title
setattr(result, '_NP_serializer', get_serializer_class(serializer))
return result
if not ref_name or not use_references:
@ -125,11 +135,15 @@ class InlineSerializerInspector(SerializerInspector):
actual_schema = definitions.setdefault(ref_name, make_schema_definition)
actual_schema._remove_read_only()
actual_serializer = get_serializer_class(getattr(actual_schema, '_serializer', None))
actual_serializer = getattr(actual_schema, '_NP_serializer', None)
this_serializer = get_serializer_class(field)
if actual_serializer and actual_serializer != this_serializer: # pragma: no cover
logger.warning("Schema for %s will override distinct serializer %s because they "
"share the same ref_name", actual_serializer, this_serializer)
explicit_refs = self._has_ref_name(actual_serializer) and self._has_ref_name(this_serializer)
if not explicit_refs:
raise SwaggerGenerationError(
"Schema for %s would override distinct serializer %s because they implicitly share the same "
"ref_name; explicitly set the ref_name atribute on both serializers' Meta classes"
% (actual_serializer, this_serializer))
return openapi.SchemaRef(definitions, ref_name)
@ -177,11 +191,11 @@ def get_queryset_from_view(view, serializer=None):
:return: queryset or ``None``
"""
try:
queryset = call_view_method(view, 'get_queryset', 'queryset', None)
queryset = call_view_method(view, 'get_queryset', 'queryset')
if queryset is not None and serializer is not None:
# make sure the view is actually using *this* serializer
assert type(serializer) == view.get_serializer_class()
assert type(serializer) == call_view_method(view, 'get_serializer_class', 'serializer_class')
return queryset
except Exception: # pragma: no cover
@ -368,8 +382,21 @@ def find_limits(field):
def decimal_field_type(field):
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 = [
(models.OneToOneField, (recurse_one_to_one, None)),
(models.AutoField, (openapi.TYPE_INTEGER, None)),
(models.BinaryField, (openapi.TYPE_STRING, openapi.FORMAT_BINARY)),
(models.BooleanField, (openapi.TYPE_BOOLEAN, None)),
@ -455,18 +482,57 @@ def decimal_return_type():
return openapi.TYPE_STRING if rest_framework_settings.COERCE_DECIMAL_TO_STRING else openapi.TYPE_NUMBER
raw_type_info = [
def get_origin_type(hint_class):
return getattr(hint_class, '__origin__', None) or hint_class
def hint_class_issubclass(hint_class, check_class):
origin_type = get_origin_type(hint_class)
return inspect.isclass(origin_type) and issubclass(origin_type, check_class)
hinting_type_info = [
(bool, (openapi.TYPE_BOOLEAN, None)),
(int, (openapi.TYPE_INTEGER, None)),
(str, (openapi.TYPE_STRING, None)),
(float, (openapi.TYPE_NUMBER, None)),
(dict, (openapi.TYPE_OBJECT, None)),
(Decimal, (decimal_return_type, openapi.FORMAT_DECIMAL)),
(uuid.UUID, (openapi.TYPE_STRING, openapi.FORMAT_UUID)),
(datetime.datetime, (openapi.TYPE_STRING, openapi.FORMAT_DATETIME)),
(datetime.date, (openapi.TYPE_STRING, openapi.FORMAT_DATE)),
# TODO - support typing.List etc
]
hinting_type_info = raw_type_info
if sys.version_info < (3, 0):
# noinspection PyUnresolvedReferences
hinting_type_info.append((unicode, (openapi.TYPE_STRING, None))) # noqa: F821
if typing:
def inspect_collection_hint_class(hint_class):
args = hint_class.__args__
child_class = args[0] if args else str
child_type_info = get_basic_type_info_from_hint(child_class) or {'type': openapi.TYPE_STRING}
return OrderedDict([
('type', openapi.TYPE_ARRAY),
('items', openapi.Items(**child_type_info)),
])
hinting_type_info.append(((typing.Sequence, typing.AbstractSet), inspect_collection_hint_class))
def _get_union_types(hint_class):
if typing:
origin_type = get_origin_type(hint_class)
if origin_type is typing.Union:
return hint_class.__args__
try:
# python 3.5.2 and lower compatibility
if issubclass(origin_type, typing.Union):
return hint_class.__union_params__
except TypeError:
pass
return None
def get_basic_type_info_from_hint(hint_class):
@ -478,28 +544,35 @@ def get_basic_type_info_from_hint(hint_class):
:return: the extracted attributes as a dictionary, or ``None`` if the field type is not known
:rtype: OrderedDict
"""
union_types = _get_union_types(hint_class)
for check_class, type_format in hinting_type_info:
if issubclass(hint_class, check_class):
swagger_type, format = type_format
if callable(swagger_type):
swagger_type = swagger_type()
# if callable(format):
# format = format(klass)
break
else: # pragma: no cover
return None
pattern = None
result = OrderedDict([
('type', swagger_type),
('format', format),
('pattern', pattern)
])
if typing and union_types:
# Optional is implemented as Union[T, None]
if len(union_types) == 2 and isinstance(None, union_types[1]):
result = get_basic_type_info_from_hint(union_types[0])
if result:
result['x-nullable'] = True
return result
return None
for check_class, info in hinting_type_info:
if hint_class_issubclass(hint_class, check_class):
if callable(info):
return info(hint_class)
swagger_type, format = info
if callable(swagger_type):
swagger_type = swagger_type()
return OrderedDict([
('type', swagger_type),
('format', format),
])
return None
class SerializerMethodFieldInspector(FieldInspector):
"""Provides conversion for SerializerMethodField, optionally using information from the swagger_serializer_method
@ -585,13 +658,24 @@ class ChoiceFieldInspector(FieldInspector):
if isinstance(field, serializers.ChoiceField):
enum_type = openapi.TYPE_STRING
enum_values = list(field.choices.keys())
enum_values = []
for choice in field.choices.keys():
if isinstance(field, serializers.MultipleChoiceField):
choice = field_value_to_representation(field, [choice])[0]
else:
choice = field_value_to_representation(field, choice)
enum_values.append(choice)
# for ModelSerializer, try to infer the type from the associated model field
serializer = get_parent_serializer(field)
if isinstance(serializer, serializers.ModelSerializer):
model = getattr(getattr(serializer, 'Meta'), 'model')
model_field = get_model_field(model, field.source)
# Use the parent source for nested fields
model_field = get_model_field(model, field.source or field.parent.source)
# If the field has a base_field its type must be used
if getattr(model_field, "base_field", None):
model_field = model_field.base_field
if model_field:
model_type = get_basic_type_info(model_field)
if model_type:
@ -676,11 +760,23 @@ class HiddenFieldInspector(FieldInspector):
return NotHandled
class JSONFieldInspector(FieldInspector):
"""Provides conversion for ``JSONField``."""
def field_to_swagger_object(self, field, swagger_object_type, use_references, **kwargs):
SwaggerType, ChildSwaggerType = self._get_partial_types(field, swagger_object_type, use_references, **kwargs)
if isinstance(field, serializers.JSONField) and swagger_object_type == openapi.Schema:
return SwaggerType(type=openapi.TYPE_OBJECT)
return NotHandled
class StringDefaultFieldInspector(FieldInspector):
"""For otherwise unhandled fields, return them as plain :data:`.TYPE_STRING` objects."""
def field_to_swagger_object(self, field, swagger_object_type, use_references, **kwargs): # pragma: no cover
# TODO unhandled fields: TimeField JSONField
# TODO unhandled fields: TimeField
SwaggerType, ChildSwaggerType = self._get_partial_types(field, swagger_object_type, use_references, **kwargs)
return SwaggerType(type=openapi.TYPE_STRING)
@ -755,10 +851,19 @@ else:
if isinstance(field, RecursiveField) and swagger_object_type == openapi.Schema:
assert use_references is True, "Can not create schema for RecursiveField when use_references is False"
ref_name = get_serializer_ref_name(field.proxied)
assert ref_name is not None, "Can't create RecursiveField schema for inline " + str(type(field.proxied))
proxied = field.proxied
if isinstance(field.proxied, serializers.ListSerializer):
proxied = proxied.child
ref_name = get_serializer_ref_name(proxied)
assert ref_name is not None, "Can't create RecursiveField schema for inline " + str(type(proxied))
definitions = self.components.with_scope(openapi.SCHEMA_DEFINITIONS)
return openapi.SchemaRef(definitions, ref_name, ignore_unresolved=True)
ref = openapi.SchemaRef(definitions, ref_name, ignore_unresolved=True)
if isinstance(field.proxied, serializers.ListSerializer):
ref = openapi.Items(type=openapi.TYPE_ARRAY, items=ref)
return ref
return NotHandled

View File

@ -1,5 +1,4 @@
import logging
import warnings
from collections import OrderedDict
from rest_framework.request import is_form_media_type
@ -10,7 +9,7 @@ from .. import openapi
from ..errors import SwaggerGenerationError
from ..utils import (
filter_none, force_real_str, force_serializer_instance, get_consumes, get_produces, guess_response_status,
is_list_view, merge_params, no_body, param_list_to_odict
merge_params, no_body, param_list_to_odict
)
from .base import ViewInspector, call_view_method
@ -18,28 +17,15 @@ logger = logging.getLogger(__name__)
class SwaggerAutoSchema(ViewInspector):
def __init__(self, view, path, method, components, request, overrides):
def __init__(self, view, path, method, components, request, overrides, operation_keys=None):
super(SwaggerAutoSchema, self).__init__(view, path, method, components, request, overrides)
self._sch = AutoSchema()
self._sch.view = view
self.operation_keys = operation_keys
def _summary_and_description_compat(self):
# TODO: remove in 1.13
base_methods = (SwaggerAutoSchema.get_summary, SwaggerAutoSchema.get_description)
self_methods = (type(self).get_summary, type(self).get_description)
if self_methods != base_methods:
warnings.warn(
"`SwaggerAutoSchema` methods `get_summary` and `get_description` are deprecated and "
"will be removed in drf-yasg 1.13. Override `get_summary_and_description` instead.",
DeprecationWarning, stacklevel=2
)
# if get_summary or get_description are overriden by a child class,
# we must call them for backwards compatibility
return self.get_summary(), self.get_description()
def get_operation(self, operation_keys=None):
operation_keys = operation_keys or self.operation_keys
return self.get_summary_and_description()
def get_operation(self, operation_keys):
consumes = self.get_consumes()
produces = self.get_produces()
@ -50,7 +36,7 @@ class SwaggerAutoSchema(ViewInspector):
parameters = self.add_manual_parameters(parameters)
operation_id = self.get_operation_id(operation_keys)
summary, description = self._summary_and_description_compat()
summary, description = self.get_summary_and_description()
security = self.get_security()
assert security is None or isinstance(security, list), "security must be a list of security requirement objects"
deprecated = self.is_deprecated()
@ -225,7 +211,7 @@ class SwaggerAutoSchema(ViewInspector):
default_schema = self.serializer_to_schema(default_schema) or ''
if default_schema:
if is_list_view(self.path, self.method, self.view) and self.method.lower() == 'get':
if self.has_list_response():
default_schema = openapi.Schema(type=openapi.TYPE_ARRAY, items=default_schema)
if self.should_page():
default_schema = self.get_paginated_response(default_schema) or default_schema
@ -317,7 +303,7 @@ class SwaggerAutoSchema(ViewInspector):
return natural_parameters + serializer_parameters
def get_operation_id(self, operation_keys):
def get_operation_id(self, operation_keys=None):
"""Return an unique ID for this operation. The ID must be unique across
all :class:`.Operation` objects in the API.
@ -325,6 +311,8 @@ class SwaggerAutoSchema(ViewInspector):
of this view in the API; e.g. ``('snippets', 'list')``, ``('snippets', 'retrieve')``, etc.
:rtype: str
"""
operation_keys = operation_keys or self.operation_keys
operation_id = self.overrides.get('operation_id', '')
if not operation_id:
operation_id = '_'.join(operation_keys)
@ -368,22 +356,6 @@ class SwaggerAutoSchema(ViewInspector):
return summary, description
def get_summary(self):
"""Return a summary description for this operation.
:return: the summary
:rtype: str
"""
return self.get_summary_and_description()[0]
def get_description(self):
"""Return an operation description determined as appropriate from the view's method and class docstrings.
:return: the operation description
:rtype: str
"""
return self.get_summary_and_description()[1]
def get_security(self):
"""Return a list of security requirements for this operation.
@ -402,7 +374,7 @@ class SwaggerAutoSchema(ViewInspector):
"""
return self.overrides.get('deprecated', None)
def get_tags(self, operation_keys):
def get_tags(self, operation_keys=None):
"""Get a list of tags for this operation. Tags determine how operations relate with each other, and in the UI
each tag will show as a group containing the operations that use it. If not provided in overrides,
tags will be inferred from the operation url.
@ -411,6 +383,8 @@ class SwaggerAutoSchema(ViewInspector):
of this view in the API; e.g. ``('snippets', 'list')``, ``('snippets', 'retrieve')``, etc.
:rtype: list[str]
"""
operation_keys = operation_keys or self.operation_keys
tags = self.overrides.get('tags')
if not tags:
tags = [operation_keys[0]]

View File

@ -1,12 +1,11 @@
import json
import logging
import os
from collections import OrderedDict
from importlib import import_module
from django.contrib.auth import get_user_model
from django.core.exceptions import ImproperlyConfigured
from django.core.management.base import BaseCommand
from django.utils.module_loading import import_string
from rest_framework.settings import api_settings
from rest_framework.test import APIRequestFactory, force_authenticate
from rest_framework.views import APIView
@ -15,15 +14,6 @@ from ...app_settings import swagger_settings
from ...codecs import OpenAPICodecJson, OpenAPICodecYaml
def import_class(import_string):
if not import_string:
return None
module_path, class_name = import_string.rsplit('.', 1)
module = import_module(module_path)
return getattr(module, class_name)
class Command(BaseCommand):
help = 'Write the Swagger schema to disk in JSON or YAML format.'
@ -85,11 +75,9 @@ class Command(BaseCommand):
def write_schema(self, schema, stream, format):
if format == 'json':
codec = OpenAPICodecJson(validators=[])
swagger_json = codec.encode(schema)
swagger_json = json.loads(swagger_json.decode('utf-8'), object_pairs_hook=OrderedDict)
pretty_json = json.dumps(swagger_json, indent=4, ensure_ascii=True)
stream.write(pretty_json)
codec = OpenAPICodecJson(validators=[], pretty=True)
swagger_json = codec.encode(schema).decode('utf-8')
stream.write(swagger_json)
elif format == 'yaml':
codec = OpenAPICodecYaml(validators=[])
swagger_yaml = codec.encode(schema).decode('utf-8')
@ -107,6 +95,20 @@ class Command(BaseCommand):
request = APIView().initialize_request(request)
return request
def get_schema_generator(self, generator_class_name, api_info, api_version, api_url):
generator_class = swagger_settings.DEFAULT_GENERATOR_CLASS
if generator_class_name:
generator_class = import_string(generator_class_name)
return generator_class(
info=api_info,
version=api_version,
url=api_url,
)
def get_schema(self, generator, request, public):
return generator.get_schema(request=request, public=public)
def handle(self, output_file, overwrite, format, api_url, mock, api_version, user, private, generator_class_name,
*args, **kwargs):
# disable logs of WARNING and below
@ -129,7 +131,7 @@ class Command(BaseCommand):
if user:
# Only call get_user_model if --user was passed in order to
# 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)
if mock and not api_url:
@ -142,16 +144,12 @@ class Command(BaseCommand):
if mock:
request = self.get_mock_request(api_url, format, user)
api_version = api_version or api_settings.DEFAULT_VERSION
if request and api_version:
request.version = api_version
generator_class = import_class(generator_class_name) or swagger_settings.DEFAULT_GENERATOR_CLASS
generator = generator_class(
info=info,
version=api_version,
url=api_url,
)
schema = generator.get_schema(request=request, public=not private)
generator = self.get_schema_generator(generator_class_name, info, api_version, api_url)
schema = self.get_schema(generator, request, not private)
if output_file == '-':
self.write_schema(schema, self.stdout, format)

View File

@ -10,7 +10,7 @@ from django.urls import get_script_prefix
from django.utils.functional import Promise
from inflection import camelize
from .utils import filter_none, force_real_str
from .utils import dict_has_ordered_keys, filter_none, force_real_str
try:
from collections import abc as collections_abc
@ -145,7 +145,7 @@ class SwaggerDict(OrderedDict):
result = OrderedDict()
memo[id(obj)] = result
items = obj.items()
if not isinstance(obj, OrderedDict):
if not dict_has_ordered_keys(obj):
items = sorted(items)
for attr, val in items:
result[attr] = SwaggerDict._as_odict(val, memo)
@ -167,7 +167,8 @@ class SwaggerDict(OrderedDict):
def __reduce__(self):
# for pickle supprt; this skips calls to all SwaggerDict __init__ methods and relies
# on the already set attributes instead
return _bare_SwaggerDict, (type(self),), vars(self), None, iter(self.items())
attrs = {k: v for k, v in vars(self).items() if not k.startswith('_NP_')}
return _bare_SwaggerDict, (type(self),), attrs, None, iter(self.items())
class Contact(SwaggerDict):
@ -400,7 +401,7 @@ class Items(SwaggerDict):
self.format = format
self.enum = enum
self.pattern = pattern
self.items = items
self.items_ = items
self._insert_extras__()
_check_type(type, format, enum, pattern, items, self.__class__)
@ -434,7 +435,7 @@ class Parameter(SwaggerDict):
self.format = format
self.enum = enum
self.pattern = pattern
self.items = items
self.items_ = items
self.default = default
self._insert_extras__()
if (not schema and not type) or (schema and type):
@ -469,7 +470,7 @@ class Schema(SwaggerDict):
:type properties: dict[str,Schema or SchemaRef]
:param additional_properties: allow wildcard properties not listed in `properties`
: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``
:type items: Schema or SchemaRef
:param default: only valid when insider another ``Schema``\\ 's ``properties``;
@ -492,7 +493,7 @@ class Schema(SwaggerDict):
self.format = format
self.enum = enum
self.pattern = pattern
self.items = items
self.items_ = items
self.read_only = read_only
self.default = default
self._insert_extras__()
@ -617,16 +618,26 @@ class ReferenceResolver(object):
::
> components = ReferenceResolver('definitions', 'parameters')
> definitions = ReferenceResolver.with_scope('definitions')
> definitions = components.with_scope('definitions')
> definitions.set('Article', Schema(...))
> print(components)
{'definitions': OrderedDict([('Article', Schema(...)]), 'parameters': OrderedDict()}
"""
def __init__(self, *scopes):
def __init__(self, *scopes, **kwargs):
"""
:param str scopes: an enumeration of the valid scopes this resolver will contain
"""
force_init = kwargs.pop('force_init', False)
if not force_init:
raise AssertionError(
"Creating an instance of ReferenceResolver almost certainly won't do what you want it to do.\n"
"See https://github.com/axnsan12/drf-yasg/issues/211, "
"https://github.com/axnsan12/drf-yasg/issues/271, "
"https://github.com/axnsan12/drf-yasg/issues/325.\n"
"Pass `force_init=True` to override this."
)
self._objects = OrderedDict()
self._force_scope = None
for scope in scopes:
@ -641,7 +652,7 @@ class ReferenceResolver(object):
:rtype: .ReferenceResolver
"""
assert scope in self.scopes, "unknown scope %s" % scope
ret = ReferenceResolver()
ret = ReferenceResolver(force_init=True)
ret._objects = self._objects
ret._force_scope = scope
return ret

View File

@ -2,7 +2,7 @@ import six
from django.shortcuts import resolve_url
from django.template.loader import render_to_string
from django.utils.encoding import force_text
from django.utils.encoding import force_str
from django.utils.functional import Promise
from rest_framework.renderers import BaseRenderer, JSONRenderer, TemplateHTMLRenderer
from rest_framework.utils import encoders, json
@ -124,7 +124,7 @@ class SwaggerUIRenderer(_UIRenderer):
swagger_ui_settings = self.get_swagger_ui_settings()
request = renderer_context.get('request', None)
oauth_redirect_url = force_text(swagger_ui_settings.get('oauth2RedirectUrl', ''))
oauth_redirect_url = force_str(swagger_ui_settings.get('oauth2RedirectUrl', ''))
if request and oauth_redirect_url:
swagger_ui_settings['oauth2RedirectUrl'] = request.build_absolute_uri(oauth_redirect_url)

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 628 B

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,11 +1,15 @@
import inspect
import logging
import sys
import textwrap
from collections import OrderedDict
from decimal import Decimal
from django.db import models
from django.utils.encoding import force_text
from django.utils.encoding import force_str
from rest_framework import serializers, status
from rest_framework.mixins import DestroyModelMixin, RetrieveModelMixin, UpdateModelMixin
from rest_framework.mixins import DestroyModelMixin, ListModelMixin, RetrieveModelMixin, UpdateModelMixin
from rest_framework.parsers import FileUploadParser
from rest_framework.request import is_form_media_type
from rest_framework.settings import api_settings as rest_framework_settings
from rest_framework.utils import encoders, json
@ -92,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 :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`
:type responses: dict[str,(drf_yasg.openapi.Schema or drf_yasg.openapi.SchemaRef or drf_yasg.openapi.Response or
str or rest_framework.serializers.Serializer)]
:type responses: dict[int or str, (drf_yasg.openapi.Schema or drf_yasg.openapi.SchemaRef or
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
will be tried before :attr:`.ViewInspector.field_inspectors` on the :class:`.inspectors.SwaggerAutoSchema`
@ -163,7 +167,7 @@ def swagger_auto_schema(method=None, methods=None, auto_schema=unset, request_bo
if len(available_http_methods) > 1:
assert _methods, \
"on multi-method api_view, action, detail_route or list_route, you must specify " \
"on multi-method api_view or action, you must specify " \
"swagger_auto_schema on a per-method basis using one of the `method` or `methods` arguments"
else:
# for a single-method view we assume that single method as the decorator target
@ -176,8 +180,8 @@ def swagger_auto_schema(method=None, methods=None, auto_schema=unset, request_bo
view_method._swagger_auto_schema = existing_data
else:
assert not _methods, \
"the methods argument should only be specified when decorating an action, detail_route or " \
"list_route; you should also ensure that you put the swagger_auto_schema decorator " \
"the methods argument should only be specified when decorating an action; " \
"you should also ensure that you put the swagger_auto_schema decorator " \
"AFTER (above) the _route decorator"
assert not existing_data, "swagger_auto_schema applied twice to method"
view_method._swagger_auto_schema = data
@ -212,7 +216,7 @@ def is_list_view(path, method, view):
:param APIView view: target view
:rtype: bool
"""
# for ViewSets, it could be the default 'list' action, or a list_route
# for ViewSets, it could be the default 'list' action, or an @action(detail=False)
action = getattr(view, 'action', '')
method = getattr(view, action, None) or method
detail = getattr(method, 'detail', None)
@ -224,6 +228,9 @@ def is_list_view(path, method, view):
# a detail action is surely not a list route
return False
if isinstance(view, ListModelMixin):
return True
# for GenericAPIView, if it's a detail view it can't also be a list view
if isinstance(view, (RetrieveModelMixin, UpdateModelMixin, DestroyModelMixin)):
return False
@ -365,11 +372,18 @@ def get_consumes(parser_classes):
:rtype: list[str]
"""
parser_classes = get_object_classes(parser_classes)
parser_classes = [pc for pc in parser_classes if not issubclass(pc, FileUploadParser)]
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)]
# 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:
return media_types
else:
# 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
@ -389,8 +403,7 @@ def get_produces(renderer_classes):
def decimal_as_float(field):
"""
Returns true if ``field`` is a django-rest-framework DecimalField and its ``coerce_to_string`` attribute or the
"""Returns true if ``field`` is a django-rest-framework DecimalField and its ``coerce_to_string`` attribute or the
``COERCE_DECIMAL_TO_STRING`` setting is set to ``False``.
:rtype: bool
@ -401,8 +414,7 @@ def decimal_as_float(field):
def get_serializer_ref_name(serializer):
"""
Get serializer's ref_name (or None for ModelSerializer if it is named 'NestedSerializer')
"""Get serializer's ref_name (or None for ModelSerializer if it is named 'NestedSerializer')
:param serializer: Serializer instance
:return: Serializer's ``ref_name`` or ``None`` for inline serializer
@ -429,13 +441,35 @@ def force_real_str(s, encoding='utf-8', strings_only=False, errors='strict'):
Fix for https://github.com/axnsan12/drf-yasg/issues/159
"""
if s is not None:
s = force_text(s, encoding, strings_only, errors)
s = force_str(s, encoding, strings_only, errors)
if type(s) != str:
s = '' + s
# Remove common indentation to get the correct Markdown rendering
s = textwrap.dedent(s)
return s
def field_value_to_representation(field, value):
"""Convert a python value related to a field (default, choices, etc.) into its OpenAPI-compatible representation.
:param serializers.Field field: field associated with the value
:param object value: value
:return: the converted value
"""
value = field.to_representation(value)
if isinstance(value, Decimal):
if decimal_as_float(field):
value = float(value)
else:
value = str(value)
# JSON roundtrip ensures that the value is valid JSON;
# for example, sets and tuples get transformed into lists
return json.loads(json.dumps(value, cls=encoders.JSONEncoder))
def get_field_default(field):
"""
Get the default value for a field, converted to a JSON-compatible value while properly handling callables.
@ -449,6 +483,9 @@ def get_field_default(field):
try:
if hasattr(default, 'set_context'):
default.set_context(field)
if getattr(default, 'requires_context', False):
default = default(field)
else:
default = default()
except Exception: # pragma: no cover
logger.warning("default for %s is callable but it raised an exception when "
@ -457,15 +494,23 @@ def get_field_default(field):
if default is not serializers.empty and default is not None:
try:
default = field.to_representation(default)
# JSON roundtrip ensures that the value is valid JSON;
# for example, sets and tuples get transformed into lists
default = json.loads(json.dumps(default, cls=encoders.JSONEncoder))
if decimal_as_float(field):
default = float(default)
default = field_value_to_representation(field, default)
except Exception: # pragma: no cover
logger.warning("'default' on schema for %s will not be set because "
"to_representation raised an exception", field, exc_info=True)
default = serializers.empty
return default
def dict_has_ordered_keys(obj):
"""Check if a given object is a dict that maintains insertion order.
:param obj: the dict object to check
:rtype: bool
"""
if sys.version_info >= (3, 7):
# the Python 3.7 language spec says that dict must maintain insertion order.
return isinstance(obj, dict)
return isinstance(obj, OrderedDict)

View File

@ -1,8 +1,7 @@
import warnings
from functools import wraps
from functools import WRAPPER_ASSIGNMENTS, wraps
from django.utils.cache import add_never_cache_headers
from django.utils.decorators import available_attrs
from django.views.decorators.cache import cache_page
from django.views.decorators.vary import vary_on_headers
from rest_framework import exceptions
@ -30,7 +29,7 @@ def deferred_never_cache(view_func):
never be cached.
"""
@wraps(view_func, assigned=available_attrs(view_func))
@wraps(view_func, assigned=WRAPPER_ASSIGNMENTS)
def _wrapped_view_func(request, *args, **kwargs):
response = view_func(request, *args, **kwargs)
@ -57,7 +56,7 @@ def get_schema_view(info=None, url=None, patterns=None, urlconf=None, public=Fal
:param patterns: same as :class:`.OpenAPISchemaGenerator`
:param urlconf: same as :class:`.OpenAPISchemaGenerator`
:param bool public: if False, includes only the endpoints that are accesible by the user viewing the schema
:param list validators: a list of validator names to apply; allowed values are ``flex``, ``ssv``
:param list validators: a list of validator names to apply; the only allowed value is ``ssv``, for now
:param type generator_class: schema generator class to use; should be a subclass of :class:`.OpenAPISchemaGenerator`
:param tuple authentication_classes: authentication classes for the schema view itself
:param tuple permission_classes: permission classes for the schema view itself

View File

@ -0,0 +1,18 @@
# Generated by Django 2.1.5 on 2019-03-02 03:51
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('articles', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='article',
name='read_only_nullable',
field=models.CharField(blank=True, max_length=20, null=True),
),
]

View File

@ -20,6 +20,7 @@ class Article(models.Model):
on_delete=models.PROTECT)
original_group = models.ForeignKey('ArticleGroup', related_name='articles_as_original', blank=True, default=None,
on_delete=models.PROTECT)
read_only_nullable = models.CharField(max_length=20, null=True, blank=True)
class ArticleGroup(models.Model):

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 articles.models import Article, ArticleGroup
@ -17,10 +17,9 @@ class ArticleSerializer(serializers.ModelSerializer):
class Meta:
model = Article
fields = ('title', 'author', 'body', 'slug', 'date_created', 'date_modified',
fields = ('title', 'author', 'body', 'slug', 'date_created', 'date_modified', 'read_only_nullable',
'references', 'uuid', 'cover', 'cover_name', 'article_type', 'group', 'original_group', )
read_only_fields = ('date_created', 'date_modified',
'references', 'uuid', 'cover_name')
read_only_fields = ('date_created', 'date_modified', 'references', 'uuid', 'cover_name', 'read_only_nullable')
lookup_field = 'slug'
extra_kwargs = {
'body': {'help_text': 'body serializer help_text'},
@ -29,6 +28,7 @@ class ArticleSerializer(serializers.ModelSerializer):
'help_text': _("The ID of the user that created this article; if none is provided, "
"defaults to the currently logged in user.")
},
'read_only_nullable': {'allow_null': True},
}

View File

@ -1,13 +1,11 @@
import datetime
import functools
from django.utils.decorators import method_decorator
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework import viewsets
# noinspection PyDeprecation
from rest_framework.filters import OrderingFilter
from rest_framework.pagination import LimitOffsetPagination
from rest_framework.parsers import MultiPartParser
from rest_framework.parsers import FileUploadParser, MultiPartParser
from rest_framework.response import Response
from articles import serializers
@ -93,17 +91,10 @@ class ArticleViewSet(viewsets.ModelViewSet):
swagger_schema = NoTitleAutoSchema
try:
from rest_framework.decorators import action
list_route = functools.partial(action, detail=False)
detail_route = functools.partial(action, detail=True)
except ImportError:
# TODO: remove when dropping support for DRF 3.7
action = None
from rest_framework.decorators import list_route, detail_route
@swagger_auto_schema(auto_schema=NoPagingAutoSchema, filter_inspectors=[DjangoFilterDescriptionInspector])
@list_route(methods=['get'])
@action(detail=False, methods=['get'])
def today(self, request):
today_min = datetime.datetime.combine(datetime.date.today(), datetime.time.min)
today_max = datetime.datetime.combine(datetime.date.today(), datetime.time.max)
@ -118,7 +109,7 @@ class ArticleViewSet(viewsets.ModelViewSet):
type=openapi.TYPE_INTEGER,
description="this should not crash (form parameter on DELETE method)"
)])
@detail_route(methods=['get', 'post', 'delete'], parser_classes=(MultiPartParser,))
@action(detail=True, methods=['get', 'post', 'delete'], parser_classes=(MultiPartParser, FileUploadParser))
def image(self, request, slug=None):
"""
image method docstring

View File

@ -1,13 +1,19 @@
from rest_framework import viewsets
from rest_framework.pagination import BasePagination
from .models import Identity, Person
from .serializers import IdentitySerializer, PersonSerializer
class UnknownPagination(BasePagination):
paginator_query_args = ['unknown_paginator']
class PersonViewSet(viewsets.ModelViewSet):
model = Person
queryset = Person.objects
serializer_class = PersonSerializer
pagination_class = UnknownPagination
class IdentityViewSet(viewsets.ModelViewSet):

View File

@ -0,0 +1,24 @@
# Generated by Django 2.1.7 on 2019-03-16 14:06
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('snippets', '0002_auto_20181219_1016'),
]
operations = [
migrations.CreateModel(
name='SnippetViewer',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('snippet', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='viewers', to='snippets.Snippet')),
('viewer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='snippet_views', to=settings.AUTH_USER_MODEL)),
],
),
]

View File

@ -0,0 +1,23 @@
# Generated by Django 2.2.2 on 2019-06-12 22:54
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('snippets', '0003_snippetviewer'),
]
operations = [
migrations.AlterField(
model_name='snippet',
name='language',
field=models.CharField(choices=[('cpp', 'cpp'), ('js', 'js'), ('python', 'python')], default='python', max_length=100),
),
migrations.AlterField(
model_name='snippet',
name='style',
field=models.CharField(choices=[('monokai', 'monokai'), ('solarized-dark', 'solarized-dark'), ('vim', 'vim')], default='solarized-dark', max_length=100),
),
]

View File

@ -1,10 +1,7 @@
from django.db import models
from pygments.lexers import get_all_lexers
from pygments.styles import get_all_styles
LEXERS = [item for item in get_all_lexers() if item[1]]
LANGUAGE_CHOICES = sorted([(item[1][0], item[0]) for item in LEXERS])
STYLE_CHOICES = sorted((item, item) for item in get_all_styles())
LANGUAGE_CHOICES = sorted((item, item) for item in ('cpp', 'python', 'js'))
STYLE_CHOICES = sorted((item, item) for item in ('solarized-dark', 'monokai', 'vim'))
class Snippet(models.Model):
@ -14,7 +11,12 @@ class Snippet(models.Model):
code = models.TextField(help_text="code model help text")
linenos = models.BooleanField(default=False)
language = models.CharField(choices=LANGUAGE_CHOICES, default='python', max_length=100)
style = models.CharField(choices=STYLE_CHOICES, default='friendly', max_length=100)
style = models.CharField(choices=STYLE_CHOICES, default='solarized-dark', max_length=100)
class Meta:
ordering = ('created',)
class SnippetViewer(models.Model):
snippet = models.ForeignKey(Snippet, on_delete=models.CASCADE, related_name='viewers')
viewer = models.ForeignKey('auth.User', related_name='snippet_views', on_delete=models.CASCADE)

View File

@ -1,22 +1,29 @@
from decimal import Decimal
import rest_framework
from django.contrib.auth import get_user_model
from packaging.version import Version
from rest_framework import serializers
from rest_framework.compat import MinValueValidator
from snippets.models import LANGUAGE_CHOICES, STYLE_CHOICES, Snippet
from snippets.models import LANGUAGE_CHOICES, STYLE_CHOICES, Snippet, SnippetViewer
if Version(rest_framework.__version__) < Version('3.10'):
from rest_framework.compat import MaxLengthValidator, MinValueValidator
else:
from django.core.validators import MaxLengthValidator, MinValueValidator
class LanguageSerializer(serializers.Serializer):
name = serializers.ChoiceField(
choices=LANGUAGE_CHOICES, default='python', help_text='The name of the programming language')
read_only_nullable = serializers.CharField(read_only=True, allow_null=True)
class Meta:
ref_name = None
class ExampleProjectSerializer(serializers.Serializer):
project_name = serializers.CharField(help_text='Name of the project')
project_name = serializers.CharField(label='project name custom title', help_text='Name of the project')
github_repo = serializers.CharField(required=True, help_text='Github repository of the project')
class Meta:
@ -65,11 +72,13 @@ class SnippetSerializer(serializers.Serializer):
)
title = serializers.CharField(required=False, allow_blank=True, max_length=100)
code = serializers.CharField(style={'base_template': 'textarea.html'})
tags = serializers.ListField(child=serializers.CharField(min_length=2), min_length=3, max_length=15)
linenos = serializers.BooleanField(required=False)
language = LanguageSerializer(help_text="Sample help text for language")
styles = serializers.MultipleChoiceField(choices=STYLE_CHOICES, default=['friendly'])
styles = serializers.MultipleChoiceField(choices=STYLE_CHOICES, default=['solarized-dark'])
lines = serializers.ListField(child=serializers.IntegerField(), allow_empty=True, allow_null=True, required=False)
example_projects = serializers.ListSerializer(child=ExampleProjectSerializer(), read_only=True)
example_projects = serializers.ListSerializer(child=ExampleProjectSerializer(), read_only=True,
validators=[MaxLengthValidator(100)])
difficulty_factor = serializers.FloatField(help_text="this is here just to test FloatField",
read_only=True, default=lambda: 6.9)
rate_as_string = serializers.DecimalField(max_digits=6, decimal_places=3, default=Decimal('0.0'),
@ -97,3 +106,9 @@ class SnippetSerializer(serializers.Serializer):
instance.style = validated_data.get('style', instance.style)
instance.save()
return instance
class SnippetViewerSerializer(serializers.ModelSerializer):
class Meta:
model = SnippetViewer
fields = '__all__'

View File

@ -8,10 +8,13 @@ if django.VERSION[:2] >= (2, 0):
urlpatterns = [
path('', views.SnippetList.as_view()),
path('<int:pk>/', views.SnippetDetail.as_view()),
path('views/<int:snippet_pk>/', views.SnippetViewerList.as_view()),
]
else:
from django.conf.urls import url
urlpatterns = [
url('^$', views.SnippetList.as_view()),
url(r'^(?P<pk>\d+)/$', views.SnippetDetail.as_view()),
url(r'^views/(?P<snippet_pk>\d+)/$', views.SnippetViewerList.as_view()),
]

View File

@ -2,13 +2,15 @@ from djangorestframework_camel_case.parser import CamelCaseJSONParser
from djangorestframework_camel_case.render import CamelCaseJSONRenderer
from inflection import camelize
from rest_framework import generics, status
from rest_framework.parsers import FormParser
from rest_framework.generics import get_object_or_404
from rest_framework.pagination import PageNumberPagination
from rest_framework.parsers import FileUploadParser, FormParser
from drf_yasg import openapi
from drf_yasg.inspectors import SwaggerAutoSchema
from drf_yasg.utils import swagger_auto_schema
from snippets.models import Snippet
from snippets.serializers import SnippetSerializer
from snippets.models import Snippet, SnippetViewer
from snippets.serializers import SnippetSerializer, SnippetViewerSerializer
class CamelCaseOperationIDAutoSchema(SwaggerAutoSchema):
@ -22,7 +24,7 @@ class SnippetList(generics.ListCreateAPIView):
queryset = Snippet.objects.all()
serializer_class = SnippetSerializer
parser_classes = (FormParser, CamelCaseJSONParser,)
parser_classes = (FormParser, CamelCaseJSONParser, FileUploadParser)
renderer_classes = (CamelCaseJSONRenderer,)
swagger_schema = CamelCaseOperationIDAutoSchema
@ -93,3 +95,31 @@ class SnippetDetail(generics.RetrieveUpdateDestroyAPIView):
def delete(self, request, *args, **kwargs):
"""delete method docstring"""
return super(SnippetDetail, self).patch(request, *args, **kwargs)
class SnippetViewerList(generics.ListAPIView):
"""SnippetViewerList classdoc"""
serializer_class = SnippetViewerSerializer
pagination_class = PageNumberPagination
parser_classes = (FormParser, CamelCaseJSONParser, FileUploadParser)
renderer_classes = (CamelCaseJSONRenderer,)
swagger_schema = CamelCaseOperationIDAutoSchema
lookup_url_kwarg = 'snippet_pk'
def get_object(self):
queryset = Snippet.objects.all()
# Perform the lookup filtering.
lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field
filter_kwargs = {self.lookup_field: self.kwargs[lookup_url_kwarg]}
obj = get_object_or_404(queryset, **filter_kwargs)
# May raise a permission denied
self.check_object_permissions(self.request, obj)
return obj
def get_queryset(self):
return SnippetViewer.objects.filter(snippet=self.get_object())

View File

@ -0,0 +1,11 @@
from drf_yasg import openapi
from drf_yasg.inspectors import NotHandled, PaginatorInspector
class UnknownPaginatorInspector(PaginatorInspector):
def get_paginator_parameters(self, paginator):
if hasattr(paginator, 'paginator_query_args'):
return [openapi.Parameter(name=arg, in_=openapi.IN_QUERY, type=openapi.TYPE_STRING)
for arg in getattr(paginator, 'paginator_query_args')]
return NotHandled

View File

@ -116,31 +116,36 @@ SWAGGER_SETTINGS = {
'type': 'basic'
},
'Bearer': {
'type': 'apiKey',
'in': 'header',
'name': 'Authorization',
'in': 'header'
},
'Query': {
'type': 'apiKey',
'name': 'auth',
'in': 'query'
},
'OAuth2 password': {
'type': 'oauth2',
'flow': 'password',
'tokenUrl': OAUTH2_TOKEN_URL,
'scopes': {
'read': 'Read everything.',
'write': 'Write everything,',
}
}
},
'tokenUrl': OAUTH2_TOKEN_URL,
'type': 'oauth2',
},
'Query': {
'in': 'query',
'name': 'auth',
'type': 'apiKey',
},
},
'OAUTH2_REDIRECT_URL': OAUTH2_REDIRECT_URL,
'OAUTH2_CONFIG': {
'clientId': OAUTH2_CLIENT_ID,
'clientSecret': OAUTH2_CLIENT_SECRET,
'appName': OAUTH2_APP_NAME,
}
},
"DEFAULT_PAGINATOR_INSPECTORS": [
'testproj.inspectors.UnknownPaginatorInspector',
'drf_yasg.inspectors.DjangoRestResponsePagination',
'drf_yasg.inspectors.CoreAPICompatInspector',
]
}
REDOC_SETTINGS = {
@ -188,16 +193,6 @@ LOGGING = {
'propagate': False,
},
'django': {
'handlers': ['console_log'],
'level': 'DEBUG',
'propagate': False,
},
'django.db.backends': {
'handlers': ['console_log'],
'level': 'INFO',
'propagate': False,
},
'django.template': {
'handlers': ['console_log'],
'level': 'INFO',
'propagate': False,

View File

@ -0,0 +1,21 @@
# Generated by Django 2.1.5 on 2019-04-01 00:28
from decimal import Decimal
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('todo', '0002_todotree'),
]
operations = [
migrations.CreateModel(
name='Pack',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('size_code', models.DecimalField(choices=[(Decimal('50'), '5x10'), (Decimal('100'), '10x10'), (Decimal('200'), '10x20')], decimal_places=3, default=Decimal('200'), max_digits=7)),
],
),
]

View File

@ -1,3 +1,5 @@
from decimal import Decimal
from django.db import models
@ -18,3 +20,19 @@ class TodoYetAnother(models.Model):
class TodoTree(models.Model):
parent = models.ForeignKey('self', on_delete=models.CASCADE, related_name='children', null=True)
title = models.CharField(max_length=50)
class Pack(models.Model):
SIZE_10x20 = Decimal(200.000)
SIZE_10x10 = Decimal(100.000)
SIZE_5x10 = Decimal(50.000)
size_code_choices = (
(SIZE_5x10, '5x10'),
(SIZE_10x10, '10x10'),
(SIZE_10x20, '10x20'),
)
size_code = models.DecimalField(max_digits=7,
decimal_places=3,
choices=size_code_choices,
default=SIZE_10x20)

View File

@ -4,7 +4,7 @@ from django.utils import timezone
from rest_framework import serializers
from rest_framework_recursive.fields import RecursiveField
from .models import Todo, TodoAnother, TodoTree, TodoYetAnother
from .models import Pack, Todo, TodoAnother, TodoTree, TodoYetAnother
class TodoSerializer(serializers.ModelSerializer):
@ -41,10 +41,11 @@ class TodoYetAnotherSerializer(serializers.ModelSerializer):
class TodoTreeSerializer(serializers.ModelSerializer):
children = serializers.ListField(child=RecursiveField(), source='children.all')
many_children = RecursiveField(many=True, source='children')
class Meta:
model = TodoTree
fields = ('id', 'title', 'children')
fields = ('id', 'title', 'children', 'many_children')
class TodoRecursiveSerializer(serializers.ModelSerializer):
@ -56,3 +57,14 @@ class TodoRecursiveSerializer(serializers.ModelSerializer):
class Meta:
model = TodoTree
fields = ('id', 'title', 'parent', 'parent_id')
class HarvestSerializer(serializers.ModelSerializer):
class Meta:
model = Pack
fields = (
'size_code',
)
read_only_fields = (
'size_code',
)

View File

@ -9,6 +9,7 @@ router.register(r'another', views.TodoAnotherViewSet)
router.register(r'yetanother', views.TodoYetAnotherViewSet)
router.register(r'tree', views.TodoTreeView)
router.register(r'recursive', views.TodoRecursiveView)
router.register(r'harvest', views.HarvestViewSet)
urlpatterns = router.urls

View File

@ -1,11 +1,13 @@
from rest_framework import viewsets
from rest_framework import mixins, permissions, viewsets
from rest_framework.authentication import TokenAuthentication
from rest_framework.generics import RetrieveAPIView
from drf_yasg.utils import swagger_auto_schema
from .models import Todo, TodoAnother, TodoTree, TodoYetAnother
from .models import Pack, Todo, TodoAnother, TodoTree, TodoYetAnother
from .serializer import (
TodoAnotherSerializer, TodoRecursiveSerializer, TodoSerializer, TodoTreeSerializer, TodoYetAnotherSerializer
HarvestSerializer, TodoAnotherSerializer, TodoRecursiveSerializer, TodoSerializer, TodoTreeSerializer,
TodoYetAnotherSerializer
)
@ -75,3 +77,16 @@ class TodoRecursiveView(viewsets.ModelViewSet):
@swagger_auto_schema(responses={200: TodoRecursiveSerializer(many=True)})
def list(self, request, *args, **kwargs):
return super(TodoRecursiveView, self).list(request, *args, **kwargs)
class HarvestViewSet(mixins.ListModelMixin,
mixins.UpdateModelMixin,
viewsets.GenericViewSet):
queryset = Pack.objects.all()
serializer_class = HarvestSerializer
permission_classes = [permissions.IsAuthenticated]
authentication_classes = [TokenAuthentication]
def perform_update(self, serializer):
pass

View File

@ -3,7 +3,7 @@ import sys
from django.conf import settings
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):
@ -13,6 +13,7 @@ def add_default_user(apps, schema_editor):
User = apps.get_model(settings.AUTH_USER_MODEL)
try:
with transaction.atomic():
admin = User(
username=username,
email=email,

View File

@ -1,3 +1,5 @@
import sys
from django.contrib.auth.models import User
from rest_framework import serializers
@ -6,7 +8,10 @@ from snippets.models import Snippet
try:
import typing # noqa: F401
if sys.version_info >= (3, 4):
from .method_serializers_with_typing import MethodFieldExampleSerializer
else:
from .method_serializers_without_typing import MethodFieldExampleSerializer
except ImportError:
from .method_serializers_without_typing import MethodFieldExampleSerializer

View File

@ -57,10 +57,15 @@ def swagger_dict(swagger, codec_json):
@pytest.fixture
def validate_schema():
def validate_schema(swagger):
try:
from flex.core import parse as validate_flex
from swagger_spec_validator.validator20 import validate_spec as validate_ssv
validate_flex(copy.deepcopy(swagger))
except ImportError:
pass
from swagger_spec_validator.validator20 import validate_spec as validate_ssv
validate_ssv(copy.deepcopy(swagger))
return validate_schema

View File

@ -1,11 +1,14 @@
swagger: '2.0'
info:
title: Snippets API
description: "This is a demo project for the [drf-yasg](https://github.com/axnsan12/drf-yasg)\
\ Django Rest Framework library.\n\nThe `swagger-ui` view can be found [here](/cached/swagger).\
\ \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 pre-existing `admin` user with password `passwordadmin`."
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`.
termsOfService: https://www.google.com/policies/terms/
contact:
email: contact@snippets.local
@ -276,7 +279,10 @@ paths:
get:
operationId: people_list
description: ''
parameters: []
parameters:
- name: unknown_paginator
in: query
type: string
responses:
'200':
description: ''
@ -440,6 +446,46 @@ paths:
tags:
- snippets
parameters: []
/snippets/views/{snippet_pk}/:
get:
operationId: snippetsViewsRead
description: SnippetViewerList classdoc
parameters:
- name: page
in: query
description: A page number within the paginated result set.
required: false
type: integer
responses:
'200':
description: ''
schema:
required:
- count
- results
type: object
properties:
count:
type: integer
next:
type: string
format: uri
x-nullable: true
previous:
type: string
format: uri
x-nullable: true
results:
type: array
items:
$ref: '#/definitions/SnippetViewer'
tags:
- snippets
parameters:
- name: snippet_pk
in: path
required: true
type: string
/snippets/{id}/:
get:
operationId: snippetsRead
@ -552,6 +598,60 @@ paths:
description: A unique integer value identifying this todo another.
required: true
type: integer
/todo/harvest/:
get:
operationId: todo_harvest_list
description: ''
parameters: []
responses:
'200':
description: ''
schema:
type: array
items:
$ref: '#/definitions/Harvest'
tags:
- todo
parameters: []
/todo/harvest/{id}/:
put:
operationId: todo_harvest_update
description: ''
parameters:
- name: data
in: body
required: true
schema:
$ref: '#/definitions/Harvest'
responses:
'200':
description: ''
schema:
$ref: '#/definitions/Harvest'
tags:
- todo
patch:
operationId: todo_harvest_partial_update
description: ''
parameters:
- name: data
in: body
required: true
schema:
$ref: '#/definitions/Harvest'
responses:
'200':
description: ''
schema:
$ref: '#/definitions/Harvest'
tags:
- todo
parameters:
- name: id
in: path
description: A unique integer value identifying this pack.
required: true
type: integer
/todo/recursive/:
get:
operationId: todo_recursive_list
@ -902,6 +1002,11 @@ definitions:
type: string
format: date-time
readOnly: true
read_only_nullable:
type: string
readOnly: true
minLength: 1
x-nullable: true
references:
description: this is a really bad example
type: object
@ -1011,7 +1116,7 @@ definitions:
type: object
properties:
projectName:
title: Project name
title: project name custom title
description: Name of the project
type: string
minLength: 1
@ -1023,6 +1128,7 @@ definitions:
Snippet:
required:
- code
- tags
- language
type: object
properties:
@ -1057,6 +1163,13 @@ definitions:
title: Code
type: string
minLength: 1
tags:
type: array
items:
type: string
minLength: 2
maxItems: 15
minItems: 3
linenos:
title: Linenos
type: boolean
@ -1069,480 +1182,26 @@ definitions:
description: The name of the programming language
type: string
enum:
- abap
- abnf
- ada
- adl
- agda
- aheui
- ahk
- alloy
- ampl
- antlr
- antlr-as
- antlr-cpp
- antlr-csharp
- antlr-java
- antlr-objc
- antlr-perl
- antlr-python
- antlr-ruby
- apacheconf
- apl
- applescript
- arduino
- as
- as3
- aspectj
- aspx-cs
- aspx-vb
- asy
- at
- autoit
- awk
- basemake
- bash
- bat
- bbcode
- bc
- befunge
- bib
- blitzbasic
- blitzmax
- bnf
- boo
- boogie
- brainfuck
- bro
- bst
- bugs
- c
- c-objdump
- ca65
- cadl
- camkes
- capdl
- capnp
- cbmbas
- ceylon
- cfc
- cfengine3
- cfm
- cfs
- chai
- chapel
- cheetah
- cirru
- clay
- clean
- clojure
- clojurescript
- cmake
- cobol
- cobolfree
- coffee-script
- common-lisp
- componentpascal
- console
- control
- coq
- cpp
- cpp-objdump
- cpsa
- cr
- crmsh
- croc
- cryptol
- csharp
- csound
- csound-document
- csound-score
- css
- css+django
- css+erb
- css+genshitext
- css+lasso
- css+mako
- css+mozpreproc
- css+myghty
- css+php
- css+smarty
- cucumber
- cuda
- cypher
- cython
- d
- d-objdump
- dart
- delphi
- dg
- diff
- django
- docker
- doscon
- dpatch
- dtd
- duel
- dylan
- dylan-console
- dylan-lid
- earl-grey
- easytrieve
- ebnf
- ec
- ecl
- eiffel
- elixir
- elm
- emacs
- erb
- erl
- erlang
- evoque
- extempore
- ezhil
- factor
- fan
- fancy
- felix
- fennel
- fish
- flatline
- forth
- fortran
- fortranfixed
- foxpro
- fsharp
- gap
- gas
- genshi
- genshitext
- glsl
- gnuplot
- go
- golo
- gooddata-cl
- gosu
- groff
- groovy
- gst
- haml
- handlebars
- haskell
- haxeml
- hexdump
- hlsl
- hsail
- html
- html+cheetah
- html+django
- html+evoque
- html+genshi
- html+handlebars
- html+lasso
- html+mako
- html+myghty
- html+ng2
- html+php
- html+smarty
- html+twig
- html+velocity
- http
- hx
- hybris
- hylang
- i6t
- idl
- idris
- iex
- igor
- inform6
- inform7
- ini
- io
- ioke
- irc
- isabelle
- j
- jags
- jasmin
- java
- javascript+mozpreproc
- jcl
- jlcon
- js
- js+cheetah
- js+django
- js+erb
- js+genshitext
- js+lasso
- js+mako
- js+myghty
- js+php
- js+smarty
- jsgf
- json
- json-object
- jsonld
- jsp
- julia
- juttle
- kal
- kconfig
- koka
- kotlin
- lagda
- lasso
- lcry
- lean
- less
- lhs
- lidr
- lighty
- limbo
- liquid
- live-script
- llvm
- logos
- logtalk
- lsl
- lua
- make
- mako
- maql
- mask
- mason
- mathematica
- matlab
- matlabsession
- md
- minid
- modelica
- modula2
- monkey
- monte
- moocode
- moon
- mozhashpreproc
- mozpercentpreproc
- mql
- mscgen
- mupad
- mxml
- myghty
- mysql
- nasm
- ncl
- nemerle
- nesc
- newlisp
- newspeak
- ng2
- nginx
- nim
- nit
- nixos
- nsis
- numpy
- nusmv
- objdump
- objdump-nasm
- objective-c
- objective-c++
- objective-j
- ocaml
- octave
- odin
- ooc
- opa
- openedge
- pacmanconf
- pan
- parasail
- pawn
- perl
- perl6
- php
- pig
- pike
- pkgconfig
- plpgsql
- postgresql
- postscript
- pot
- pov
- powershell
- praat
- prolog
- properties
- protobuf
- ps1con
- psql
- pug
- puppet
- py3tb
- pycon
- pypylog
- pytb
- python
- python3
- qbasic
- qml
- qvto
- racket
- ragel
- ragel-c
- ragel-cpp
- ragel-d
- ragel-em
- ragel-java
- ragel-objc
- ragel-ruby
- raw
- rb
- rbcon
- rconsole
- rd
- rebol
- red
- redcode
- registry
- resource
- rexx
- rhtml
- rnc
- roboconf-graph
- roboconf-instances
- robotframework
- rql
- rsl
- rst
- rts
- rust
- sas
- sass
- sc
- scala
- scaml
- scheme
- scilab
- scss
- shen
- silver
- slim
- smali
- smalltalk
- smarty
- sml
- snobol
- snowball
- sourceslist
- sp
- sparql
- spec
- splus
- sql
- sqlite3
- squidconf
- ssp
- stan
- stata
- swift
- swig
- systemverilog
- tads3
- tap
- tasm
- tcl
- tcsh
- tcshcon
- tea
- termcap
- terminfo
- terraform
- tex
- text
- thrift
- todotxt
- trac-wiki
- treetop
- ts
- tsql
- turtle
- twig
- typoscript
- typoscriptcssdata
- typoscripthtmldata
- urbiscript
- vala
- vb.net
- vcl
- vclsnippets
- vctreestatus
- velocity
- verilog
- vgl
- vhdl
- vim
- wdiff
- whiley
- x10
- xml
- xml+cheetah
- xml+django
- xml+erb
- xml+evoque
- xml+lasso
- xml+mako
- xml+myghty
- xml+php
- xml+smarty
- xml+velocity
- xorg.conf
- xquery
- xslt
- xtend
- xul+mozpreproc
- yaml
- yaml+jinja
- zephir
default: python
readOnlyNullable:
title: Read only nullable
type: string
readOnly: true
minLength: 1
x-nullable: true
styles:
type: array
items:
type: string
enum:
- abap
- algol
- algol_nu
- arduino
- autumn
- borland
- bw
- colorful
- default
- emacs
- friendly
- fruity
- igor
- lovelace
- manni
- monokai
- murphy
- native
- paraiso-dark
- paraiso-light
- pastie
- perldoc
- rainbow_dash
- rrt
- tango
- trac
- solarized-dark
- vim
- vs
- xcode
default:
- friendly
- solarized-dark
lines:
type: array
items:
@ -1553,6 +1212,7 @@ definitions:
items:
$ref: '#/definitions/Project'
readOnly: true
maxItems: 100
difficultyFactor:
title: Difficulty factor
description: this is here just to test FloatField
@ -1570,6 +1230,22 @@ definitions:
format: decimal
default: 0.0
minimum: 0.0
SnippetViewer:
required:
- snippet
- viewer
type: object
properties:
id:
title: ID
type: integer
readOnly: true
snippet:
title: Snippet
type: integer
viewer:
title: Viewer
type: integer
Todo:
required:
- title
@ -1593,6 +1269,17 @@ definitions:
minLength: 1
todo:
$ref: '#/definitions/Todo'
Harvest:
type: object
properties:
size_code:
title: Size code
type: string
enum:
- '50'
- '100'
- '200'
readOnly: true
TodoRecursive:
required:
- title
@ -1617,6 +1304,7 @@ definitions:
required:
- title
- children
- many_children
type: object
properties:
id:
@ -1632,6 +1320,10 @@ definitions:
type: array
items:
$ref: '#/definitions/TodoTree'
many_children:
type: array
items:
$ref: '#/definitions/TodoTree'
TodoYetAnother:
required:
- title
@ -1813,7 +1505,9 @@ definitions:
readOnly: true
help_text_example_3:
title: Help text example 3
description: "\n docstring is set so should appear in swagger as fallback\n\
\ :return:\n "
description: |2
docstring is set so should appear in swagger as fallback
:return:
type: integer
readOnly: true

View File

@ -0,0 +1,43 @@
import uuid
import pytest
from drf_yasg import openapi
from drf_yasg.inspectors.field import get_basic_type_info_from_hint
try:
import typing
from typing import Dict, List, Union, Optional, Set
except ImportError:
typing = None
if typing:
@pytest.mark.parametrize('hint_class, expected_swagger_type_info', [
(int, {'type': openapi.TYPE_INTEGER, 'format': None}),
(str, {'type': openapi.TYPE_STRING, 'format': None}),
(bool, {'type': openapi.TYPE_BOOLEAN, 'format': None}),
(dict, {'type': openapi.TYPE_OBJECT, 'format': None}),
(Dict[int, int], {'type': openapi.TYPE_OBJECT, 'format': None}),
(uuid.UUID, {'type': openapi.TYPE_STRING, 'format': openapi.FORMAT_UUID}),
(List[int], {'type': openapi.TYPE_ARRAY, 'items': openapi.Items(openapi.TYPE_INTEGER)}),
(List[str], {'type': openapi.TYPE_ARRAY, 'items': openapi.Items(openapi.TYPE_STRING)}),
(List[bool], {'type': openapi.TYPE_ARRAY, 'items': openapi.Items(openapi.TYPE_BOOLEAN)}),
(Set[int], {'type': openapi.TYPE_ARRAY, 'items': openapi.Items(openapi.TYPE_INTEGER)}),
(Optional[bool], {'type': openapi.TYPE_BOOLEAN, 'format': None, 'x-nullable': True}),
(Optional[List[int]], {
'type': openapi.TYPE_ARRAY, 'items': openapi.Items(openapi.TYPE_INTEGER), 'x-nullable': True
}),
(Union[List[int], type(None)], {
'type': openapi.TYPE_ARRAY, 'items': openapi.Items(openapi.TYPE_INTEGER), 'x-nullable': True
}),
# Following cases are not 100% correct, but it should work somehow and not crash.
(Union[int, float], None),
(List, {'type': openapi.TYPE_ARRAY, 'items': openapi.Items(openapi.TYPE_STRING)}),
('SomeType', None),
(type('SomeType', (object,), {}), None),
(None, None),
(6, None),
])
def test_get_basic_type_info_from_hint(hint_class, expected_swagger_type_info):
type_info = get_basic_type_info_from_hint(hint_class)
assert type_info == expected_swagger_type_info

View File

@ -5,7 +5,7 @@ from drf_yasg.openapi import ReferenceResolver
def test_basic():
scopes = ['s1', 's2']
rr = ReferenceResolver(*scopes)
rr = ReferenceResolver(*scopes, force_init=True)
assert scopes == rr.scopes == list(rr.keys()) == list(rr)
rr.set('o1', 1, scope='s1')
assert rr.has('o1', scope='s1')
@ -25,7 +25,7 @@ def test_basic():
def test_scoped():
scopes = ['s1', 's2']
rr = ReferenceResolver(*scopes)
rr = ReferenceResolver(*scopes, force_init=True)
r1 = rr.with_scope('s1')
r2 = rr.with_scope('s2')
with pytest.raises(AssertionError):

View File

@ -1,9 +1,13 @@
import json
import sys
from collections import OrderedDict
import pytest
from django.conf.urls import url
from django.contrib.postgres import fields as postgres_fields
from django.db import models
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.decorators import api_view
from rest_framework.response import Response
@ -14,6 +18,11 @@ from drf_yasg.errors import SwaggerGenerationError
from drf_yasg.generators import OpenAPISchemaGenerator
from drf_yasg.utils import swagger_auto_schema
try:
import typing
except ImportError:
typing = None
def test_schema_is_valid(swagger, codec_yaml):
codec_yaml.encode(swagger)
@ -204,6 +213,7 @@ def test_action_mapping():
@pytest.mark.parametrize('choices, expected_type', [
(['A', 'B'], openapi.TYPE_STRING),
([u'A', u'B'], openapi.TYPE_STRING),
([123, 456], openapi.TYPE_INTEGER),
([1.2, 3.4], openapi.TYPE_NUMBER),
(['A', 456], openapi.TYPE_STRING)
@ -229,3 +239,116 @@ def test_choice_field(choices, expected_type):
property_schema = swagger['definitions']['Detail']['properties']['detail']
assert property_schema == openapi.Schema(title='Detail', type=expected_type, enum=choices)
@pytest.mark.parametrize('choices, field, expected_type', [
([1, 2, 3], models.IntegerField, openapi.TYPE_INTEGER),
(["A", "B"], models.CharField, openapi.TYPE_STRING),
])
def test_nested_choice_in_array_field(choices, field, expected_type):
# Create a model class on the fly to avoid warnings about using the several
# model class name several times
model_class = type(
"%sModel" % field.__name__,
(fake_models.FakeModel,),
{
"array": postgres_fields.ArrayField(
field(choices=((i, "choice %s" % i) for i in choices))
),
"__module__": "test_models",
}
)
class ArraySerializer(serializers.ModelSerializer):
class Meta:
model = model_class
fields = ("array",)
class ArrayViewSet(viewsets.ModelViewSet):
serializer_class = ArraySerializer
router = routers.DefaultRouter()
router.register(r'arrays', ArrayViewSet, **_basename_or_base_name('arrays'))
generator = OpenAPISchemaGenerator(
info=openapi.Info(title='Test array model generator', default_version='v1'),
patterns=router.urls
)
swagger = generator.get_schema(None, True)
property_schema = swagger['definitions']['Array']['properties']['array']['items']
assert property_schema == openapi.Schema(title='Array', type=expected_type, enum=choices)
def test_json_field():
class TestJSONFieldSerializer(serializers.Serializer):
json = serializers.JSONField()
class JSONViewSet(viewsets.ModelViewSet):
serializer_class = TestJSONFieldSerializer
router = routers.DefaultRouter()
router.register(r'jsons', JSONViewSet, **_basename_or_base_name('jsons'))
generator = OpenAPISchemaGenerator(
info=openapi.Info(title='Test json field generator', default_version='v1'),
patterns=router.urls
)
swagger = generator.get_schema(None, True)
property_schema = swagger["definitions"]["TestJSONField"]["properties"]["json"]
assert property_schema == openapi.Schema(title='Json', type=openapi.TYPE_OBJECT)
@pytest.mark.parametrize('py_type, expected_type', [
(str, openapi.TYPE_STRING),
(int, openapi.TYPE_INTEGER),
(float, openapi.TYPE_NUMBER),
(bool, openapi.TYPE_BOOLEAN),
])
@pytest.mark.skipif(typing is None or sys.version_info.major < 3, reason="typing not supported")
def test_optional_return_type(py_type, expected_type):
class OptionalMethodSerializer(serializers.Serializer):
x = serializers.SerializerMethodField()
def get_x(self, instance):
pass
# Add the type annotation here in order to avoid a SyntaxError in py27
get_x.__annotations__["return"] = typing.Optional[py_type]
class OptionalMethodViewSet(viewsets.ViewSet):
@swagger_auto_schema(responses={200: openapi.Response("OK", OptionalMethodSerializer)})
def retrieve(self, request, pk=None):
return Response({'optional': None})
router = routers.DefaultRouter()
router.register(r'optional', OptionalMethodViewSet, **_basename_or_base_name('optional'))
generator = OpenAPISchemaGenerator(
info=openapi.Info(title='Test optional parameter', default_version='v1'),
patterns=router.urls
)
swagger = generator.get_schema(None, True)
property_schema = swagger["definitions"]["OptionalMethod"]["properties"]["x"]
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

35
tox.ini
View File

@ -5,10 +5,11 @@ isolated_build_env=.package
# https://docs.djangoproject.com/en/dev/faq/install/#what-python-version-can-i-use-with-django
envlist =
py{27,34,35,36}-django111-drf{37,38,39},
py{34,35,36,37}-django20-drf{37,38,39},
py{35,36,37}-django21-drf{37,38,39},
py36-django{111,22}-drf{38,39},
py37-django22-drf{38,39,310,311},
py38-django{22,3}-drf{310,311},
djmaster, lint, docs
skip_missing_interpreters = true
[testenv:.package]
# no additional dependencies besides PEP 517
@ -17,16 +18,30 @@ deps =
[testenv]
deps =
django111: Django>=1.11,<2.0
django20: Django>=2.0,<2.1
django111: django-oauth-toolkit>=1.1.0,<1.2.0
django21: Django>=2.1,<2.2
django21: django-oauth-toolkit>=1.2.0
drf37: djangorestframework>=3.7.7,<3.8
drf38: djangorestframework>=3.8.0,<3.9
django22: Django>=2.2,<2.3
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
drf39: djangorestframework>=3.9,<3.10
drf310: djangorestframework>=3.10,<3.11
drf311: djangorestframework>=3.11,<3.12
# test with the latest build of django-rest-framework to get early warning of compatibility issues
djmaster: https://github.com/encode/django-rest-framework/archive/master.tar.gz
typing: typing>=3.6.6
# test with the latest builds of Django and django-rest-framework
# to get early warning of compatibility issues
djmaster: https://github.com/django/django/archive/master.tar.gz
djmaster: https://github.com/ottoyiu/django-cors-headers/archive/master.tar.gz
djmaster: https://github.com/encode/django-rest-framework/archive/master.tar.gz
djmaster: django-oauth-toolkit>=1.2.0
# other dependencies
-r requirements/validation.txt
@ -72,6 +87,6 @@ known_standard_library =
types,warnings
known_third_party =
coreapi,coreschema,datadiff,dj_database_url,django,django_filters,djangorestframework_camel_case,
rest_framework_recursive,flex,gunicorn,inflection,pygments,pytest,rest_framework,ruamel,setuptools_scm,
swagger_spec_validator,uritemplate,user_agents,whitenoise,oauth2_provider
rest_framework_recursive,flex,gunicorn,inflection,pytest,rest_framework,ruamel,setuptools_scm,
swagger_spec_validator,uritemplate,user_agents,whitenoise,oauth2_provider,packaging
known_first_party = drf_yasg,testproj,articles,people,snippets,todo,users,urlconfs