pax_global_header00006660000000000000000000000064145357215040014520gustar00rootroot0000000000000052 comment=9926bbaf23927bedadafbe2e2c99aa4fda7b691a drf-spectacular-0.27.0/000077500000000000000000000000001453572150400146655ustar00rootroot00000000000000drf-spectacular-0.27.0/.github/000077500000000000000000000000001453572150400162255ustar00rootroot00000000000000drf-spectacular-0.27.0/.github/ISSUE_TEMPLATE/000077500000000000000000000000001453572150400204105ustar00rootroot00000000000000drf-spectacular-0.27.0/.github/ISSUE_TEMPLATE/bug_report.md000066400000000000000000000005571453572150400231110ustar00rootroot00000000000000--- name: Bug report about: Create a report to help us improve title: '' labels: '' assignees: '' --- **Describe the bug** A clear and concise description of what the bug is. **To Reproduce** It would be most helpful to provide a small snippet to see how the bug was provoked. **Expected behavior** A clear and concise description of what you expected to happen. drf-spectacular-0.27.0/.github/workflows/000077500000000000000000000000001453572150400202625ustar00rootroot00000000000000drf-spectacular-0.27.0/.github/workflows/ci.yml000066400000000000000000000027741453572150400214120ustar00rootroot00000000000000name: CI on: [ push, pull_request ] jobs: prep-tests: runs-on: ubuntu-latest outputs: matrix: ${{ steps.set-vars.outputs.matrix }} date: ${{ steps.set-vars.outputs.date }} steps: - uses: actions/checkout@v3 - uses: actions/setup-python@v4 with: python-version: '3.10' - run: pip install tox - name: Generate matrix & vars id: set-vars run: python helper/github-ci-vars.py tests: needs: prep-tests runs-on: ubuntu-20.04 name: tests-${{ matrix.setup.toxenv }} continue-on-error: ${{ matrix.setup.experimental }} strategy: matrix: setup: ${{ fromJson(needs.prep-tests.outputs.matrix) }} steps: - uses: actions/checkout@v3 - uses: actions/cache@v3 with: path: ~/.cache/pip key: pip-${{ needs.prep-tests.date }} - uses: actions/setup-python@v4 with: python-version: ${{ matrix.setup.python-version }} - name: Install system dependencies run: | sudo apt-get update sudo apt-get install -y gdal-bin libsqlite3-mod-spatialite - name: Install tox run: pip install tox - name: Run Tox run: tox --skip-missing-interpreters=false -e ${{ matrix.setup.toxenv }} - uses: codecov/codecov-action@v3 with: name: ${{ matrix.setup.toxenv }} passed-tests: name: Required tests passed needs: [ tests ] runs-on: ubuntu-latest steps: - run: echo "All Done"drf-spectacular-0.27.0/.github/workflows/publish.yml000066400000000000000000000013541453572150400224560ustar00rootroot00000000000000# For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries name: Release Python Package to PyPi on: [ workflow_dispatch ] jobs: publish: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Set up Python uses: actions/setup-python@v2 with: python-version: '3.x' - name: Install dependencies run: | python -m pip install --upgrade pip pip install -r requirements/packaging.txt - name: Build and publish env: TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} run: | python setup.py publishdrf-spectacular-0.27.0/.gitignore000066400000000000000000000002461453572150400166570ustar00rootroot00000000000000*.pyc *.db *~ .* html/ htmlcov/ coverage/ build/ dist/ venv/ *.egg-info/ MANIFEST docs/_build/ bin/ include/ lib/ local/ generated_clients/ *_out.yml !.gitignore drf-spectacular-0.27.0/.readthedocs.yaml000066400000000000000000000012011453572150400201060ustar00rootroot00000000000000# Read the Docs configuration file for Sphinx projects # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details # Required version: 2 # Set the OS, Python version and other tools you might need build: os: ubuntu-22.04 tools: python: "3.11" # Build documentation in the "docs/" directory with Sphinx sphinx: configuration: docs/conf.py # Optional but recommended, declare the Python requirements required # to build your documentation # See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html python: install: - requirements: requirements/base.txt - requirements: requirements/docs.txtdrf-spectacular-0.27.0/CHANGELOG.rst000066400000000000000000002246111453572150400167140ustar00rootroot00000000000000Changelog ========= 0.27.0 (2023-12-12) ------------------- - improve mypy typing `#600 `_ - add django 5 to test suite and adapt to changes `#1126 `_ - Use correctly allowed http methods for schema generation [Jekel] - OAS 3.1 - Fix Enum collision with same choices & varying labels `#790 `_ `#1104 `_ - Undo adding middleware [Jelmer Draaijer] - Set JWTTokenUserAuthentication to None when missing [Jelmer Draaijer] - Add setuptools required for packaging [Jelmer Draaijer] - Add allauth.account.middleware.AccountMiddleware to middleware [Jelmer Draaijer] - Add Python 3.12 to test matrix and add classifiers [Jelmer Draaijer] - Add official support for pydantic decoration. - bugfix ignored OpenApiRequest case `#1106 `_ - JSONField may also be a non-object/primitive `#1095 `_ - add test for empty whitelist (no auth) `#1094 `_ - Avoid ChoiceField duplicate enum values for allow_null, allow_blank (`#1085 `_) [Marti Raudsepp] - add test for django-filter and ListAPIView `#1086 `_ - Fix the blueprint for pydantic version 2 [Carmen Alvarez] Breaking changes / important additions: - Biggest release in quite some time that contains a bunch of long running PR that finally found their way into master. - We now officially support OpenAPI 3.1 and Pydantic 2 - Quite a few bug fixes (thanks to all contributors) and improved typing 0.26.5 (2023-09-23) ------------------- - update FAQ entry on extension loading - Fix (`#1079 `_) crash when generating schema for field with UUID choices. [Pedro Borges] - chore: fix typos [Heinz-Alexander Fuetterer] - Use schema_url in SpectacularElementsView (`#1067 `_) [q0w] - add helper to disable viewset list detection `#1064 `_ - pin django-allauth test dep due to breaking change with dj-rest-auth - fix example building for pagination with basic list `#1055 `_ - Fix discarded falsy examples values `#1049 `_ Breaking changes / important additions: - Added helper function ``forced_singular_serializer`` to disable a list detection on a endpoint, that has been quite difficult to properly undo previously. This closes the functional gap for ``@extend_schema_serializer(many=False)`` in single-use (non-envelope) situations. - Several small bugfixes 0.26.4 (2023-07-23) ------------------- - fix django-polymorphic empty serializer case `#1029 `_ `#542 `_ - Add a blueprint for pydantic 2 [Carmen Alvarez] - bugfix exclude behavior on subclassing `#1025 `_ - relax django-filter subclassing restriction `#1022 `_ - factor out serializer name estimation for easier modification `#976 `_ - Fixing Pydantic Extension (`#1021 `_) [sydney-runkle] - add Authorization header for oauth2 Bearer token [Danial] - allow already supported lazy string in types `#982 `_ Breaking changes / important additions: - some minor bugfixes as well as improvements to ``django-filter`` and ``django-polymorphic``. - it is now significantly easier to adapt serializer naming via ``AutoSchema`` subclassing. 0.26.3 (2023-06-22) ------------------- - allow implicit list expansion of PolymorphicProxySerializer `#995 `_ - selectively distinguish real serializers from mocked ones `#1006 `_ - fix functionality gap for decoration of django-filter fields `#1007 `_ - add pydantic blueprint - robustify subclass check in extensions `#1006 `_ - Prevent exception for non-serializer classes targeted by SerializerExtensions `#1006 `_ - add middleware support for djangorestframework_camel_case - close functionality gap in drf dataclasses naming `#1004 `_ - fix: Camelize query parameters [v.kovalchuk] - docs(examples): Fix wrong bool value in example [schew2381] - bugfix test `#991 `_ - bugfix duplicate enum list for django-filter `#991 `_ - dj-rest-auth test changes 3.x -> 4.x - Add blocks to redoc template `#978 `_ Breaking changes / important additions: - no major changes but a multiude of small improvements. - we are now a lot more tolerant when it comes to writing extensions for non-standard classes (e.g. Pydantic). - there should be no unexpected schema changes except for when ``djangorestframework_camel_case``'s Middleware is used. 0.26.2 (2023-04-15) ------------------- - fix jwt cookie name settings not being recognised (`#972 `_) [Nix Siow] - Add OpenApiRequest for encoding options `#714 `_ `#965 `_ Breaking changes / important additions: - small bugfix release that also contains the new ``OpenApiRequest`` feature 0.26.1 (2023-03-18) ------------------- - reorder typed polymorphic fields `#958 `_ - Fix test warnings [Dmitry Gribanov] - Fix PolymorphicSerializer type field handling `#885 `_ `#958 `_ - Add PresentablePrimarKeyRelatedField schema for drf-exrta-fields blueprint [Đào Minh Hạt] - bugfix KeyError for disabled ENUM_GENERATE_CHOICE_DESCRIPTION `#952 `_ Breaking changes / important additions: - small bugfix release that addresses a issue when turning off choice description generation - improve/bugfix ``PolymorphicSerializer`` type field handling 0.26.0 (2023-03-04) ------------------- - honor djangorestframework_camel_case settings "ignore_keys" and "ignore_fields" `#945 `_ - If available, use docstrings from properties for field descriptions (`#954 `_) - Don't let validators override values already set in the schema (`#911 `_) [StopMotionCuber] - add test and another case to `#901 `_ - add enum key/value list to description string `#337 `_ `#403 `_ `#105 `_ `#563 `_ - Add option to provide a callable for PolymorphicProxySerializer.serializers [Glenn Matthews] - consolidate sort fix for enum sorting - add testcase to `#950 `_. ensure raw schema dict remains unmodified - Don't edit the original django-filters schema. [Will Giddens] - Fix typos and grammar errors in FAQ doc page. [Foad Lind] - fix OpenApiResponse nested example defaults `#875 `_ - mitigate ``runtests.py`` fail when GDAL library is not installed `#945 `_ `#821 `_ `#775 `_ `#777 `_ - bugfix SlugRelatedField with a model property target `#943 `_ - suppress erroneous warning for optional extensions `#940 `_ - fix whitelist mechanics (enables deny all) `#923 `_ - mitigate many=True with default array value `#936 `_ - fix dj-rest-auth>=3.0.0 breaking changes `#937 `_ - Update plumbing.py - add swagger UI template blocks for customization [Jan Lis] - Add support for drf ReturnList and ReturnDict hint [zengqiu] - add example/test for DynamicFieldsModelSerializer `#375 `_ `#912 `_ - adapt test schema for dj-rest-auth 2.2.6 - clarify docs for postproc hook mechanics `#908 `_ - Add test for custom serializer field pagination `#904 `_ - fix: let use a default value for foreignkey model field [Frederic de Zorzi] Breaking changes / important additions: - A lot of bug fixes and a few feature additions. - We now render a descriptive ``Enum`` key/value list into the description by default. Opt-out with new setting ``ENUM_GENERATE_CHOICE_DESCRIPTION``. - Beware that we now extract more docstrings. Check your schema diff on update whether you are now leaking unintended information. - The ``whitelist`` mechanics changed slightly on what is considered default behavior. - Fix a breaking change in ``dj-rest-auth>=3.0.0`` - It should not be possible to run the tests without installing system libraries like GDAL for the contrib tests 0.25.1 (2022-12-16) ------------------- - Fix warning source line performance regression `#889 `_ `#897 `_ - improve warning for transient @api_view objects `#889 `_ - adapt package arg due to setuptools deprecation `#786 `_ - utilize queryset for SlugRelatedField `#897 `_ Breaking changes / important additions: - Bugfix release that addresses a performance regression in ``SpectacularApiView`` and an oversight in the now stricter handling of ``SlugRelatedField`` 0.25.0 (2022-12-13) ------------------- - Fix missing description for ManyRelatedField and tested for SlugField (`#895 `_) [StopMotionCuber] - Simplify hashable_values `#833 `_ - Add custom settings to CLI (view parity) `#892 `_ - fix function misnomer `#891 `_ - improve trace messages / warnings & add color `#866 `_ - Treat SlugRelatedField analog to PrimaryKeyRelatedField `#854 `_ - Include filename in call to _get_sidecar_url [Justin Spencer] - add django-parler blueprint `#887 `_ - add a view to handle SwaggerUI oauth callbacks (`#882 `_) [Finn-Thorben Sell] - improve documentation - Introduce setting DEFAULT_QUERY_MANAGER to allow other managers for querset retrieval - fix flake8 6.0.0 breaking change - fix example list detection (symmetry with schema) `#872 `_ - Use direct view methods for getting serializer instances [Numerlor] - name overrides for rest_framework_dataclasses `#839 `_ - decouple TypedDict class from Py version `#861 `_ `#654 `_ - bugfix djangorestframework_camel_case `#861 `_ - bugfix djangorestframework_camel_case nested object handling `#861 `_ - Utils: Replace ``List[]`` with ``Sequence[]``, because of Mypy note 'List is invariant. Consider using Sequence instead.' [Hans Aarne Liblik] - Fixed minor typos [Conrad] - Removing blank and null keys when generating the overridden choices hash to match the hash generation logic in the enum post processor hook [Trent Holliday] - fix test fixture overlap `#826 `_ - specify min patch release for DRF (fixes `#812 `_) - Preserve context in ``get_list_serializer``. [Brady Dean] - Allow field extensions to return None from map_serializer_field [Andrew Backer] Breaking changes / important additions: - Officially set the lower bound for DRF version to ``3.10.3`` - Refactored the CLI warning system for better code navigation / orientation, GUI support and color! - Some minor mechanics changes, several overall improvements, feature additions, and a few bugfixes. 0.24.2 (2022-09-26) ------------------- - robustify extension class loading `#821 `_ - fix regression due to GIS import for django-filter `#821 `_ Breaking changes / important additions: - Hotfix release to mitigate optional GDAL import errors for django-filter. 0.24.1 (2022-09-23) ------------------- - bugfix GeometryFilter for GIS and django-filter `#814 `_ - NullBooleanField comment and add 3.14.0 to test suite `#818 `_ - fix: `#816 `_ NullBooleanField does not exist in DRF >= 3.14.0 [Laurent Tramoy] - fix GIS source lookup with hops `#813 `_ - Add blueprint for Stoplight Elements docs UI [Alex Burgel] - fix OpenApiParameter enum and pattern for many=True `#808 `_ Breaking changes / important additions: - Hotfix release to mitigate removal of ``NullBooleanField`` in DRF 3.14.0 - Small fixes to OpenApiParameter and ``django-filter`` 0.24.0 (2022-09-14) ------------------- - fix yaml serialization error on Django SafeString `#802 `_ - mitigate DRF bug in ObtainAuthToken < 3.12.0 `#796 `_ - add FAQ entry for django-csp errors `#173 `_ `#797 `_ - bugfix TokenMatchesOASRequirements `#469 `_ and JWTCookieAuthentication `#626 `_ - add custom redoc settings option - fix error with PrimaryKeyRelatedField on non-ModelSerializer `#353 `_ - provide context to serializer for @extend_schema use-cases `#699 `_ - add example value hint to doc `#788 `_ - fix packages= so top_level.txt is correct [anthony sottile] - Adding documentation for the OpenApiParameter 'many' argument [Paul Wayper] - Extend OpenApiSerializerExtension interface. `#392 `_ `#705 `_ - Include context with request when instantiating serializers [Mike Hansen] Breaking changes / important additions: - Some minor gaps closed in the extension interface and serializer context initialization. It is a y-stream release, because there remains a small chance of change for users that sport non-standard customizations. 0.23.1 (2022-07-26) ------------------- - improve CAMELIZE_NAMES doc `#774 `_ - move import into build_geo_schema function [bidaya0] Breaking changes / important additions: - Hotfix release to mitigate unwanted import of optional GIS features that depend on GDAL. GDAL is **not** a new requirement. 0.23.0 (2022-07-25) ------------------- - fix infinite recursion when accessing missing attributes in generator stats [Oleg Hoefling] - fix list pagination when examples are provided [topher235] - accept integer status codes in OpenApiExample [Nicholas Guriev] - Missing ":" in example documentation [Josué Millán Zamora] - Flip direction for callbacks serializers [Justas] - grammar fix [Kojo Idrissa] - fix sidecar for alternate staticfile storages `#718 `_ - add support for ``rest_framework_gis`` - add mechanism to handle custom ListSerializers with extensions - Update based on review [johnthagen] - Hyphenate in-memory [johnthagen] - Add FAQ entry for how to serve in-memory generated files [johnthagen] - add pattern to OpenApiParameter `#738 `_ - Add test that extend_schema_field on django-filter is not modified [Take Weiland] - Do not forcefully overwrite enum setting on custom django-filter schema [Take Weiland] - django-filter: Enable type extraction fallback for MultipleChoiceFilter as well [Take Weiland] - Add examples camelization note to settings.py [Zac Miller] - fix codecov badge url issue on github `#713 `_ Breaking changes / important additions: - A whole bunch of smaller bug fixes. - OpenAPI Callbacks should now be production ready - Introduction of ``rest_framework_gis`` support. This might impact APIs that are using GIS so this is a y-stream release. 0.22.1 (2022-04-25) ------------------- - Update customization.rst [Lane Zhang] - Remove invalid example in drf-yasg migration documentation. [Nick Pope] - Avoid using default role in documentation. [Nick Pope] - Small documentation fixes. [Nick Pope] - improve parameter many handling and warnings `#703 `_ - bugfix unconsidered warnings/errors for return code `#706 `_ `#702 `_ - Include a list of applications urls as a parameter for SERVE_URLCONF `#709 `_ [anoirak] - bugfix/improve analyze_named_regex_pattern(path) `#697 `_ [Jon Iturmendi] - django-filter: added type extraction fallback for ChoiceFields `#690 `_ - fix test, more precise naming, also wrap validation `#693 `_ - bugfix PolymorphicProxySerializer many handling and add manual mode `#692 `_ - Use Django management CommandError to eliminate the traceback on error [Brandon W Maister] - add ``swagger_fake_view`` FAQ entry `#321 `_ - Fix `#688 `_ - avoid a TypeError when ChoiceFilter choices are a callable [Glenn Matthews] - map explicit float hints/decoration to double `#687 `_ `#674 `_ Breaking changes / important additions: - Small release consisting of minor bug fixes, improved ``PolymorphicProxySerializer``, cleaned up documentation, and some improvements to **django-filter** 0.22.0 (2022-03-21) ------------------- - Added ``detype_patterns()`` with ``@cache``. [Nick Pope] - add "externalDocs" to operation via extend_schema `#681 `_ - warn on invalid components names `#685 `_ - wrap examples in list/pagination when serializer is many=True `#641 `_ `#640 `_ `#595 `_ - python's and django's float is really "double precision" `#674 `_ - Support negative numbers in pattern regex for coerced decimal fields [Mike Hansen] - add OpenAPI callback operations `#665 `_ - Keep the urlpatterns in the apiview and pass it to the generator [Jorge Cardona] - django-filter: raise priority of explicitly given filter method type hints `#660 `_ - also allow @extend_schema_field on django-filter filter method `#660 `_ - accommodate pyright limitations `#657 `_ - fix doc extraction for built-in types `#654 `_ - use get_doc for description [Josh Ferge] - add more information to resolved TypedDicts [Josh Ferge] - fix url escaping bug introduced in `#556 `_ (`#650 `_) - pass through version from UI to schema endpoint `#650 `_ - factor out schema_url generation `#650 `_ - relax AcceptHeaderVersioning constraint for modification `#650 `_ - Enable the use of lists in extend_schema_view() [François Travais] Breaking changes / important additions: - This is a y-stream release with a lot of bugfixes, some new features and potentially small schema changes (if affected features are used). - Examples are now wrapped in pagination/lists when endpoint/serializer is ``many=True`` - django-filter had some internal restructuring and thus overrides are now always honored. - added callback functionality (EXPERIMENTAL and subject to change due to pending issue) - Many thanks to all the contributors! 0.21.2 (2022-02-01) ------------------- - Add support for djangorestframework-dataclasses [Oxan van Leeuwen] - add version to schema for AcceptHeaderVersioning `#637 `_ - FAQ for @api_view `#635 `_ - add extensions for dj_rest_auth's JWTCookieAuthentication `#626 `_ Breaking changes / important additions: - Some minor bugfixes and feature additions. Schemas using AcceptHeaderVersioning contain a small change. 0.21.1 (2021-12-20) ------------------- - add root level extension setting `#619 `_ - ease schema browser handling with "Content-Disposition" `#607 `_ - custom settings per SpectacularAPIView instance `#365 `_ - Support new X | Y union syntax in Python 3.10 (PEP 604) [Marti Raudsepp] - upstream release updates, compat test fix for jwt, consistency fix - add blueprint for django-auth-adfs [1110sillabo] - use is_list_serializer instead of isinstance() [Roman Sichnyi] - Fix schema generation for RecursiveField(many=True) [Roman Sichnyi] - enable clearing auth methods with empty list `#99 `_ - Fix typos in the code example [Marcin Kurczewski] Breaking changes / important additions: - Some minor bugfixes and small feature additions. No large schema changes are expected 0.21.0 (2021-11-10) ------------------- - add renderer & parser whitelist setting `#598 `_ - catch attr exception for invalid SerializerMethodField `#592 `_ - add regression test for catch-all status codes `#573 `_ - bugfix OpenApiResponse without description argument `#591 `_ - introduce direction literal / import consolidation `#582 `_ - mitigate CORS issues for external requests in Swagger UI `#588 `_ - Swagger UI authorized schema retrieval `#342 `_ `#458 `_ - remove cyclic import warning as fixes haves mitigated the issue. `#581 `_ - bugfix: anchor parameter patterns with ^$ - bugfix isolation of derivatives for @extend_schema_serializer/@extend_schema_field `#585 `_ - add support for djangorestframework-recursive `#586 `_ - Add blueprint for drf-extra-fields Base64FileField [johnthagen] - Add note about extensions registering themselves [johnthagen] - Document alternative to drf-yasg swagger_schema_field [johnthagen] - allow to bypass list detection for filter discovery `#407 `_ - add blueprint (closes `#448 `_), fix test misnomer - non-blank string enforcement for parameters `#282 `_ - add setting ENFORCE_NON_BLANK_FIELDS to enable blank checks `#186 `_ Breaking changes / important additions: - Fixed two more decorator isolation issues. - Added Swagger UI plugin to handle reloading the schema on authentication changes (``'SERVE_PUBLIC': False``). - Added ``minLength`` where a blank value is not allowed. Apart the dedicated setting, it is implicitly enabled by ``COMPONENT_SPLIT_REQUEST``. - Several other small fixes and additional settings for corner cases. This is mainly a y-steam release due to the potential impact on the Swagger UI and ``minLength`` changes. 0.20.2 (2021-10-15) ------------------- - add setting for manual path prefix: SCHEMA_PATH_PREFIX_INSERT `#567 `_ - improve type hint for @extend_schema_field `#569 `_ - bugfix COMPONENT_SPLIT_REQUEST for empty req/resp serializers `#572 `_ - Make it cleared that ENUM_NAME_OVERRIDES is a key within SPECTACULAR_SETTINGS [johnthagen] - Improve formatting in customization docs [johnthagen] - bugfix @extend_schema_view on @api_view `#554 `_ - bugfix isolation for @extend_schema/@extend_schema_view reorg `#554 `_ - Fix inheritance bugs with @extend_schema_view(). [Nick Pope] - Allow methods in @extend_schema to be case insensitive. [Nick Pope] - Added a documentation blueprint for RapiDoc. [Nick Pope] - Tidy templates for documentation views. [Nick Pope] - Use latest version for CDN packages. [Nick Pope] Breaking changes / important additions: - Mainly a bugfix release that solves several longstanding issues with ``@extend_schema_view``/``@extend_schema`` annotation isolation. There should be no more side effects from arbitrarily mixing and matching the decorators. - Improved handling of completely empty serializers with COMPONENT_SPLIT_REQUEST. 0.20.1 (2021-10-03) ------------------- - move swagger CDN to jsdelivr (unpkg has been flaky) - bugfix wrong DIST setting in Redoc `#546 `_ - Allow paginated_name customization [Georgy Komarov] Breaking changes / important additions: - Hotfix release due to regression in the Redoc template 0.20.0 (2021-10-01) ------------------- - Add support for specification extensions. [Nick Pope] - add example injection for (discovered) parameters `#414 `_ - Fix crash with read-only polymorphic sub-serializer. [Nick Pope] - Add arbitrarily deep ListSerializer nesting `#539 `_ - tighten serializer assumptions `#539 `_ - fix whitespace stripping on methods - Rename ``AutoSchema._map_field_validators()`` → ``.insert_field_validators()``. [Nick Pope] - Rename ``AutoSchema._map_min_max()`` → ``.insert_min_max()``. [Nick Pope] - Fix detection of int64 from min/max values. [Nick Pope] - Fix zero handling in _map_min_max(). [Nick Pope] - Add support for introspection of nested validators. [Nick Pope] - Fix invalid schemas caused by validator introspection. [Nick Pope] - Overhaul validator logic. [Nick Pope] - support multiple headers in OpenApiAuthenticationExtension `#537 `_ - docs: Missing end quote for INSTALLED_APPS [Prayash Mohapatra] - update doc `#530 `_ - introducing the spectacular sidecar - fallback improvements to typing system with typing_extensions Breaking changes / important additions: - Added vendor specification extensions - Completely overhauled validator logic and bugfixes - Offline UI assets with optional *drf-spectacular-sidecar* package - several internal logic improvements and stricter assumptions 0.19.0 (2021-09-21) ------------------- - fix/cleanup suffixed path variable coercion `#516 `_ - remove superseded Request mock from oauth_toolkit - be gracious on Enums that are not recognized by DRF `#500 `_ - remove non-required empty descriptions - added test case for lookup_field `#524 `_ - Fix grammatical typo [johnthagen] - remove mapping for re.Pattern (no 3.6 and mypy issues) `#526 `_ - Add missing types defined in specification. [Nick Pope] - Add type mappings for IP4, IP6, TIME & DURATION. [Nick Pope] - add support for custom converters and converter override `#502 `_ - cache static loading function calls - prevent settings loading in types, lazy load in plumbing instead - lazy settings loading in drainage - Improve guide for migration from drf-yasg. [Nick Pope] - handle default value for SerializerMethodField `#422 `_ - consolidate bearer scheme generation & bugfix `#515 `_ - prevent uncaught exception on modified django-filter `#519 `_ - add decoupled model docstrings `#522 `_ - Fix warnings raised during testing. [Nick Pope] - add name override to @extend_schema_serializer `#517 `_ - Fix deprecation warning about default_app_config from Django 3.2+ [Janne Rönkkö] - Remove obsolete value from IMPORT_STRINGS. [Nick Pope] - Add extension for TokenVerifySerializer. [Nick Pope] - Use SESSION_COOKIE_NAME in SessionScheme. [Nick Pope] - add regex path parameter extraction for explicit cases `#510 `_ - honor lookup_url_kwarg name customization `#509 `_ - add contrib compat tests for drf-nested-routers - improve path coersion model resolution - add test_fields API response test `#501 `_ - Handle 'lookup_field' containing relationships for path parameters [Luke Plant] - add BinaryField case to tests `#506 `_ - fix: BinaryField's schema type should be string `#505 `_ (`#506 `_) [jtamm-red] - bugfix incomplete regex stripping for literal dots `#507 `_ - Fix tests [Jameel Al-Aziz] - Fix type hint support for functools cached_property wrapped funcs [Jameel Al-Aziz] - Extend enum type hint support to more Enum subclasses [Jameel Al-Aziz] Breaking changes / important additions: - Severely improved path parameter detection for Django-style parameters, RE parameters, and custom converters - Significantly more defensive settings loading for safer project imports (less prone to import loops) - Improved type hint support for ``Enum`` and other native types - Explicit support for *drf-nested-routers* - A lot more small improvements 0.18.2 (2021-09-04) ------------------- - fix default value handling for custom ModelField `#422 `_ - fill html title with title from settings `#491 `_ - add Enum support in type hints `#492 `_ - Move system check registration to AppConfig [Jameel Al-Aziz] Breaking changes / important additions: - Primarily ironing out another issue with the Django check and some minor improvements 0.18.1 (2021-08-31) ------------------- - Improved docs regarding how ENUM_NAME_OVERRIDES works [Luke Plant] - bugfix raw schema handling for @extend_schema_field on SerializerMethodField method 481 - load common SwaggerUI dep SwaggerUIStandalonePreset `#483 `_ - allow versioning of SpectacularAPIView via query `#483 `_ - update swagger UI - move checks to "--deploy" section, bugfix public=True `#487 `_ Breaking changes / important additions: - This is a hotfix release as the newly introduced Django check was executing the wrong code path. - Check also moved into the ``--deploy`` section to prevent double execution. This can be disabled with ``ENABLE_DJANGO_DEPLOY_CHECK`` - Facitities added to utilize SwaggerUI Topbar for versioning. 0.18.0 (2021-08-25) ------------------- - prevent exception and warn when ReadOnlyField is used with non-ModelSerializer `#432 `_ - allow raw JS in Swagger settings `#457 `_ - add support for check framework `#477 `_ - improve common FAQ @action question `#399 `_ - update @extend_schema doc `#476 `_ - adapt to changes in iMerica/dj-rest-auth 2.1.10 (ResendEmailVerification) - add raw schema to @extend_schema(request={MIME: RAW}) `#476 `_ - bugfix test case for 3.6 `#474 `_ - bugfix header underscore handling for simplejwt `#474 `_ - properly parse TokenMatchesOASRequirements (oauth toolkit) `#469 `_ - add whitelist setting to manage auth method exposure `#326 `_ `#471 `_ - Update set_password instead of list [Greg Campion] - Update documentation to illustrate how to override a specific method [Greg Campion] Breaking changes / important additions: - This is a y-stream release because we added `Django checks `_ which might emit warnings and subsequently break CI. This can be easily suppressed with Django's ``SILENCED_SYSTEM_CHECKS``. - Several small fixes and features that should not have a big impact. 0.17.3 (2021-07-26) ------------------- - port custom "Bearer" bugfix/workaround to simplejwt `#467 `_ - add setting for listing/paginating/filtering on non-2XX `#402 `_ `#277 `_ - fix Typo [Eunsub LEE] - nit typofix [adamsteele-city] - Add a few return type annotations [Nikhil Benesch] - add django-filter queryset annotation and ``extend_schema_field`` support - account for functools.partial wrapped type hints `#451 `_ - Update swagger_ui.js [Jordan Facibene] - Update customization.rst to fix example typo [Atsuo Shiraki] - update swagger-ui version - add oauth2 config for swagger ui `#438 `_ Breaking changes / important additions: - Just a few bugfixes and some small features with minimal impact on existing schema 0.17.2 (2021-06-15) ------------------- - prevent endless loop in extensions when augmenting schema `#426 `_ - bugfix secondary import cycle (generics.APIView) `#430 `_ - fix: avoid circular import of/via rest_framework's APIView [Daniel Hahler] Breaking changes / important additions: - Hotfix release that addresses a carelessly added import in 0.17.1. In certain use-cases, this may have led to an import cycle inside DRF. 0.17.1 (2021-06-12) ------------------- - bugfix 201 response for (List)CreateAPIVIew `#428 `_ - support paginated ListSerializer with field child `#413 `_ - fix django-filter.BooleanFilter subclass issue `#317 `_ - serializer field deprecation `#415 `_ - improve extension documentation `#426 `_ - improve type hints and fix mypy issues on tests. - add missing usage case to type hints `#418 `_ - Typo(?) README fix [Jan Jurec] Breaking changes / important additions: - This release is mainly for fixing incomplete type hints which mypy will potentially complain about. - A few small fixes that should either have no or a very small impact in schemas. 0.17.0 (2021-06-01) ------------------- - improve type hint detection for Iterable and NamedTuple `#404 `_ - bugfix ReadOnlyField when used as ListSerlializer child `#404 `_ - improve component discard logic `#395 `_ - allow disabling operation sorting for sorting in PREPROCESSIN_HOOKS `#410 `_ - add regression test for `#407 `_ - fix error on read-only serializer [Matthieu Treussart] - invert component exclusion logic (OpenApiSerializerExtension) `#351 `_ `#391 `_ - add many=True support to PolymorphicProxySerializer `#382 `_ - improve documentation, remove py2 wheel tag, mark as mypy-enabled - bugfix YAML serialization errors that are ok with JSON `#388 `_ - bugfix missing auth extension for JWTTokenUserAuthentication `#387 `_ - Rename MethodSerializerField -> SerializerMethodField in README [Christoph Krybus] Breaking changes / important additions: - Quite a few small improvements. The biggest change is the inversion of the component discard logic. This should have no negative impact, but to be on the safe side we'll opt for a y-stream release. - The package is now marked as being typed, which should get picked up natively by mypy 0.16.0 (2021-05-10) ------------------- - add redoc dist setting - bugfix mock request asymmetry `#370 `_ `#250 `_ - refactor urlpattern simplification `#373 `_ `#168 `_ - include relation PKs into SCHEMA_COERCE_PATH_PK handling `#251 `_ - allow PolymorphicProxySerializer to be simple 'oneOf' - bugfix incorrect PolymorphicProxySerializer warning on extend_schema_field `#263 `_ - add break-out option for SerializerFieldExtension - Modify urls for nested routers [Matthias Erll] Breaking changes / important additions: - Revamped handling of mocked requests. Now ``GET_MOCK_REQUEST`` is always called, not just for offline schema generation. In case there is a real request available, we carry over headers and authentication. If you use your own implementation, you may want to inspect the new default implementation. - NamespaceVersioning: switched path variable substitution from regex to custom state machine due to parethesis counting issue. - Improved implicit support for `drf-nested-routers `_ - Added some convenience options for plain ``oneOf`` to PolymorphicProxySerializer - This release should have minimal impact on the generated schema. We opt for a y-stream release due to potentially breaking changes when a user-provided ``GET_MOCK_REQUEST`` is used. 0.15.1 (2021-04-08) ------------------- - bugfix prefix estimation with RE special char literals in path `#358 `_ Breaking changes / important additions: - minor release to fix newly introduced default prefix estimation. 0.15.0 (2021-04-03) ------------------- - fix boundaries for decimals coerced to strings `#335 `_ - improve util type hints - add convenience response wrapper OpenApiResponse `#345 `_ `#272 `_ `#116 `_ - adapt for dj-rest-auth upstream changes in iMerica/dj-rest-auth#227 - Fixed traversing of 'Optional' type annotations [Luke Plant] - prevent pagination on error responses. `#277 `_ - fix SCHEMA_PATH_PREFIX_TRIM ^/ pitfall & remove unused old URL mounting - slightly improve `#332 `_ for django-filter range filters - introduce non-redundant title field. `#191 `_ `#286 `_ - improve schema version string handling including variations `#303 `_ - bugfix ENUM_NAME_OVERRIDES for categorized choices `#339 `_ - improve SCHEMA_PATH_PREFIX handling, add auto-detect default, introduce prefix trimming `#336 `_ - add support for all django-filters RangeFilter [Jules Waldhart] - Added default value for missing attribute [Matthias Erll] - Fix map_renderers where format is None [Matthias Erll] Breaking changes / important additions: - explicitly set responses via ``@extend_schema`` will not get paginated/listed anymore for non ``2XX`` status codes. - New default ``None`` for ``SCHEMA_PATH_PREFIX`` will attempt to determine a reasonable prefix. Previous behavior is restored with ``''`` - Added ``OpenApiResponses`` to gain access to response object descriptions. 0.14.0 (2021-03-09) ------------------- - Fixed bug with ``cached_property`` non-Model objects not being traversed [Luke Plant] - Fixed issue `#314 `_ - include information about view/serializer in warnings. [Luke Plant] - bugfix forward/reverse model traversal `#323 `_ - fix nested serializer detection & smarter metadata extraction `#319 `_ - add drf-yasg compatibility feature 'swagger_fake_view' `#321 `_ - fix django-filter through model edge case & catch exceptions `#320 `_ - refactor/bugfix PATCH & Serializer(partial=True) behaviour. - bugfix django-filter custom filter class resolution `#317 `_ - bugfix django-filter for Django 2.2 AutoField - improved/restructured resolution priority in django-filter extension `#317 `_ `#234 `_ - handle Decimals for YAML `#316 `_ - remove deprecated django-filter backend solution - update swagger-ui version - bugfix [] case and lint `#312 `_ - discriminate None and typing.Any usage `#315 `_ - fix multi-step source relation field resolution, again. `#274 `_ `#296 `_ - Add any type for OpenApiTypes [André da Silva] - improve Extension usage documentation `#307 `_ - restructure request body for extend_schema `#266 `_ `#279 `_ - bugfix multipart boundary showing up in Accept header - bugfix: use get_parsers() and get_renderers() `#266 `_ - Fix for better support of PEP 563 compatible annotations. [Luke Plant] - Add document authentication [gongul] - Do not override query params [Fabricio Aguiar] - New setting for enabling/disabling error/warn messages [Fabricio Aguiar] - bugfix response headers without body `#297 `_ - issue `#296 `_ [Luis Saavedra] - Fixes `#283 `_ -- implement response header parameters [Sergei Maertens] - Added feature test for response headers [Sergei Maertens] - robustify django-filter enum sorting `#295 `_ Breaking changes / important additions: - *drf-spectacular*'s custom ``DjangoFilterBackend`` removed after previous deprecation. Just use the original class again. - *django-filter* extension received a significant refactoring so your schema may have several changes, hopefully positive ones. - Added response headers feature - Extended ``@extend_schema(request=X)``, where ``X`` may now also be a ``Dict[content_type, serializer_etc]`` - Updated Swagger UI version - Fixed several model traversal issues that may lead to PK changes in the schema - Added *drf-yasg*'s ``swagger_fake_view`` 0.13.2 (2021-02-11) ------------------- - add setting for operation parameter sorting `#281 `_ - bugfix/generalize Union hint extraction `#284 `_ - bugfix functools.partial methods in django-filters `#290 `_ - bugfix django-filter method filter `#290 `_ - Check serialzer help_text field is passed to the query description [Jorge Rodríguez-Flores Esparza] - QUERY Parameters from serializer ignore description in SwaggerUI [Jorge Rodríguez-Flores Esparza] - README.rst encoding change [gongul] - Add support for SCOPES_BACKEND_CLASS setting from django-oauth-toolkit [diesieben07] - use source instead of field_name for model field detection `#274 `_ [diesieben07] - bugfix parameter removal from custom AutoSchema `#212 `_ - add specification extension option to info section `#165 `_ - add default to OpenApiParameter `#271 `_ - show violating view for easier fixing `#278 `_ - fix readonly related fields generating incorrect schema `#274 `_ [diesieben07] - bugfix save parameter removal `#212 `_ 0.13.1 (2021-01-21) ------------------- - bugfix/handle more django-filter cases `#263 `_ - bugfix missing meta on extend_serializer_field, raw schema, and breakout - expose explode and style for OpenApiParameter `#267 `_ - Only generate mock request if there is no actual request [Matthias Erll] - Update blueprints.rst [takizuka] - bugfix enum substitution for enumed arrays (multiple choice) - Update README.rst [Chad Ramos] - Create new mock request on each operation [Matthias Erll] 0.13.0 (2021-01-13) ------------------- - add setting for additionalProperties handling `#238 `_ - bugfix path param extraction for PrimaryKeyRelatedField `#258 `_ - use injected django-filter help_text `#234 `_ - robustify normalization of types `#257 `_ - bugfix PATCH split serializer disparity `#249 `_ - django-filter description bugfix `#234 `_ - bugfix unsupported http verbs `#244 `_ - bugfix assert on methods in django-filter `#252 `_ `#234 `_ `#241 `_ - Regression: Filterset defined as method (and from a @property) are not supported [Nicolas Delaby] - bugfix view-level AutoSchema noneffective with extend_schema `#241 `_ - bugfix incorrect warning on paginated actions `#233 `_ Breaking changes: - several small improvements that should not have a big impact. this is a y-stream release mainly due to schema changes that may occur with *django-filter*. 0.12.0 (2020-12-19) ------------------- - add exclusion for discovered parameters `#212 `_ - bugfix incorrect collision warning `#233 `_ - introduce filter extensions `#234 `_ - revert Swagger UI view to single request and alternative `#211 `_ `#173 `_ - bugfix Simple JWT token refresh `#232 `_ - bugfix simple JWT serializer schema `#232 `_ - Fix enum postprocessor to allow 0 as possible value [Vikas] - bugfix/restore optional default parameter value `#226 `_ - Include QuerySerializer in documentation [KimSoungRyoul] - support OAS3.0 ExampleObject to @extend_schema & @extend_schema_serializer `#115 `_ [KimSoungRyoul] - add explicit double and int32 types. `#214 `_ - added type extension for int64 format support [Peter Dreuw] - fix TokenAuthentication handling of keyword `#205 `_ - Allow callable limit_value in schema [Serkan Hosca] - @extend_schema responses param now accepts tuples with media type `#201 `_ - bugfix List hint extraction with non-basic sub types `#207 `_ Breaking changes: - reverted back to *0.10.0* Swagger UI behavior as default. Users relying on stricter CSP should use ``SpectacularSwaggerSplitView`` - ``tokenAuth`` slightly changed to properly model correct ``Authorization`` header - a lot of minor improvements that may slightly alter the schema 0.11.1 (2020-11-15) ------------------- - bugfix hint extraction on @cached_property `#198 `_ - add support for basic TypedDict hints `#184 `_ - improve type hint resolution `#199 `_ - add option to disable Null/Blank enum choice feature `#185 `_ - bugfix return code for Viewset create methods `#196 `_ - honor SCHEMA_COERCE_PATH_PK on path param type resolution `#194 `_ - bugfix absolute schema URL to relative in UI `#193 `_ Breaking changes: - return code for ``create`` on ``ViewSet`` changed from ``200`` to ``201``. Some generator targets are picky, others don't care. 0.11.0 (2020-11-06) ------------------- - Remove unnecessary view permission from action [Vikas] - Fix security definition for IsAuthenticatedOrReadOnly permission [Vikas] - introduce convenience decorator @schema_extend_view `#182 `_ - bugfix override behaviour of extend_schema with methods and views - move some plumbing to drainage to make importable without cirular import issues - bugfix naming for ListSerializer with pagination `#183 `_ - cleanup trailing whitespace in docstrings - normalize regex in pattern, remove ECMA-incompatible URL pattern `#175 `_ - remove Swagger UI inline script for stricter CSP `#173 `_ - fixed typo [Sebastian Pabst] - add the PASSWORD format to types.py [Sebastian Pabst] - docs(settings): fix favicon example [Max Wittig] Breaking changes: - ``@extend_schema`` override mechanics are now consistent. may affect schema only if used on both view and view method - otherwise mainly small improvement/fixes that should have minimal impact on the schema. 0.10.0 (2020-10-20) ------------------- - bugfix non-effective multi-usage of view extension. - improve resolvable enum collisions with split components - Update README.rst [Jose Luis da Cruz Junior] - fix regular expression in detype_pattern [Ruslan Ibragimov] - improve enum naming with resolvable collisions - improve handling of discouraged SECURITY setting (fixes `#48 `_ fixes `#136 `_) - instance check with ViewSetMixin instead of GenericViewSet [SoungRyoul Kim] - support swagger-ui-settings [SoungRyoul Kim] - Change Settings variable, allow override of default swagger settings and remove unnecessary line [Nix] - Fix whitespace issues in code [Nix] - Allow Swagger-UI configuration through settings Closes `#162 `_ [Nix] - extend django_filters test case `#155 `_ - add enum postprocessing handling of blank and null `#135 `_ - rest-auth improvements - test_rest_auth: Add test schema transforms [John Vandenberg] - tests: Allow transformers on expected schemas [John Vandenberg] - Improve schema difference test harness [John Vandenberg] - Add rest-auth tests [John Vandenberg] - contrib: Add rest-auth support [John Vandenberg] Breaking changes: - enum naming collision resolution changed in cleanly resolvable situations. - enums gained ``null`` and ``blank`` cases, which are modeled through ``oneOf`` for deduplication - SECURITY setting is now additive instead of being the mostly overridden default 0.9.14 (2020-10-04) ------------------- - improve client generation for paginated listings - update pinned swagger-ui version `#160 `_ - Hot fix for AcceptVersioningHeader support [Nicolas Delaby] - bugfix module string includes with urlpatterns `#157 `_ - add expressive error in case of misconfiguration `#156 `_ - fix django-filter related resolution. improve test `#150 `_ `#151 `_ - improve follow_field_source for reverse resolution and model leafs `#150 `_ - add ref if list field child is serializer [Matt Shirley] - add customization option for mock request generation `#135 `_ Breaking changes: - paginated list response is now wrapped in its own component 0.9.13 (2020-09-13) ------------------- - bugfix filter parameter application on non-list views `#147 `_ - improved support for django-filter - add mocked request for view processing. `#81 `_ `#141 `_ - Use sha256 to hash lists [David Davis] - change empty operation name on API prefix-cut to "root" - bugfix lost "missing hint" warning and incorrect empty fallback - add operationId collision resolution `#137 `_ - bugfix leaking path var names in operationId `#137 `_ - add config for camelizing names `#138 `_ - bugfix parameterized patterns for namespace versioning `#145 `_ - Add support for Accept header versioning [Krzysztof Socha] - support for DictField child type (`#142 `_) and models.JSONField (Django>=3.1) - add convenience inline_serializer for extend_schema `#139 `_ - remove multipleOf due to schema violation `#131 `_ Breaking changes: - ``operationId`` changed for endpoints using the DRF's ``FORMAT`` path feature. - ``operationId`` changed where there were path variables leaking into the name. 0.9.12 (2020-07-22) ------------------- - Temporarily pin the swagger-ui unpkg URL to 3.30.0 [Mohamed Abdulaziz] - Add ``deepLinking`` parameter [p.alekseev] - added preprocessing hooks for operation list modification/filtering `#93 `_ - Document effective DRF settings [John Vandenberg] - add format query parameter `#110 `_ - improve assert messages `#126 `_ - more graceful handling of magic fields `#126 `_ - allow for field child on ListSerializer. `#120 `_ - Fix sorting of endpoints with params [John Vandenberg] - Emit enum of possible format suffixes [John Vandenberg] - i18n `#109 `_ - bugfix INSTALLED_APP retrieval `#114 `_ - emit import warning for extensions with installed apps `#114 `_ Breaking changes: - ``drf_spectacular.hooks.postprocess_schema_enums`` moved from ``blumbing`` to ``hooks`` for consistency. Only relevant if ``POSTPROCESSING_HOOKS`` is explicitly set by user. - preprocessing hooks are currently experimental and may change on the next release. 0.9.11 (2020-07-08) ------------------- - extend instead of replace extra parameters `#111 `_ - add client generator helper settings for readOnly - bugfix format param: path params must be required=True - bugfix DRF docstring excludes and configuration `#107 `_ - bugfix operations with urlpattern override `#92 `_ - decrease built-in extension priority and improve doc `#106 `_ - add option to hide serializer fields `#100 `_ - allow None on @extend_schema request/response - bugfix json spec violation on "required :[]" for COMPONENT_SPLIT_REQUEST Breaking changes: - ``@extend_schema(parameters=...)`` is extending instead of replacing for custom ``AutoSchema`` - path parameter are now always ``required=True`` as required by specification 0.9.10 (2020-06-23) ------------------- - bugfix cyclic import in plumbing. `#104 `_ - add upstream test target with contrib allowed to fail - preparations for django 3.1 and DRF 3.12 - improve tox targets for unreleased upstream 0.9.9 (2020-06-20) ------------------ - added explicit URL option to UI views. `#103 `_ - improve auth extension doc `#99 `_ - bugfix attr typo with Token auth extension `#99 `_ - improve docstring extraction `#96 `_ - Manual polymorphic [Jair Henrique] - Add summary field to extend_schema `#97 `_ [lilisha100] - reduce minimal package requirements - extend sdist with tests & doc - bugfix nested RO/WO serializer on COMPONENT_SPLIT_REQUEST - add pytest option --skip-missing-contrib `#87 `_ - Save test files in temporary folder [Jair Henrique] - Setup isort library [Jair Henrique] 0.9.8 (2020-06-07) ------------------ - bugfix read-only many2many relation processing `#79 `_ - Implement OrderedDict representer for yaml dumper [Jair Henrique] - bugfix UI permissions `#84 `_ - fix abc import `#82 `_ - add duration field `#78 `_ 0.9.7 (2020-06-05) ------------------ - put contrib code in packages named files - improve djangorestframework-camel-case support `#73 `_ - Add support to djangorestframework-camel-case [Jair Henrique] - ENUM_NAME_OVERRIDES accepts import string for easier handling `#70 `_ - honor versioning on schema UIs `#71 `_ - improve enum naming mechanism. `#63 `_ `#70 `_ - provide global enum naming. `#70 `_ - refactor choice field - remove unused sorter setting - improve FileField, add test and documentation. `#69 `_ - Fix file fields [John Vandenberg] - allow for functions on models beside properties. `#68 `_ - replace removed DRF compat function Breaking changes: - Enum naming conflicts are now resolved explicitly. `how to resolve conflicts `_ - Choice fields may be rendered slightly different - Swagger UI and Redoc views now honor versioned requests - Contrib package code moved. each package has its own file now 0.9.6 (2020-05-23) ------------------ - overhaul documentation `#52 `_ - improve serializer field mapping (nullbool & time) - remove duplicate and misplaced description. `#61 `_ - extract serializer docstring - Recognise ListModelMixin as a list [John Vandenberg] - bugfix component sorting to include enums. `#60 `_ - bugfix fail on missing readOnly flag - Fix incorrect parameter cutting [p.alekseev] 0.9.5 (2020-05-20) ------------------ - add optional serializer component split - improve SerializerField meta extraction - improve serializer directionality - add mypy static analysis - make all readonly fields required for output. `#54 `_ - make yaml multi-line strings nicer - alphanumeric component sorting. - generalize postprocessing hooks - extension override through priority attr Breaking changes: - Schemas are functionally identical, but component sorting changed slightly. - All ``read_only`` fields are required by default - ``SerializerFieldExtension`` gained direction parameter 0.9.4 (2020-05-13) ------------------ - robustify serializer resolution & enum postprocessing - expose api_version to command. robustify version matching. `#22 `_ - add versioning support `#22 `_ - robustify urlconf wrapping. resolver does not like lists - explicit override for non-list serializers on ViewSet list `#49 `_ - improve model field mapping via DRF init logic - bugfix enum substitution with additional field parameters. - Fix getting default parameter for ``MultipleChoiceField`` [p.alekseev] - bugfix model path traversal via intermediate property - try to be more graceful with unknown custom model fields. `#33 `_ Breaking changes: - If URL or namespace versioning is set in views, it is automatically used for generation. Schemas might shrink because of that. Explicit usage of ``--api-version="XXX"`` should yield the old result. - Some warnings might change, as the field/view introspection tries to go deeper. 0.9.3 (2020-05-07) ------------------ - Add (partial) support for drf-yasg's serializer ref_name `#27 `_ - Add thin wrappers for redoc and swagger-ui. `#19 `_ - Simplify serializer naming override `#27 `_ - Handle drf type error for yaml. `#41 `_ - Tox.ini: Add {posargs} [John Vandenberg] - add djangorestframework-jwt auth handler [John Vandenberg] - Docs: example of a manual configuration to use a apiKey in securitySchemes [Jelmer Draaijer] - Introduce view override extension - Consolidate extensions - Parse path parameter type hints from url. closes `#34 `_ - Consolidate duplicate warnings/add error `#28 `_ - Prevent warning for DRF format suffix param - Improve ACCEPT header handling `#42 `_ Breaking changes: - all extension base classes moved to ``drf_spectacular.extensions`` 0.9.2 (2020-04-27) ------------------ - Fix incorrect PK access through id. `#25 `_. - Enable attr settings on SpectacularAPIView `#35 `_. - Bugfix @api_view annotation and tests. - Fix exception/add support for explicit ListSerializer `#29 `_. - Introduce custom serializer field extension mechanic. enables tackling `#31 `_ - Improve serializer estimation with educated guesses. `#28 `_. - Bugfix import error and incorrect warning `#26 `_. - Improve scope parsing for oauth2. `#26 `_. - Postprocessing enums to components - Handle decimal coersion. closes `#24 `_. - Improvement: patched serializer variation only on request. - Add serializer directionality. - End the bucket brigade / cleaner interface. - Add poly serializer warning. - Bugfix: add serialization for default values. - Bugfix reverse access collision from schema to view. Breaking changes: - internal interface changed (method & path removed) - fewer PatchedSerializers emitted - Enums are no longer inlined 0.9.1 (2020-04-09) ------------------ - Bugfix missing openapi schema spec json in package - Add multi-method action decoration support. - rest-polymorphic str loading prep. - Improve list view detection. - Bugfix: response codes must be string. closes `#17 `_. 0.9.0 (2020-03-29) ------------------ - Add missing related serializer fields `#15 `_. - Bugfix properties with $ref component. closes `#16 `_. - Bugfix polymorphic resource_type lookup. closes `#14 `_. - Generalize plugin system. - Support ``required`` parameter for body. [p.alekseev] - Improve serializer retrieval. - Add query serializer support `#10 `_. - Custom serializer parsing with plugins. - Refactor auth plugin system. support for DjangoOAuthToolkit & SimpleJWT. - Bugfix extra components. Breaking changes: - removed ``to_schema()`` from ``OpenApiParameter``. Handled in ``AutoSchema`` now. 0.8.8 (2020-03-21) ------------------ - Documentation. - Schema serving with ``SpectacularAPIView`` (configurable) - Add generator stats and ``--fail-on-warn`` command option. - Schema validation with ``--validation`` against OpenAPI JSON specification - Added various settings. - Bugfix/add support for basic type responses (parity with requests) - Bugfix required in parameters. failed schema validation. - Add validation against OpenAPI schema specification. - Improve parameter resolution, warnings and tests. - Allow default parameter override. (e.g. ``id``) - Fix queryset function call. [p.g.alekseev] - Supporting enum values in params. [p.g.alekseev] - Allow ``@extend_schema`` request basic type annotation. - Add support for typing Optional[*] - Bugfix: handle proxy models where pk is a OnetoOne relation. - Warn on duplicate serializer names. - Added explicit exclude flag for operation. - Bugfix: PrimaryKeyRelatedField(read_only=True) failing to find type. - Change operation sorting to alphanumeric with option (`#6 `_) - Robustify serializer field support for ``@extend_schema_field``. - Enable field serializers support. [p.g.alekseev] - Adding custom tags support [p.g.alekseev] - Document extend_schema. - Allow operation hiding. - Catch unknown model traversals. custom fields can be tricky. - Improve model field mapping. extend field tests. - Add deprecated method to extend_schema decorator. [p.g.alekseev] Breaking changes: - ``@extend_schema`` renamed ``extra_parameters`` -> ``parameters`` - ``ExtraParameter`` renamed to ``OpenApiParameter`` 0.8.5 (2020-03-08) ------------------ - Generalize ``PolymorphicResponse`` into ``PolymorphicProxySerializer``. - Type dict is resolved as object. - Simplify hint resolution. - Allow ``@extend_schema_field`` for custom serializer fields. 0.8.4 (2020-03-06) ------------------ - ``@extend_schema_field`` accepts Serializers and OpenApiTypes - Generalize query parameter. - Bugfix serializer init. - Fix unused get_request_serializer. - Refactor and robustify typing system. - Helper scripts for swagger and generator. - Fix license. 0.8.3 (2020-03-02) ------------------ - Fix parameter type resolution. - Remove empty parameters. - Improved assert message. 0.8.2 (2020-03-02) ------------------ - Working release. - Bugfix wrong call & remove yaml aliases. 0.8.1 (2020-03-01) ------------------ - Initial published version. drf-spectacular-0.27.0/CONTRIBUTING.rst000066400000000000000000000057451453572150400173410ustar00rootroot00000000000000Contributing to drf-spectacular =============================== As an open source project, drf-spectacular welcomes any form of contribution. The project was initially forked off DRF's schema generator and has since then been continually receiving improvements from the community. Your contribution matters even if it is only a small one. Contributions come in different shapes and sizes. * Documentation improvements, clarifications & fixing typos * Creating issues for feature requests & bug reports * Creating pull requests for features and bug fixes * Questions that highlight inconsistencies or workflow issues * Adding `blueprints`_ for not officially supported 3rd party tools. Issues ------ Generating schemas is a complicated business and the devil often lies in the details. A concise description with examples goes a long way towards getting a good understanding of the issue at hand. If possible/applicable please include * A concise description * Example code that produces the issue * Generated (partial) schema with the issue * Stacktraces if an error occurred * drf-spectacular/Django/DRF versions Pull requests ------------- drf-spectacular prides itself on having very high `code coverage`_ and an extensive `test suite`_. It is really well tested, which enables us to maintain quality, reliability, and consistency. * The git history is important to the project. Please make minimally invasive changes where possible. On receiving feedback, we prefer having a small set of amended commits. Consider using ``git commit --amend`` and ``git push --force`` for updating your PR. * If you have a non-trivial PR please consider getting `early feedback`_. We don't want to waste anyone's time. * We have great tooling around tests. Have a look into `test_regressions.py`_ for inspiration. The tests are mainly structured in feature units but in doubt small things go into the regressions. * We use tox to make sure drf-spectacular works for a range of Django/DRF versions. Your PR must pass the whole test suite to get merged. Local testing with ``./runtests.py`` usually suffices. You don't need to install a bunch of python versions. The github actions for PRs will take care of the rest. A quick cheat sheet to get you rolling .. code:: console $ # fork the repo on github $ git clone https://github.com/YOURGITHUBNAME/drf-spectacular $ cd drf-spectacular $ python -m venv venv $ source venv/bin/activate (venv) $ pip install -r requirements.txt (venv) $ ./runtests.py # runs tests (pytest) & linting (isort, flake8, mypy) With that out of the way, we hope to hear from you soon. .. _code coverage: https://app.codecov.io/gh/tfranzel/drf-spectacular .. _test suite: https://github.com/tfranzel/drf-spectacular/tree/master/tests .. _blueprints: https://drf-spectacular.readthedocs.io/en/latest/blueprints.html .. _early feedback: https://github.com/tfranzel/drf-spectacular/issues .. _test_regressions.py: https://github.com/tfranzel/drf-spectacular/blob/master/tests/test_regressions.py drf-spectacular-0.27.0/LICENSE000066400000000000000000000031561453572150400156770ustar00rootroot00000000000000Copyright © 2011-present, Encode OSS Ltd. Copyright © 2019-2021, T. Franzel , Cashlink Technologies GmbH. Copyright © 2021-present, T. Franzel . All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. drf-spectacular-0.27.0/MANIFEST.in000066400000000000000000000006211453572150400164220ustar00rootroot00000000000000include *.rst include LICENSE include drf_spectacular/py.typed include drf_spectacular/validation/*.json include drf_spectacular/templates/drf_spectacular/*.html include drf_spectacular/templates/drf_spectacular/*.js include runtests.py graft tests recursive-exclude tests *_out.yml graft requirements graft docs prune docs/_build prune docs/_static global-exclude __pycache__ global-exclude *.py[co] drf-spectacular-0.27.0/README.rst000066400000000000000000000276361453572150400163720ustar00rootroot00000000000000=============== drf-spectacular =============== |build-status| |codecov| |docs| |pypi-version| |pypi-dl| Sane and flexible `OpenAPI`_ (`3.0.3`_ & `3.1`_) schema generation for `Django REST framework`_. This project has 3 goals: 1. Extract as much schema information from DRF as possible. 2. Provide flexibility to make the schema usable in the real world (not only toy examples). 3. Generate a schema that works well with the most popular client generators. The code is a heavily modified fork of the `DRF OpenAPI generator `_, which is/was lacking all of the below listed features. Features - Serializers modelled as components. (arbitrary nesting and recursion supported) - `@extend_schema `_ decorator for customization of APIView, Viewsets, function-based views, and ``@action`` - additional parameters - request/response serializer override (with status codes) - polymorphic responses either manually with ``PolymorphicProxySerializer`` helper or via ``rest_polymorphic``'s PolymorphicSerializer) - ... and more customization options - Authentication support (DRF natives included, easily extendable) - Custom serializer class support (easily extendable) - ``SerializerMethodField()`` type via type hinting or ``@extend_schema_field`` - i18n support - Tags extraction - Request/response/parameter examples - Description extraction from ``docstrings`` - Vendor specification extensions (``x-*``) in info, operations, parameters, components, and security schemes - Sane fallbacks - Sane ``operation_id`` naming (based on path) - Schema serving with ``SpectacularAPIView`` (Redoc and Swagger-UI views are also available) - Optional input/output serializer component split - Callback operations - OpenAPI 3.1 support (via setting ``OAS_VERSION``) - Included support for: - `django-polymorphic `_ / `django-rest-polymorphic `_ - `SimpleJWT `_ - `DjangoOAuthToolkit `_ - `djangorestframework-jwt `_ (tested fork `drf-jwt `_) - `dj-rest-auth `_ (maintained fork of `django-rest-auth `_) - `djangorestframework-camel-case `_ (via postprocessing hook ``camelize_serializer_fields``) - `django-filter `_ - `drf-nested-routers `_ - `djangorestframework-recursive `_ - `djangorestframework-dataclasses `_ - `django-rest-framework-gis `_ - `Pydantic (>=2.0) `_ For more information visit the `documentation `_. License ------- Provided by `T. Franzel `_. `Licensed under 3-Clause BSD `_. Requirements ------------ - Python >= 3.6 - Django (2.2, 3.2, 4.0, 4.1, 4.2, 5.0) - Django REST Framework (3.10.3, 3.11, 3.12, 3.13, 3.14) Installation ------------ Install using ``pip``\ ... .. code:: bash $ pip install drf-spectacular then add drf-spectacular to installed apps in ``settings.py`` .. code:: python INSTALLED_APPS = [ # ALL YOUR APPS 'drf_spectacular', ] and finally register our spectacular AutoSchema with DRF. .. code:: python REST_FRAMEWORK = { # YOUR SETTINGS 'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema', } drf-spectacular ships with sane `default settings `_ that should work reasonably well out of the box. It is not necessary to specify any settings, but we recommend to specify at least some metadata. .. code:: python SPECTACULAR_SETTINGS = { 'TITLE': 'Your Project API', 'DESCRIPTION': 'Your project description', 'VERSION': '1.0.0', 'SERVE_INCLUDE_SCHEMA': False, # OTHER SETTINGS } .. _self-contained-ui-installation: Self-contained UI installation ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Certain environments have no direct access to the internet and as such are unable to retrieve Swagger UI or Redoc from CDNs. `drf-spectacular-sidecar`_ provides these static files as a separate optional package. Usage is as follows: .. code:: bash $ pip install drf-spectacular[sidecar] .. code:: python INSTALLED_APPS = [ # ALL YOUR APPS 'drf_spectacular', 'drf_spectacular_sidecar', # required for Django collectstatic discovery ] SPECTACULAR_SETTINGS = { 'SWAGGER_UI_DIST': 'SIDECAR', # shorthand to use the sidecar instead 'SWAGGER_UI_FAVICON_HREF': 'SIDECAR', 'REDOC_DIST': 'SIDECAR', # OTHER SETTINGS } Release management ^^^^^^^^^^^^^^^^^^ *drf-spectacular* deliberately stays below version *1.x.x* to signal that every new version may potentially break you. For production we strongly recommend pinning the version and inspecting a schema diff on update. With that said, we aim to be extremely defensive w.r.t. breaking API changes. However, we also acknowledge the fact that even slight schema changes may break your toolchain, as any existing bug may somehow also be used as a feature. We define version increments with the following semantics. *y-stream* increments may contain potentially breaking changes to both API and schema. *z-stream* increments will never break the API and may only contain schema changes that should have a low chance of breaking you. Take it for a spin ------------------ Generate your schema with the CLI: .. code:: bash $ ./manage.py spectacular --color --file schema.yml $ docker run -p 80:8080 -e SWAGGER_JSON=/schema.yml -v ${PWD}/schema.yml:/schema.yml swaggerapi/swagger-ui If you also want to validate your schema add the ``--validate`` flag. Or serve your schema directly from your API. We also provide convenience wrappers for ``swagger-ui`` or ``redoc``. .. code:: python from drf_spectacular.views import SpectacularAPIView, SpectacularRedocView, SpectacularSwaggerView urlpatterns = [ # YOUR PATTERNS path('api/schema/', SpectacularAPIView.as_view(), name='schema'), # Optional UI: path('api/schema/swagger-ui/', SpectacularSwaggerView.as_view(url_name='schema'), name='swagger-ui'), path('api/schema/redoc/', SpectacularRedocView.as_view(url_name='schema'), name='redoc'), ] Usage ----- *drf-spectacular* works pretty well out of the box. You might also want to set some metadata for your API. Just create a ``SPECTACULAR_SETTINGS`` dictionary in your ``settings.py`` and override the defaults. Have a look at the `available settings `_. The toy examples do not cover your cases? No problem, you can heavily customize how your schema will be rendered. Customization by using ``@extend_schema`` ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Most customization cases should be covered by the ``extend_schema`` decorator. We usually get pretty far with specifying ``OpenApiParameter`` and splitting request/response serializers, but the sky is the limit. .. code:: python from drf_spectacular.utils import extend_schema, OpenApiParameter, OpenApiExample from drf_spectacular.types import OpenApiTypes class AlbumViewset(viewset.ModelViewset): serializer_class = AlbumSerializer @extend_schema( request=AlbumCreationSerializer, responses={201: AlbumSerializer}, ) def create(self, request): # your non-standard behaviour return super().create(request) @extend_schema( # extra parameters added to the schema parameters=[ OpenApiParameter(name='artist', description='Filter by artist', required=False, type=str), OpenApiParameter( name='release', type=OpenApiTypes.DATE, location=OpenApiParameter.QUERY, description='Filter by release date', examples=[ OpenApiExample( 'Example 1', summary='short optional summary', description='longer description', value='1993-08-23' ), ... ], ), ], # override default docstring extraction description='More descriptive text', # provide Authentication class that deviates from the views default auth=None, # change the auto-generated operation name operation_id=None, # or even completely override what AutoSchema would generate. Provide raw Open API spec as Dict. operation=None, # attach request/response examples to the operation. examples=[ OpenApiExample( 'Example 1', description='longer description', value=... ), ... ], ) def list(self, request): # your non-standard behaviour return super().list(request) @extend_schema( request=AlbumLikeSerializer, responses={204: None}, methods=["POST"] ) @extend_schema(description='Override a specific method', methods=["GET"]) @action(detail=True, methods=['post', 'get']) def set_password(self, request, pk=None): # your action behaviour ... More customization ^^^^^^^^^^^^^^^^^^ Still not satisfied? You want more! We still got you covered. Visit `customization `_ for more information. Testing ------- Install testing requirements. .. code:: bash $ pip install -r requirements.txt Run with runtests. .. code:: bash $ ./runtests.py You can also use the excellent `tox`_ testing tool to run the tests against all supported versions of Python and Django. Install tox globally, and then simply run: .. code:: bash $ tox .. _Django REST framework: https://www.django-rest-framework.org/ .. _OpenAPI: https://swagger.io/ .. _3.0.3: https://spec.openapis.org/oas/v3.0.3 .. _3.1: https://spec.openapis.org/oas/v3.1.0 .. _tox: https://tox.wiki/ .. _drf-spectacular-sidecar: https://github.com/tfranzel/drf-spectacular-sidecar .. |build-status| image:: https://github.com/tfranzel/drf-spectacular/actions/workflows/ci.yml/badge.svg :target: https://github.com/tfranzel/drf-spectacular/actions/workflows/ci.yml .. |pypi-version| image:: https://img.shields.io/pypi/v/drf-spectacular.svg :target: https://pypi.org/project/drf-spectacular/ .. |codecov| image:: https://codecov.io/gh/tfranzel/drf-spectacular/branch/master/graph/badge.svg :target: https://codecov.io/gh/tfranzel/drf-spectacular .. |docs| image:: https://readthedocs.org/projects/drf-spectacular/badge/ :target: https://drf-spectacular.readthedocs.io/ .. |pypi-dl| image:: https://img.shields.io/pypi/dm/drf-spectacular :target: https://pypi.org/project/drf-spectacular/ drf-spectacular-0.27.0/codecov.yml000066400000000000000000000001701453572150400170300ustar00rootroot00000000000000coverage: status: project: default: threshold: 1% patch: default: threshold: 100% drf-spectacular-0.27.0/docs/000077500000000000000000000000001453572150400156155ustar00rootroot00000000000000drf-spectacular-0.27.0/docs/Makefile000066400000000000000000000011721453572150400172560ustar00rootroot00000000000000# Minimal makefile for Sphinx documentation # # You can set these variables from the command line, and also # from the environment for the first two. SPHINXOPTS ?= SPHINXBUILD ?= sphinx-build SOURCEDIR = . BUILDDIR = _build # Put it first so that "make" without argument is like "make help". help: @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) .PHONY: help Makefile # Catch-all target: route all unknown targets to Sphinx using the new # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). %: Makefile @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) drf-spectacular-0.27.0/docs/_static/000077500000000000000000000000001453572150400172435ustar00rootroot00000000000000drf-spectacular-0.27.0/docs/_static/.gitignore000066400000000000000000000000001453572150400212210ustar00rootroot00000000000000drf-spectacular-0.27.0/docs/blueprints.rst000066400000000000000000000134721453572150400205450ustar00rootroot00000000000000.. _blueprints: Extension Blueprints ==================== Blueprints are a collection of schema fixes for Django and REST Framework apps. Some libraries/apps do not play well with *drf-spectacular*'s automatic introspection. With extensions you can manually provide the necessary information to generate a better schema. There is no blueprint for the app you are looking for? No problem, you can easily write extensions yourself. Take the blueprints here as examples and have a look at :ref:`customization`. Feel free to contribute new ones or fixes with a `PR `_. Blueprint files can be found `here `_. .. note:: Simply copy&paste the snippets into your codebase. The extensions register themselves automatically. Just be sure that the python interpreter sees them at least once. To that end, we suggest creating a ``PROJECT/schema.py`` file and importing it in your ``PROJECT/__init__.py`` (same directory as ``settings.py`` and ``urls.py``) with ``import PROJECT.schema``. Now you are all set. dj-stripe --------- Stripe Models for Django: `dj-stripe `_ .. literalinclude:: blueprints/djstripe.py django-oscar-api ---------------- RESTful API for django-oscar: `django-oscar-api `_ .. literalinclude:: blueprints/oscarapi.py djangorestframework-api-key --------------------------- Since `djangorestframework-api-key `_ has no entry in ``authentication_classes``, *drf-spectacular* cannot pick up this library. To alleviate this shortcoming, you can manually add the appropriate security scheme. .. note:: Usage of the ``SECURITY`` setting is discouraged, unless there are special circumstances like here for example. For almost all cases ``OpenApiAuthenticationExtension`` is strongly preferred, because ``SECURITY`` will get appended to every endpoint in the schema regardless of effectiveness. .. code:: python SPECTACULAR_SETTINGS = { "APPEND_COMPONENTS": { "securitySchemes": { "ApiKeyAuth": { "type": "apiKey", "in": "header", "name": "Authorization" } } }, "SECURITY": [{"ApiKeyAuth": [], }], ... } Polymorphic models ------------------ Using polymorphic models/serializers unfortunately yields flat serializers due to the way the serializers are constructed. This means the polymorphic serializers have no inheritance hierarchy that represents common functionality. These extensions retroactively build a hierarchy by rolling up the "common denominator" fields into the base components, and importing those into the sub-components via ``allOf``. This results in components that better represent the structure of the underlying serializers/models from which they originated. The components work perfectly fine without this extension, but in some cases generated client code has a hard time with the disjunctive nature of the unmodified components. This blueprint is designed to fix that issue. .. literalinclude:: blueprints/rollup.py RapiDoc ------- `RapiDoc`__ is documentation tool that can be used as an alternate to Redoc or Swagger UI. __ https://mrin9.github.io/RapiDoc/ .. literalinclude:: blueprints/rapidoc.py .. literalinclude:: blueprints/rapidoc.html Elements -------- `Elements`__ is another documentation tool that can be used as an alternate to Redoc or Swagger UI. __ https://stoplight.io/open-source/elements .. literalinclude:: blueprints/elements.py .. literalinclude:: blueprints/elements.html drf-rw-serializers ------------------ `drf-rw-serializers`__ provides generic views, viewsets and mixins that extend the Django REST Framework ones adding separated serializers for read and write operations. *drf-spectacular* requires just a small ``AutoSchema`` augmentation to make it aware of ``drf-rw-serializers``. Remember to replace the ``AutoSchema`` in ``DEFAULT_SCHEMA_CLASS``. __ https://github.com/vintasoftware/drf-rw-serializers .. literalinclude:: blueprints/drf_rw_serializers.py drf-extra-fields Base64FileField -------------------------------- `drf-extra-fields`__ provides a ``Base64FileField`` and ``Base64ImageField`` that automatically represent binary files as base64 encoded strings. This is a useful way to embed files within a larger JSON API and keep all data within the same tree and served with a single request or response. Because requests to these fields require a base64 encoded string and responses can be either a URI or base64 contents (if ``represent_as_base64=True``) custom schema generation logic is required as this differs from the default DRF ``FileField``. .. literalinclude:: blueprints/drf_extra_fields.py __ https://github.com/Hipo/drf-extra-fields django-auth-adfs ---------------- `django-auth-adfs `_ provides "a Django authentication backend for Microsoft ADFS and Azure AD". The blueprint works for the Azure AD configuration guide (see: https://django-auth-adfs.readthedocs.io/en/latest/azure_ad_config_guide.html). .. literalinclude:: blueprints/django_auth_adfs.py django-parler-rest ------------------ `django-parler-rest `_ integration for translation package `django-parler `_. .. literalinclude:: blueprints/django_parler_rest.py Pydantic -------- Preliminary support for `Pydantic `_ models. Catches decorated Pydantic classes and integrates their schema. Pydantic 2 is now officially supported without any manual steps. Pydantic 1: .. literalinclude:: blueprints/pydantic.py drf-spectacular-0.27.0/docs/blueprints/000077500000000000000000000000001453572150400200045ustar00rootroot00000000000000drf-spectacular-0.27.0/docs/blueprints/django_auth_adfs.py000066400000000000000000000011321453572150400236330ustar00rootroot00000000000000from drf_spectacular.extensions import OpenApiAuthenticationExtension from drf_spectacular.plumbing import build_bearer_security_scheme_object class AdfsAccessTokenAuthenticationScheme(OpenApiAuthenticationExtension): target_class = 'django_auth_adfs.rest_framework.AdfsAccessTokenAuthentication' name = 'jwtAuth' def get_security_definition(self, auto_schema): return build_bearer_security_scheme_object(header_name='AUTHORIZATION', token_prefix='Bearer', bearer_format='JWT') drf-spectacular-0.27.0/docs/blueprints/django_parler_rest.py000066400000000000000000000017171453572150400242300ustar00rootroot00000000000000from django.conf import settings from drf_spectacular.extensions import OpenApiSerializerFieldExtension from drf_spectacular.plumbing import build_object_type class TranslationsFieldFix(OpenApiSerializerFieldExtension): target_class = 'parler_rest.fields.TranslatedFieldsField' def map_serializer_field(self, auto_schema, direction): # Obtain auto-generated sub-serializer from parler_rest # Contains the fields wrapped in parler.models.TranslatedFields() translation_serializer = self.target.serializer_class # resolve translation sub-serializer into reusable component. translation_component = auto_schema.resolve_serializer( translation_serializer, direction ) # advertise each language provided in PARLER_LANGUAGES return build_object_type( properties={ i['code']: translation_component.ref for i in settings.PARLER_LANGUAGES[None] } )drf-spectacular-0.27.0/docs/blueprints/djstripe.py000066400000000000000000000013511453572150400222020ustar00rootroot00000000000000from djstripe.contrib.rest_framework.serializers import ( CreateSubscriptionSerializer, SubscriptionSerializer ) from drf_spectacular.extensions import OpenApiViewExtension from drf_spectacular.utils import extend_schema class FixDjstripeSubscriptionRestView(OpenApiViewExtension): target_class = 'djstripe.contrib.rest_framework.views.SubscriptionRestView' def view_replacement(self): class Fixed(self.target_class): serializer_class = SubscriptionSerializer @extend_schema( request=CreateSubscriptionSerializer, responses=CreateSubscriptionSerializer ) def post(self, request, *args, **kwargs): pass return Fixed drf-spectacular-0.27.0/docs/blueprints/drf_extra_fields.py000066400000000000000000000031301453572150400236570ustar00rootroot00000000000000from drf_spectacular.extensions import OpenApiSerializerFieldExtension from drf_spectacular.openapi import AutoSchema from drf_spectacular.plumbing import append_meta from drf_spectacular.plumbing import build_basic_type from drf_spectacular.types import OpenApiTypes from drf_spectacular.utils import Direction class Base64FileFieldSchema(OpenApiSerializerFieldExtension): target_class = "drf_extra_fields.fields.Base64FileField" def map_serializer_field(self, auto_schema, direction): if direction == "request": return build_basic_type(OpenApiTypes.BYTE) elif direction == "response": if self.target.represent_in_base64: return build_basic_type(OpenApiTypes.BYTE) else: return build_basic_type(OpenApiTypes.URI) class Base64ImageFieldSchema(Base64FileFieldSchema): target_class = "drf_extra_fields.fields.Base64ImageField" class PresentablePrimaryKeyRelatedFieldSchema(OpenApiSerializerFieldExtension): target_class = 'drf_extra_fields.relations.PresentablePrimaryKeyRelatedField' def map_serializer_field(self, auto_schema: AutoSchema, direction: Direction): if direction == 'request': return build_basic_type(OpenApiTypes.INT) meta = auto_schema._get_serializer_field_meta(self.target, direction) schema = auto_schema.resolve_serializer( self.target.presentation_serializer( context=self.target.context, **self.target.presentation_serializer_kwargs, ), direction, ).ref return append_meta(schema, meta) drf-spectacular-0.27.0/docs/blueprints/drf_rw_serializers.py000066400000000000000000000011361453572150400242560ustar00rootroot00000000000000from drf_rw_serializers.generics import GenericAPIView as RWGenericAPIView from drf_spectacular.openapi import AutoSchema class CustomAutoSchema(AutoSchema): """ Utilize custom drf_rw_serializers methods for directional serializers """ def get_request_serializer(self): if isinstance(self.view, RWGenericAPIView): return self.view.get_write_serializer() return self._get_serializer() def get_response_serializers(self): if isinstance(self.view, RWGenericAPIView): return self.view.get_read_serializer() return self._get_serializer() drf-spectacular-0.27.0/docs/blueprints/elements.html000066400000000000000000000006211453572150400225050ustar00rootroot00000000000000 {{ title|default:"Elements" }} drf-spectacular-0.27.0/docs/blueprints/elements.py000066400000000000000000000027531453572150400222010ustar00rootroot00000000000000from rest_framework.renderers import TemplateHTMLRenderer from rest_framework.response import Response from rest_framework.reverse import reverse from rest_framework.views import APIView from drf_spectacular.plumbing import get_relative_url, set_query_parameters from drf_spectacular.settings import spectacular_settings from drf_spectacular.utils import extend_schema from drf_spectacular.views import AUTHENTICATION_CLASSES class SpectacularElementsView(APIView): renderer_classes = [TemplateHTMLRenderer] permission_classes = spectacular_settings.SERVE_PERMISSIONS authentication_classes = AUTHENTICATION_CLASSES url_name = 'schema' url = None template_name = 'elements.html' title = spectacular_settings.TITLE @extend_schema(exclude=True) def get(self, request, *args, **kwargs): return Response( data={ 'title': self.title, 'js_dist': 'https://unpkg.com/@stoplight/elements/web-components.min.js', 'css_dist': 'https://unpkg.com/@stoplight/elements/styles.min.css', 'schema_url': self._get_schema_url(request), }, template_name=self.template_name ) def _get_schema_url(self, request): schema_url = self.url or get_relative_url(reverse(self.url_name, request=request)) return set_query_parameters( url=schema_url, lang=request.GET.get('lang'), version=request.GET.get('version') ) drf-spectacular-0.27.0/docs/blueprints/oscarapi.py000066400000000000000000000071301453572150400221600ustar00rootroot00000000000000from rest_framework import serializers from drf_spectacular.extensions import ( OpenApiSerializerExtension, OpenApiSerializerFieldExtension, OpenApiViewExtension ) from drf_spectacular.plumbing import build_basic_type from drf_spectacular.types import OpenApiTypes from drf_spectacular.utils import OpenApiParameter, extend_schema, extend_schema_field class Fix1(OpenApiViewExtension): target_class = 'oscarapi.views.root.api_root' def view_replacement(self): return extend_schema(responses=OpenApiTypes.OBJECT)(self.target_class) class Fix2(OpenApiViewExtension): target_class = 'oscarapi.views.product.ProductAvailability' def view_replacement(self): from oscarapi.serializers.product import AvailabilitySerializer class Fixed(self.target_class): serializer_class = AvailabilitySerializer return Fixed class Fix3(OpenApiViewExtension): target_class = 'oscarapi.views.product.ProductPrice' def view_replacement(self): from oscarapi.serializers.checkout import PriceSerializer class Fixed(self.target_class): serializer_class = PriceSerializer return Fixed class Fix4(OpenApiViewExtension): target_class = 'oscarapi.views.checkout.UserAddressDetail' def view_replacement(self): from oscar.apps.address.models import UserAddress class Fixed(self.target_class): queryset = UserAddress.objects.none() return Fixed class Fix5(OpenApiViewExtension): target_class = 'oscarapi.views.product.CategoryList' def view_replacement(self): class Fixed(self.target_class): @extend_schema(parameters=[ OpenApiParameter(name='breadcrumbs', type=OpenApiTypes.STR, location=OpenApiParameter.PATH) ]) def get(self, request, *args, **kwargs): pass return Fixed class Fix6(OpenApiSerializerExtension): target_class = 'oscarapi.serializers.checkout.OrderSerializer' def map_serializer(self, auto_schema, direction): from oscarapi.serializers.checkout import OrderOfferDiscountSerializer, OrderVoucherOfferSerializer class Fixed(self.target_class): @extend_schema_field(OrderOfferDiscountSerializer(many=True)) def get_offer_discounts(self): pass @extend_schema_field(OpenApiTypes.URI) def get_payment_url(self): pass @extend_schema_field(OrderVoucherOfferSerializer(many=True)) def get_voucher_discounts(self): pass return auto_schema._map_serializer(Fixed, direction) class Fix7(OpenApiSerializerFieldExtension): target_class = 'oscarapi.serializers.fields.CategoryField' def map_serializer_field(self, auto_schema, direction): return build_basic_type(OpenApiTypes.STR) class Fix8(OpenApiSerializerFieldExtension): target_class = 'oscarapi.serializers.fields.AttributeValueField' def map_serializer_field(self, auto_schema, direction): return { 'oneOf': [ build_basic_type(OpenApiTypes.STR), ] } class Fix9(OpenApiSerializerExtension): target_class = 'oscarapi.serializers.basket.BasketSerializer' def map_serializer(self, auto_schema, direction): class Fixed(self.target_class): is_tax_known = serializers.SerializerMethodField() def get_is_tax_known(self) -> bool: pass return auto_schema._map_serializer(Fixed, direction) class Fix10(Fix9): target_class = 'oscarapi.serializers.basket.BasketLineSerializer' drf-spectacular-0.27.0/docs/blueprints/pydantic.py000066400000000000000000000017641453572150400222010ustar00rootroot00000000000000from drf_spectacular.extensions import OpenApiSerializerExtension from drf_spectacular.plumbing import ResolvedComponent from pydantic.schema import model_schema class PydanticExtension(OpenApiSerializerExtension): target_class = "pydantic.BaseModel" match_subclasses = True priority = 1 def get_name(self, auto_schema, direction): return self.target.__name__ def map_serializer(self, auto_schema, direction): # let pydantic generate a JSON schema schema = model_schema(self.target, ref_prefix="#/components/schemas/") # pull out potential sub-schemas and put them into component section for sub_name, sub_schema in schema.pop("definitions", {}).items(): component = ResolvedComponent( name=sub_name, type=ResolvedComponent.SCHEMA, object=sub_name, schema=sub_schema, ) auto_schema.registry.register_on_missing(component) return schema drf-spectacular-0.27.0/docs/blueprints/rapidoc.html000066400000000000000000000005311453572150400223120ustar00rootroot00000000000000 {{ title|default:"RapiDoc" }} drf-spectacular-0.27.0/docs/blueprints/rapidoc.py000066400000000000000000000025721453572150400220050ustar00rootroot00000000000000from rest_framework.renderers import TemplateHTMLRenderer from rest_framework.response import Response from rest_framework.reverse import reverse from rest_framework.views import APIView from drf_spectacular.plumbing import get_relative_url, set_query_parameters from drf_spectacular.settings import spectacular_settings from drf_spectacular.utils import extend_schema from drf_spectacular.views import AUTHENTICATION_CLASSES class SpectacularRapiDocView(APIView): renderer_classes = [TemplateHTMLRenderer] permission_classes = spectacular_settings.SERVE_PERMISSIONS authentication_classes = AUTHENTICATION_CLASSES url_name = 'schema' url = None template_name = 'rapidoc.html' title = spectacular_settings.TITLE @extend_schema(exclude=True) def get(self, request, *args, **kwargs): return Response( data={ 'title': self.title, 'dist': 'https://cdn.jsdelivr.net/npm/rapidoc@latest', 'schema_url': self._get_schema_url(request), }, template_name=self.template_name, ) def _get_schema_url(self, request): schema_url = self.url or get_relative_url(reverse(self.url_name, request=request)) return set_query_parameters( url=schema_url, lang=request.GET.get('lang'), version=request.GET.get('version') ) drf-spectacular-0.27.0/docs/blueprints/rollup.py000066400000000000000000000072431453572150400217010ustar00rootroot00000000000000from drf_spectacular.contrib.rest_polymorphic import PolymorphicSerializerExtension from drf_spectacular.plumbing import ResolvedComponent from drf_spectacular.serializers import PolymorphicProxySerializerExtension from drf_spectacular.settings import spectacular_settings class RollupMixin: """ This is a schema helper that pulls the "common denominator" fields from child components into their parent component. It only applies to PolymorphicSerializer as well as PolymorphicProxySerializer, where there is an (implicit) inheritance hierarchy. The actual functionality is realized via extensions defined below. """ def map_serializer(self, auto_schema, direction): schema = super().map_serializer(auto_schema, direction) if isinstance(self, PolymorphicProxySerializerExtension): sub_serializers = self.target.serializers else: sub_serializers = [ self.target._get_serializer_from_model_or_instance(sub_model) for sub_model in self.target.model_serializer_mapping ] resolved_sub_serializers = [ auto_schema.resolve_serializer(sub, direction) for sub in sub_serializers ] # this will only be generated on return of map_serializer so mock it for now mocked_component = ResolvedComponent( name=auto_schema._get_serializer_name(self.target, direction), type=ResolvedComponent.SCHEMA, object=self.target, schema=schema ) # hack for recursive models. at the time of extension execution, not all sub # serializer schema have been generated, so no rollup is possible. # by registering a local variable scoped postproc hook, we delay this # execution to the end where all schemas are present. def postprocessing_rollup_hook(generator, result, **kwargs): rollup_properties(mocked_component, resolved_sub_serializers) result['components'] = generator.registry.build({}) return result # register postproc hook. must run before enum postproc due to rebuilding the registry spectacular_settings.POSTPROCESSING_HOOKS.insert(0, postprocessing_rollup_hook) # and do nothing for now return schema def rollup_properties(component, resolved_sub_serializers): # rollup already happened (spectacular bug and normally not needed) if any('allOf' in r.schema for r in resolved_sub_serializers): return all_field_sets = [ set(list(r.schema['properties'])) for r in resolved_sub_serializers ] common_fields = all_field_sets[0].intersection(*all_field_sets[1:]) common_schema = { 'properties': {}, 'required': set(), } # substitute sub serializers' common fields with base class for r in resolved_sub_serializers: for cf in sorted(common_fields): if cf in r.schema['properties']: common_schema['properties'][cf] = r.schema['properties'][cf] del r.schema['properties'][cf] if cf in r.schema.get('required', []): common_schema['required'].add(cf) r.schema = {'allOf': [component.ref, r.schema]} # modify regular schema for field rollup del component.schema['oneOf'] component.schema['properties'] = common_schema['properties'] if common_schema['required']: component.schema['required'] = sorted(common_schema['required']) class PolymorphicRollupSerializerExtension(RollupMixin, PolymorphicSerializerExtension): priority = 1 class PolymorphicProxyRollupSerializerExtension(RollupMixin, PolymorphicProxySerializerExtension): priority = 1 drf-spectacular-0.27.0/docs/changelog.rst000066400000000000000000000000361453572150400202750ustar00rootroot00000000000000.. include:: ../CHANGELOG.rst drf-spectacular-0.27.0/docs/client_generation.rst000066400000000000000000000101401453572150400220340ustar00rootroot00000000000000.. _client_generation: Client generation ================= *drf-spectacular* aims to generate the most accurate schema possible under the constraints of OpenAPI 3.0.3. Unfortunately, sometimes this goal conflicts with generating a good and functional client. To serve the two main use cases, i.e. documenting the API and generating clients, we opt for getting the most accurate schema first, and then provide settings that allow to resolve potential issues with client generation. .. note:: TL;DR - Simply setting ``'COMPONENT_SPLIT_REQUEST': True`` will most likely yield the best and most accurate client. .. note:: *drf-spectacular* generates warnings where it recognizes potential problems. Some warnings are important to having a correct client. Fixing all warning is highly recommended. .. note:: For generating clients with CI, we highly recommend using ``./manage.py spectacular --file schema.yaml --validate --fail-on-warn`` to catch potential problems early on. Component issues ---------------- Most client issues revolve around the construction of components. Some client targets have trouble with ``readOnly`` and ``required`` fields like ``id``. Even though technically correct, the generated code may not allow creating objects with ``id`` missing for ``POST`` requests. Some fields like ``FileField`` behave very differently on requests and responses and are simply not translatable into a single component. The most useful setting is ``'COMPONENT_SPLIT_REQUEST': True``, where all affected components are split into request and response components. This takes care of almost all ``required``, ``writeOnly``, ``readOnly`` issues, and generally delivers code that is easier to understand and harder to misuse. Sometimes you may only want to fix the ``required``/``readOnly`` issue without splitting all components. This can be explicitly addressed with ``'COMPONENT_NO_READ_ONLY_REQUIRED': True``. Because this setting waters down the correctness of the schema, we generally recommend using ``COMPONENT_SPLIT_REQUEST`` instead. ``'COMPONENT_SPLIT_PATCH': True`` is already enabled by default as ``PATCH`` and ``POST`` requests clash on the ``required`` property and cannot be adequately modeled with a single component. Relevant settings: .. code:: python # Split components into request and response parts where appropriate 'COMPONENT_SPLIT_REQUEST': False, # Aid client generator targets that have trouble with read-only properties. 'COMPONENT_NO_READ_ONLY_REQUIRED': False, # Create separate components for PATCH endpoints (without required list) 'COMPONENT_SPLIT_PATCH': True, Enum issues ----------- Some generator targets choke on combined enum components or having a ``null`` choice on a ``nullable: true`` field. Even though it is the correct way (according to the specification), it sadly breaks some generator targets. Setting ``'ENUM_ADD_EXPLICIT_BLANK_NULL_CHOICE': False`` will create a less accurate schema that tends to offend fewer generator targets. For more information please refer to the `official documentation`__ and more specifically the `specification proposal`__. __ https://swagger.io/docs/specification/data-models/enums/ __ https://github.com/OAI/OpenAPI-Specification/blob/main/proposals/2019-10-31-Clarify-Nullable.md#user-content-if-a-schema-specifies-nullable-true-and-enum-1-2-3-does-that-schema-allow-null-values-see-1900 Relevant settings: .. code:: python # Adds "blank" and "null" enum choices where appropriate. disable on client generation issues 'ENUM_ADD_EXPLICIT_BLANK_NULL_CHOICE': True, Type issues ----------- Some generator targets behave differently depending on how ``additionalProperties`` is structured. According to the specification all three variations should yield identical results, which unfortunately is not the case in practice. Relevant settings: .. code:: python # Determines if and how free-form 'additionalProperties' should be emitted in the schema. Some # code generator targets are sensitive to this. None disables generic 'additionalProperties'. # allowed values are 'dict', 'bool', None 'GENERIC_ADDITIONAL_PROPERTIES': 'dict', drf-spectacular-0.27.0/docs/conf.py000066400000000000000000000070761453572150400171260ustar00rootroot00000000000000# Configuration file for the Sphinx documentation builder. # # This file only contains a selection of the most common options. For a full # list see the documentation: # https://www.sphinx-doc.org/en/master/usage/configuration.html # -- Path setup -------------------------------------------------------------- # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # import os import sys from django.conf import settings settings.configure(USE_I18N=False, USE_L10N=False) sys.path[:0] = [os.path.abspath('../'), os.path.abspath('./')] # -- Project information ----------------------------------------------------- project = 'drf-spectacular' copyright = '2020, T. Franzel' author = 'T. Franzel' needs_sphinx = '4.1' # -- General configuration --------------------------------------------------- # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ 'extensions', 'sphinx.ext.autodoc', 'sphinx.ext.intersphinx', ] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This pattern also affects html_static_path and html_extra_path. exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] default_role = 'default-role-error' linkcheck_allowed_redirects = { r"^https://tox\.wiki/$": r"https://tox\.wiki/en/latest/$", r"^https://drf-spectacular\.readthedocs\.io/$": r"https://drf-spectacular\.readthedocs\.io/en/latest/$", r"^https://docs\.djangoproject\.com/en/stable/": r"^https://docs\.djangoproject\.com/en/\d+\.\d+/", r"^https://github\.com/tfranzel/drf-spectacular/issues/\d+": "https://github\.com/tfranzel/drf-spectacular/pull/\d+", } linkcheck_ignore = [ # Special-use addresses and domain names. (RFC 6761/6890) r"^https?://(?:127\.0\.0\.1|\[::1\])(?::\d+)?/", r"^https?://(?:[^/\.]+\.)*example\.(?:com|net|org)(?::\d+)?/", r"^https?://(?:[^/\.]+\.)*(?:example|invalid|localhost|test)(?::\d+)?/", ] nitpicky = True nitpick_ignore_regex = [ # Unresolvable type hinting forward references. ('py:class', r'(?:APIView|AutoSchema|OpenApiFilterExtension)'), # Unresolvable type hinting references to packages without intersphinx support. ('py:class', r'rest_framework\..+'), ('py:class', r'django\.utils\.functional\.Promise'), # Internal undocumented objects. ('py:class', r'drf_spectacular\.generators\..+'), ('py:class', r'drf_spectacular\.plumbing\..+'), ('py:class', r'drf_spectacular\.utils\.F'), ] intersphinx_mapping = { 'python': ('https://docs.python.org/3/', None), 'django': ('https://docs.djangoproject.com/en/stable/', 'https://docs.djangoproject.com/en/stable/_objects/'), 'drf-yasg': ('https://drf-yasg.readthedocs.io/en/stable/', None), } # -- Options for HTML output ------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # # html_theme = 'alabaster' html_theme = 'sphinx_rtd_theme' # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] drf-spectacular-0.27.0/docs/contributing.rst000066400000000000000000000000411453572150400210510ustar00rootroot00000000000000.. include:: ../CONTRIBUTING.rst drf-spectacular-0.27.0/docs/customization.rst000066400000000000000000000343451453572150400212700ustar00rootroot00000000000000.. _customization: Workflow & schema customization =============================== You are not satisfied with your generated schema? Follow these steps in order to get your schema closer to your API. .. note:: The warnings emitted by ``./manage.py spectacular --file schema.yaml --validate`` are intended as an indicator to where *drf-spectacular* discovered issues. Sane fallbacks are used wherever possible and some warnings might not even be relevant to you. The remaining issues can be solved with the following steps. Step 1: ``queryset`` and ``serializer_class`` --------------------------------------------- Introspection heavily relies on those two attributes. ``get_serializer_class()`` and ``get_serializer()`` are also used if available. You can also set those on ``APIView``. Even though this is not supported by DRF, *drf-spectacular* will pick them up and use them. Step 2: :py:class:`@extend_schema ` ------------------------------------------------------------------------ Decorate your view functions with the :py:func:`@extend_schema ` decorator. There is a multitude of override options, but you only need to override what was not properly discovered in the introspection. .. code-block:: python class PersonView(viewsets.GenericViewSet): @extend_schema( parameters=[ QuerySerializer, # serializer fields are converted to parameters OpenApiParameter("nested", QuerySerializer), # serializer object is converted to a parameter OpenApiParameter("queryparam1", OpenApiTypes.UUID, OpenApiParameter.QUERY), OpenApiParameter("pk", OpenApiTypes.UUID, OpenApiParameter.PATH), # path variable was overridden ], request=YourRequestSerializer, responses=YourResponseSerializer, # more customizations ) def retrieve(self, request, pk, *args, **kwargs) # your code .. note:: ``responses`` can be detailed further by providing a dictionary instead. This could be for example ``{201: YourResponseSerializer, ...}`` or ``{(200, 'application/pdf'): OpenApiTypes.BINARY, ...}``. .. note:: For simple responses, you might not go through the hassle of writing an explicit serializer class. In those cases, you can simply specify the request/response with a call to :py:func:`inline_serializer `. This lets you conveniently define the endpoint's schema inline without actually writing a serializer class. .. note:: If you want to annotate methods that are provided by the base classes of a view, you have nothing to attach :py:func:`@extend_schema ` to. In those instances you can use :py:func:`@extend_schema_view ` to conveniently annotate the default implementations. .. code-block:: python class XViewset(mixins.ListModelMixin, viewsets.GenericViewSet): @extend_schema(description='text') def list(self, request, *args, **kwargs): return super().list(request, *args, **kwargs) is equivalent to .. code-block:: python @extend_schema_view( list=extend_schema(description='text') ) class XViewset(mixins.ListModelMixin, viewsets.GenericViewSet): ... .. note:: You may also use :py:func:`@extend_schema ` on views to attach annotations to all methods in that view (e.g. tags). Method annotations will take precedence over view annotation. Step 3: :py:class:`@extend_schema_field ` and type hints --------------------------------------------------------------------------------------------------- A custom ``SerializerField`` might not get picked up properly. You can inform *drf-spectacular* on what is to be expected with the :py:func:`@extend_schema_field ` decorator. It takes either basic types or a ``Serializer`` as argument. In case of basic types (e.g. ``str``, ``int``, etc.) a type hint is already sufficient. .. code-block:: python @extend_schema_field(OpenApiTypes.BYTE) # also takes basic python types class CustomField(serializers.Field): def to_representation(self, value): return urlsafe_base64_encode(b'\xf0\xf1\xf2') You can apply it also to the method of a ``SerializerMethodField``. .. code-block:: python class ErrorDetailSerializer(serializers.Serializer): field_custom = serializers.SerializerMethodField() @extend_schema_field(OpenApiTypes.DATETIME) def get_field_custom(self, object): return '2020-03-06 20:54:00.104248' Step 4: :py:class:`@extend_schema_serializer ` ---------------------------------------------------------------------------------------------- You may also decorate your serializer with :py:func:`@extend_schema_serializer `. Mainly used for excluding specific fields from the schema or attaching request/response examples. On rare occasions (e.g. envelope serializers), overriding list detection with ``many=False`` may come in handy. .. code:: python @extend_schema_serializer( exclude_fields=('single',), # schema ignore these fields examples = [ OpenApiExample( 'Valid example 1', summary='short summary', description='longer description', value={ 'songs': {'top10': True}, 'single': {'top10': True} }, request_only=True, # signal that example only applies to requests response_only=True, # signal that example only applies to responses ), ] ) class AlbumSerializer(serializers.ModelSerializer): songs = SongSerializer(many=True) single = SongSerializer(read_only=True) class Meta: fields = '__all__' model = Album Step 5: Extensions ------------------ The core purpose of extensions is to make the above customization mechanisms also available for library code. Usually, you cannot easily decorate or modify ``View``, ``Serializer`` or ``Field`` from libraries. Extensions provide a way to hook into the introspection without actually touching the library. All extensions work on the same principle. You provide a ``target_class`` (import path string or actual class) and then state what *drf-spectcular* should use instead of what it would normally discover. .. important:: The extensions register themselves automatically. Just be sure that the Python interpreter sees them at least once. It is good practice to collect your extensions in ``YOUR_MAIN_APP_NAME/schema.py`` and importing that file in your ``YOUR_MAIN_APP_NAME/apps.py``. Every proper Django app will already have an auto-generated ``apps.py`` file. Although not strictly necessary, doing the import in ``ready()`` is the most robust approach. It will make sure your environment (e.g. settings) is properly set up prior to loading. .. code-block:: python # your_main_app_name/apps.py class YourMainAppNameConfig(AppConfig): default_auto_field = "django.db.models.BigAutoField" name = "your_main_app_name" def ready(self): import your_main_app_name.schema # noqa: E402 .. note:: Only the first Extension matching the criteria is used. By setting the ``priority`` attribute on your extension, you can influence the matching order (default ``0``). Built-in Extensions have a priority of ``-1``. If you subclass built-in Extensions, don't forget to increase the priority. Replace views with :py:class:`OpenApiViewExtension ` ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Many libraries use ``@api_view`` or ``APIView`` instead of ``ViewSet`` or ``GenericAPIView``. In those cases, introspection has very little to work with. The purpose of this extension is to augment or switch out the encountered view (only for schema generation). Simply extending the discovered class ``class Fixed(self.target_class)`` with a ``queryset`` or ``serializer_class`` attribute will often solve most issues. .. code-block:: python class Fix4(OpenApiViewExtension): target_class = 'oscarapi.views.checkout.UserAddressDetail' def view_replacement(self): from oscar.apps.address.models import UserAddress class Fixed(self.target_class): queryset = UserAddress.objects.none() return Fixed Specify authentication with :py:class:`OpenApiAuthenticationExtension ` ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .. _customization_authentication_extension: Authentication classes that do not have 3rd party support will emit warnings and be ignored. Luckily authentication extensions are very easy to implement. Have a look at the `default authentication method extensions `_. A simple custom HTTP header based authentication could be achieved like this: .. code-block:: python class MyAuthenticationScheme(OpenApiAuthenticationExtension): target_class = 'my_app.MyAuthentication' # full import path OR class ref name = 'MyAuthentication' # name used in the schema def get_security_definition(self, auto_schema): return { 'type': 'apiKey', 'in': 'header', 'name': 'api_key', } Declare field output with :py:class:`OpenApiSerializerFieldExtension ` ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ This is mainly targeted to custom ``SerializerField``'s that are within library code. This extension is functionally equivalent to :py:func:`@extend_schema_field ` .. code-block:: python class CategoryFieldFix(OpenApiSerializerFieldExtension): target_class = 'oscarapi.serializers.fields.CategoryField' def map_serializer_field(self, auto_schema, direction): # equivalent to return {'type': 'string'} return build_basic_type(OpenApiTypes.STR) Declare serializer magic with :py:class:`OpenApiSerializerExtension ` ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ This is one of the more involved extension mechanisms. *drf-spectacular* uses those to implement `polymorphic serializers `_. The usage of this extension is rarely necessary because most custom ``Serializer`` classes stay very close to the default behaviour. In case your ``Serializer`` makes use of a custom ``ListSerializer`` (i.e. a custom ``to_representation()``), you can write a dedicated extensions for that. This is usually the case when ``many=True`` does not result in a plain list, but rather in augmented object with additional fields (e.g. envelopes). Declare custom/library filters with :py:class:`OpenApiFilterExtension ` ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ This extension only applies to filter and pagination classes and is rarely used. Built-in support for *django-filter* is realized with this extension. :py:class:`OpenApiFilterExtension ` replaces the filter's native ``get_schema_operation_parameters`` with your customized version, where you have full access to *drf-spectacular*'s more advanced introspection features. Step 6: Postprocessing hooks ---------------------------- The generated schema is still not to your liking? You are no easy customer, but there is one more thing you can do. Postprocessing hooks run at the very end of schema generation. This is how the choice ``Enum`` are consolidated into component objects. You can register hooks with the ``POSTPROCESSING_HOOKS`` setting. .. code-block:: python def custom_postprocessing_hook(result, generator, request, public): # your modifications to the schema in parameter result return result .. note:: Please note that setting ``POSTPROCESSING_HOOKS`` will override the default. If you intend to keep the ``Enum`` hook, be sure to add ``'drf_spectacular.hooks.postprocess_schema_enums'`` back into the list. Step 7: Preprocessing hooks --------------------------- .. _customization_preprocessing_hooks: Preprocessing hooks are applied shortly after collecting all API operations and before the actual schema generation starts. They provide an easy mechanism to alter which operations should be represented in your schema. You can exclude specific operations, prefix paths, introduce or hardcode path parameters or modify view initiation. additional hooks with the ``PREPROCESSING_HOOKS`` setting. .. code-block:: python def custom_preprocessing_hook(endpoints): # your modifications to the list of operations that are exposed in the schema for (path, path_regex, method, callback) in endpoints: pass return endpoints .. note:: A common use case would be the removal of duplicated ``{format}``-suffixed operations, for which we already provide the :py:func:`drf_spectacular.hooks.preprocess_exclude_path_format ` hook. You can simply enable this hook by adding the import path string to the ``PREPROCESSING_HOOKS``. Congratulations --------------- You should now have no more warnings and a spectacular schema that satisfies all your requirements. If that is not the case, feel free to open an `issue `_ and make a suggestion for improvement. drf-spectacular-0.27.0/docs/drf_spectacular.rst000066400000000000000000000021311453572150400215050ustar00rootroot00000000000000Package overview ================ drf\_spectacular\.utils ----------------------- .. automodule:: drf_spectacular.utils :members: :undoc-members: :show-inheritance: drf\_spectacular\.types ----------------------- .. autoclass:: drf_spectacular.types.OpenApiTypes :members: :undoc-members: :member-order: bysource drf\_spectacular\.views ----------------------- .. automodule:: drf_spectacular.views :members: :undoc-members: :show-inheritance: drf\_spectacular\.extensions ---------------------------- .. automodule:: drf_spectacular.extensions :members: :undoc-members: :show-inheritance: drf\_spectacular\.hooks ----------------------- .. automodule:: drf_spectacular.hooks :members: :undoc-members: :show-inheritance: drf\_spectacular\.openapi ------------------------- .. automodule:: drf_spectacular.openapi :members: :undoc-members: :show-inheritance: drf\_spectacular\.contrib\.django_filters ----------------------------------------- .. automodule:: drf_spectacular.contrib.django_filters :members: :show-inheritance: drf-spectacular-0.27.0/docs/drf_yasg.rst000066400000000000000000000320341453572150400201470ustar00rootroot00000000000000From *drf-yasg* to OpenAPI 3 ============================ `drf-yasg`__ is an excellent library and the most popular choice for generating OpenAPI 2.0 (formerly known as Swagger 2.0) schemas with `Django REST Framework`__. Unfortunately, it currently does not provide support for OpenAPI 3.x. Migration from *drf-yasg* to *drf-spectacular* requires some modifications, the complexity of which depends on what features are being used. __ https://pypi.org/project/drf-yasg __ https://pypi.org/project/djangorestframework/ .. note:: In contrast to *drf-yasg*, we don't package Redoc & Swagger UI but serve them via hyperlinked CDNs instead. If you want or need to serve those files yourself, you can do that with the optional `drf-spectacular-sidecar `_ package. See :ref:`installation instructions ` for further details. Decorators ---------- - :py:func:`@swagger_auto_schema ` is largely equivalent to :py:func:`@extend_schema `. - ``operation_description`` argument is called ``description`` - ``operation_summary`` argument is called ``summary`` - ``manual_parameters`` and ``query_serializer`` arguments are merged into a single ``parameters`` argument - ``security`` argument is called ``auth`` - ``request_body`` arguments is called ``request`` - Use ``None`` instead of :py:class:`drf_yasg.utils.no_body` - ``method`` argument doesn't exist, use ``methods`` instead (also supported by *drf-yasg*) - ``auto_schema`` has no equivalent. - ``extra_overrides`` has no equivalent. - ``field_inspectors`` has no equivalent. - ``filter_inspectors`` has no equivalent. - ``paginator_inspectors`` has no equivalent. - Additional arguments are also available: ``exclude``, ``operation``, ``versions``, ``examples``. - :py:func:`@swagger_serializer_method ` is equivalent to :py:func:`@extend_schema_field `. - ``component_name`` can be provided to break the field out as a separate component. - :py:func:`@extend_schema_serializer ` is available for overriding behavior of serializers. - Instead of using :py:func:`@method_decorator `, use :py:func:`@extend_schema_view `. - Instead of using ``swagger_schema_field``, use :py:func:`@extend_schema_field ` or :py:func:`@extend_schema_serializer `. Helper Classes -------------- - :py:class:`~drf_yasg.openapi.Parameter` is roughly equivalent to :py:class:`~drf_spectacular.utils.OpenApiParameter`. - ``in_`` argument is called ``location``. - ``schema`` argument should be passed as ``type``. - ``format`` argument is merged into ``type`` argument by using :py:class:`OpenApiTypes `. - setting the ``many`` argument to ``True`` causes the argument to take an array of values, and generates a schema similar to using the drf_yasg ``Items`` class on the ``items`` property. The type of the items in the array are defined by the ``type`` argument. - :py:class:`~drf_yasg.openapi.Response` is largely identical to :py:class:`~drf_spectacular.utils.OpenApiResponse`. - ``schema`` argument is called ``response`` - Order of arguments differs, so use keyword arguments. - :py:class:`~drf_spectacular.utils.OpenApiExample` is available for providing ``examples`` to :py:func:`@extend_schema `. - :py:class:`~drf_yasg.openapi.Schema` is not required and can be eliminated. Use a plain :py:class:`dict` instead. Types & Formats --------------- In place of separate ``drf_yasg.openapi.TYPE_*`` and ``drf_yasg.openapi.FORMAT_*`` constants, ``drf-spectacular`` provides the :py:class:`~drf_spectacular.types.OpenApiTypes` enum: - :py:data:`~drf_yasg.openapi.TYPE_BOOLEAN` is called :py:attr:`~drf_spectacular.types.OpenApiTypes.BOOL`, but you can use :py:class:`bool`. - :py:data:`~drf_yasg.openapi.TYPE_FILE` should be replaced by :py:attr:`~drf_spectacular.types.OpenApiTypes.BINARY` - :py:data:`~drf_yasg.openapi.TYPE_INTEGER` is called :py:attr:`~drf_spectacular.types.OpenApiTypes.INT`, but you can use :py:class:`int`. - :py:data:`~drf_yasg.openapi.TYPE_INTEGER` with :py:data:`~drf_yasg.openapi.FORMAT_INT32` is called :py:attr:`~drf_spectacular.types.OpenApiTypes.INT32` - :py:data:`~drf_yasg.openapi.TYPE_INTEGER` with :py:data:`~drf_yasg.openapi.FORMAT_INT64` is called :py:attr:`~drf_spectacular.types.OpenApiTypes.INT64` - :py:data:`~drf_yasg.openapi.TYPE_NUMBER` is called :py:attr:`~drf_spectacular.types.OpenApiTypes.NUMBER` - :py:data:`~drf_yasg.openapi.TYPE_NUMBER` with :py:data:`~drf_yasg.openapi.FORMAT_FLOAT` is called :py:attr:`~drf_spectacular.types.OpenApiTypes.FLOAT`, but you can use :py:class:`float`. - :py:data:`~drf_yasg.openapi.TYPE_NUMBER` with :py:data:`~drf_yasg.openapi.FORMAT_DOUBLE` is called :py:attr:`~drf_spectacular.types.OpenApiTypes.DOUBLE` (or :py:attr:`~drf_spectacular.types.OpenApiTypes.DECIMAL`, but you can use :py:class:`~decimal.Decimal`) - :py:data:`~drf_yasg.openapi.TYPE_OBJECT` is called :py:attr:`~drf_spectacular.types.OpenApiTypes.OBJECT`, but you can use :py:class:`dict`. - :py:data:`~drf_yasg.openapi.TYPE_STRING` is called :py:attr:`~drf_spectacular.types.OpenApiTypes.STR`, but you can use :py:class:`str`. - :py:data:`~drf_yasg.openapi.TYPE_STRING` with :py:data:`~drf_yasg.openapi.FORMAT_BASE64` is called :py:attr:`~drf_spectacular.types.OpenApiTypes.BYTE` (which is base64 encoded). - :py:data:`~drf_yasg.openapi.TYPE_STRING` with :py:data:`~drf_yasg.openapi.FORMAT_BINARY` is called :py:attr:`~drf_spectacular.types.OpenApiTypes.BINARY`, but you can use :py:class:`bytes`. - :py:data:`~drf_yasg.openapi.TYPE_STRING` with :py:data:`~drf_yasg.openapi.FORMAT_DATETIME` is called :py:attr:`~drf_spectacular.types.OpenApiTypes.DATETIME`, but you can use :py:class:`datetime.datetime`. - :py:data:`~drf_yasg.openapi.TYPE_STRING` with :py:data:`~drf_yasg.openapi.FORMAT_DATE` is called :py:attr:`~drf_spectacular.types.OpenApiTypes.DATE`, but you can use :py:class:`datetime.date`. - :py:data:`~drf_yasg.openapi.TYPE_STRING` with :py:data:`~drf_yasg.openapi.FORMAT_EMAIL` is called :py:attr:`~drf_spectacular.types.OpenApiTypes.EMAIL` - :py:data:`~drf_yasg.openapi.TYPE_STRING` with :py:data:`~drf_yasg.openapi.FORMAT_IPV4` is called :py:attr:`~drf_spectacular.types.OpenApiTypes.IP4`, but you can use :py:class:`ipaddress.IPv4Address`. - :py:data:`~drf_yasg.openapi.TYPE_STRING` with :py:data:`~drf_yasg.openapi.FORMAT_IPV6` is called :py:attr:`~drf_spectacular.types.OpenApiTypes.IP6`, but you can use :py:class:`ipaddress.IPv6Address`. - :py:data:`~drf_yasg.openapi.TYPE_STRING` with :py:data:`~drf_yasg.openapi.FORMAT_PASSWORD` is called :py:attr:`~drf_spectacular.types.OpenApiTypes.PASSWORD` - :py:data:`~drf_yasg.openapi.TYPE_STRING` with :py:data:`~drf_yasg.openapi.FORMAT_URI` is called :py:attr:`~drf_spectacular.types.OpenApiTypes.URI` - :py:data:`~drf_yasg.openapi.TYPE_STRING` with :py:data:`~drf_yasg.openapi.FORMAT_UUID` is called :py:attr:`~drf_spectacular.types.OpenApiTypes.UUID`, but you can use :py:class:`uuid.UUID`. - :py:data:`~drf_yasg.openapi.TYPE_STRING` with :py:data:`~drf_yasg.openapi.FORMAT_SLUG` has no direct equivalent. Use :py:attr:`~drf_spectacular.types.OpenApiTypes.STR` or :py:class:`str` instead. - :py:data:`~drf_yasg.openapi.TYPE_ARRAY` is handled by providing :py:attr:`~drf_spectacular.utils.OpenApiParameter` with ``many=True`` as a parameter. There is no need to set the ``items`` property on the parameter - the presence of ``many=True`` turns the parameter into an array parameter. - The following additional types are also available: - :py:attr:`~drf_spectacular.types.OpenApiTypes.ANY` for which you can use :py:class:`typing.Any`. - :py:attr:`~drf_spectacular.types.OpenApiTypes.DURATION` for which you can use :py:class:`datetime.timedelta`. - :py:attr:`~drf_spectacular.types.OpenApiTypes.HOSTNAME` - :py:attr:`~drf_spectacular.types.OpenApiTypes.IDN_EMAIL` - :py:attr:`~drf_spectacular.types.OpenApiTypes.IDN_HOSTNAME` - :py:attr:`~drf_spectacular.types.OpenApiTypes.IRI_REF` - :py:attr:`~drf_spectacular.types.OpenApiTypes.IRI` - :py:attr:`~drf_spectacular.types.OpenApiTypes.JSON_PTR_REL` - :py:attr:`~drf_spectacular.types.OpenApiTypes.JSON_PTR` - :py:attr:`~drf_spectacular.types.OpenApiTypes.NONE` for which you can use :py:data:`None`. - :py:attr:`~drf_spectacular.types.OpenApiTypes.REGEX` - :py:attr:`~drf_spectacular.types.OpenApiTypes.TIME` for which you can use :py:class:`datetime.time`. - :py:attr:`~drf_spectacular.types.OpenApiTypes.URI_REF` - :py:attr:`~drf_spectacular.types.OpenApiTypes.URI_TPL` Parameter Location ------------------ ``drf_yasg.openapi.IN_*`` constants are roughly equivalent to constants defined on the :py:class:`~drf_spectacular.utils.OpenApiParameter` class: - :py:data:`~drf_yasg.openapi.IN_PATH` is called :py:attr:`~drf_spectacular.utils.OpenApiParameter.PATH` - :py:data:`~drf_yasg.openapi.IN_QUERY` is called :py:attr:`~drf_spectacular.utils.OpenApiParameter.QUERY` - :py:data:`~drf_yasg.openapi.IN_HEADER` is called :py:attr:`~drf_spectacular.utils.OpenApiParameter.HEADER` - :py:data:`~drf_yasg.openapi.IN_BODY` and :py:data:`~drf_yasg.openapi.IN_FORM` have no direct equivalent. Instead you can use ``@extend_schema(request={"": ...})``. - :py:attr:`~drf_spectacular.utils.OpenApiParameter.COOKIE` is also available. Docstring Parsing ----------------- *drf-yasg* has some special handling for docstrings that is not supported by *drf-spectacular*. It attempts to split the first line from the rest of the docstring to use as the operation summary, and the remainder is used as the operation description. *drf-spectacular* uses the entire docstring as the description. Use the ``summary`` and ``description`` arguments of :py:func:`@extend_schema ` instead. Optionally, the docstring can still be used to populate the operation description. .. code-block:: python # Supported by drf-yasg: class UserViewSet(ViewSet): def list(self, request): """ List all the users. Return a list of all usernames in the system. """ ... # Updated for drf-spectacular using decorator for description: class UserViewSet(ViewSet): @extend_schema( summary="List all the users.", description="Return a list of all usernames in the system.", ) def list(self, request): ... # Updated for drf-spectacular using docstring for description: class UserViewSet(ViewSet): @extend_schema(summary="List all the users.") def list(self, request): """Return a list of all usernames in the system.""" ... In addition, *drf-yasg* also supports `named sections`__, but these are not supported by *drf-spectacular*. Again, use the ``summary`` and ``description`` arguments of :py:func:`@extend_schema ` instead: __ https://www.django-rest-framework.org/coreapi/schemas/#schemas-as-documentation .. code-block:: python # Supported by drf-yasg: class UserViewSet(ViewSet): """ list: List all the users. Return a list of all usernames in the system. retrieve: Retrieve user Get details of a specific user """ ... # Updated for drf-spectacular using decorator for description: @extend_schema_view( list=extend_schema( summary="List all the users.", description="Return a list of all usernames in the system.", ), retrieve=extend_schema( summary="Retrieve user", description="Get details of a specific user", ), ) class UserViewSet(ViewSet): ... Authentication -------------- In *drf-yasg* it was necessary to :doc:`manually describe authentication schemes `. In *drf-spectacular* there is support for auto-generating the security definitions for a number of authentication classes built in to DRF as well as other popular third-party packages. :py:class:`~drf_spectacular.extensions.OpenApiAuthenticationExtension` is available to help tie in custom authentication clasees -- see the :ref:`customization guide `. Compatibility ------------- For compatibility, the following features of *drf-yasg* have been implemented: - ``ref_name`` on ``Serializer`` ``Meta`` classes is supported (excluding inlining with ``ref_name=None``) - See :ref:`drf-yasg's documentation ` for further details. - The equivalent in ``drf-spectacular`` is ``@extend_schema_serializer(component_name="...")`` - ``swagger_fake_view`` is available as attribute on views to signal schema generation drf-spectacular-0.27.0/docs/extensions.py000066400000000000000000000013661453572150400203740ustar00rootroot00000000000000from docutils.nodes import Text from sphinx.util import logging logger = logging.getLogger(__name__) def setup(app): app.add_role("default-role-error", default_role_error) return {"parallel_read_safe": True} # Inspired by equivalent code in Django's Sphinx extension. # https://github.com/django/django/blob/6b53114d/docs/_ext/djangodocs.py#L393 def default_role_error( name, rawtext, text, lineno, inliner, options=None, content=None ): logger.warning( ( f"Default role used (`single backticks`): {rawtext}. Did you mean to use " "two backticks for ``code``, or miss an underscore for a `link`_ ?" ), location=(inliner.document.current_source, lineno), ) return [Text(text)], [] drf-spectacular-0.27.0/docs/faq.rst000066400000000000000000000500751453572150400171250ustar00rootroot00000000000000FAQ === I use library/app *XXX* and the generated schema is wrong or broken ------------------------------------------------------------------- Sometimes DRF libraries do not cooperate well with the introspection mechanics. Check the :ref:`blueprints` for already available fixes. If there aren't any, learn how to do easy :ref:`customization`. Feel free to contribute back missing fixes. If you think this is a bug in *drf-spectacular*, open an `issue `_. My Swagger UI and/or Redoc page is blank ---------------------------------------- Chances are high that you are using `django-csp `_. Take a look inside your browser console and confirm that you have ``Content Security Policy`` errors. By default, ``django-csp`` usually breaks our UIs for 2 reasons: external assets and inline scripts. Using the `sidecar `_ will mitigate the remote asset loading violation by serving the asset from your ``self``. Alternatively, you can also adapt ``CSP_DEFAULT_SRC`` to allow for those CDN assets instead. Solution for Swagger UI: ^^^^^^^^^^^^^^^^^^^^^^^^ .. code-block:: python # Option: SIDECAR SPECTACULAR_SETTINGS = { ... 'SWAGGER_UI_DIST': 'SIDECAR', 'SWAGGER_UI_FAVICON_HREF': 'SIDECAR', } CSP_DEFAULT_SRC = ("'self'", "'unsafe-inline'") CSP_IMG_SRC = ("'self'", "data:") # Option: CDN CSP_DEFAULT_SRC = ("'self'", "'unsafe-inline'", "cdn.jsdelivr.net") CSP_IMG_SRC = ("'self'", "data:", "cdn.jsdelivr.net") .. note:: Depending on how paranoid you are, you may avoid having to use ``unsafe-inline`` by using ``SpectacularSwaggerSplitView`` instead, which does a separate request for the script. Note however that some URL rewriting deployments will break it. Use this option only if you really need to. Solution for Redoc: ^^^^^^^^^^^^^^^^^^^ .. code-block:: python # Option: SIDECAR SPECTACULAR_SETTINGS = { ... 'REDOC_DIST': 'SIDECAR', } # Option: CDN CSP_DEFAULT_SRC = ("'self'", "cdn.jsdelivr.net") # required for both CDN and SIDECAR CSP_WORKER_SRC = ("'self'", "blob:") CSP_IMG_SRC = ("'self'", "data:", "cdn.redoc.ly") CSP_STYLE_SRC = ("'self'", "'unsafe-inline'", "fonts.googleapis.com") CSP_FONT_SRC = ("'self'", "fonts.gstatic.com") I cannot use :py:func:`@extend_schema ` on library code -------------------------------------------------------------------------------------------- You can easily adapt introspection for libraries/apps with the *Extension* mechanism. *Extensions* provide an easy way to attach schema information to code that you cannot modify otherwise. Have a look at :ref:`customization` on how to use *Extensions* I get an empty schema or endpoints are missing ---------------------------------------------- This is usually due to versioning (or more rarely due to permissions). In case you use versioning on all endpoints, that might be the intended output. By default the schema will only contain unversioned endpoints. Explicitly specify what version you want to generate. .. code-block:: bash ./manage.py spectacular --api-version 'YOUR_VERSION' This will contain unversioned endpoints together with the endpoints for the specified version. For the schema views you can either set a versioning class (implicit versioning via the request) or explicitly override the version with ``SpectacularAPIView.as_view(api_version='YOUR_VERSION')``. I expected a different schema ----------------------------- Sometimes views declare one thing (via ``serializer_class`` and ``queryset``) and do a entirely different thing. Usually this is attributed to making a library code flexible under varying situations. In those cases it is best to override what the introspection decided and state explicitly what is to be expected. Work through the steps in :ref:`customization` to adapt your schema. I get duplicated operations with a ``{format}``-suffix ------------------------------------------------------ Your app likely uses DRF's ``format_suffix_patterns``. If those operations are undesirable in your schema, you can simply exclude them with an already provided :ref:`preprocessing hook `. I get a lot of warnings ----------------------- The warnings are emitted to inform you of discovered schema issues. Some usage patterns like ``@api_view`` or ``APIView`` provide very little discoverable information on your API. In those cases you can easily augment those endpoints and serializers with additional information. Look at :ref:`customization` options to fill those gaps and make the warnings disappear. I get warnings regarding my ``Enum`` or my ``Enum`` names have a weird suffix ------------------------------------------------------------------------------ This is because the ``Enum`` postprocessing hook is activated by default, which attempts to find a name for a set of enum choices. The naming mechanism uses the name of the field and possibly the name of the component, followed by a suffix if necessary if there are clashes (if there are two enum fields with the same name but different set of choices). This will handle all encountered issues automatically, but also notify you of potential problems, of two kinds: * multiple names being produced for the same set of values, due to different field names (e.g. if you have a single currency enum used by distinct fields named like ``payment_currency`` and ``preferred_currency``, the naming mechanism will by default treat this as two different enums but emit a warning). * clashes that result in a suffix being needed, as above. You can resolve (or silence) enum issues by adding an entry to the ``ENUM_NAME_OVERRIDES`` setting. Values can take the form of choices (list of tuples), value lists (list of strings), or import strings. Django's ``models.Choices`` and Python's ``Enum`` classes are supported as well. The key is a string that you choose as a name to give to this set of values. For example: .. code-block:: python SPECTACULAR_SETTINGS = { ... 'ENUM_NAME_OVERRIDES': { # variable containing list of tuples, e.g. [('US', 'US'), ('RU', 'RU'),] 'LanguageEnum': language_choices, # dedicated Enum or models.Choices class 'CountryEnum': 'import_path.enums.CountryEnum', # choices is an attribute of class CurrencyContainer containing a list of tuples 'CurrencyEnum': 'import_path.CurrencyContainer.choices', } } If you have multiple semantically distinct enums that happen to have the same set of values, and you want different names for them, this mechanism won't work. My endpoints use different serializers depending on the situation ----------------------------------------------------------------- Welcome to the real world! Use :py:func:`@extend_schema ` in combination with :py:class:`PolymorphicProxySerializer ` like so: .. code-block:: python class PersonView(viewsets.GenericViewSet): @extend_schema(responses={ 200: PolymorphicProxySerializer( component_name='Person', # on 200 either a legal or a natural person is returned serializers=[LegalPersonSerializer, NaturalPersonSerializer], resource_type_field_name='type', ), 500: YourOptionalErrorSerializer, }) def retrieve(self, request, *args, **kwargs) pass My authentication method is not supported ----------------------------------------- You can easily specify a custom authentication with :py:class:`OpenApiAuthenticationExtension `. Have a look at :ref:`customization` on how to use *Extensions* How can I i18n/internationalize my schema and UI? ------------------------------------------------- You can use the Django internationalization as you would normally do. The workflow is as one would expect: ``USE_I18N=True``, settings the languages, ``makemessages``, and ``compilemessages``. The CLI tool accepts a language parameter (``./manage.py spectacular --lang="de-de"``) for offline generation. The schema view as well as the UI views accept a ``lang`` query parameter for explicitly requesting a language (``example.com/api/schema?lang=de``). If i18n is enabled and there is no query parameter provided, the ``ACCEPT_LANGUAGE`` header is used. Otherwise the translation falls back to the default language. .. code-block:: python from django.utils.translation import gettext_lazy as _ class PersonView(viewsets.GenericViewSet): __doc__ = _(""" More lengthy explanation of the view """) @extend_schema(summary=_('Main endpoint for creating person')) def retrieve(self, request, *args, **kwargs): pass FileField (ImageField) is not handled properly in the schema ------------------------------------------------------------ In contrast to most other fields, ``FileField`` behaves differently for requests and responses. This duality is impossible to represent in a single component schema. For these cases, there is an option to split components into request and response parts by setting ``COMPONENT_SPLIT_REQUEST = True``. Note that this influences the whole schema, not just components with ``FileFields``. Also consider explicitly setting ``parser_classes = [parsers.MultiPartParser]`` (or any file compatible parser) on your ``View`` or write a custom ``get_parser_classes``. These fields do not work with the default ``JsonParser`` and that fact should be represented in the schema. I'm using ``@action(detail=False)`` but the response schema is not a list ------------------------------------------------------------------------- ``detail=True/False`` only specifies whether the action should be routed at ``x/{id}/action`` or ``x/action``. The ``detail`` parameter in itself makes no statement about the action's response. Also note that the default for underspecified endpoints is a non-list response. To signal a listed response, you can use ``@extend_schema(responses=XSerializer(many=True))``. Using ``@extend_schema`` on ``APIView`` has no effect ----------------------------------------------------- ``@extend_schema`` needs to be applied to the entrypoint method of the view. For views derived from ``Viewset``, these are methods like ``retrieve``, ``list``, ``create``. For ``APIView`` based views, these are ``get``, ``post``, ``create``. This confusion commonly occurs while using convenience classes like ``ListAPIView``. ``ListAPIView`` does in fact have a ``list`` method (via mixin), but the actual entrypoint is still the ``get`` method, and the ``list`` call is proxied through the entrypoint. Where should I put my extensions? / my extensions are not detected ------------------------------------------------------------------ The extensions register themselves automatically. Just be sure that the Python interpreter sees them at least once. It is good practice to collect your extensions in ``YOUR_MAIN_APP_NAME/schema.py`` and to import that file in your ``YOUR_MAIN_APP_NAME/apps.py``. Performing the import in the ``ready()`` method is the most robust approach. It will make sure your environment (e.g. settings) is properly set up prior to loading. .. code-block:: python # your_main_app_name/apps.py class YourMainAppNameConfig(AppConfig): default_auto_field = "django.db.models.BigAutoField" name = "your_main_app_name" def ready(self): import your_main_app_name.schema # noqa: E402 While there are certainly other ways of loading your extensions, this is a battle-proven and robust way to do it. Generally in Django/DRF, importing stuff in the wrong order often results in weird errors or circular import issues, which this approach tries to carefully circumvent. My ``@action`` is erroneously paginated or has filter parameters that I do not want ----------------------------------------------------------------------------------- This usually happens when ``@extend_schema(responses=XSerializer(many=True))`` is used. Actions inherit filter and pagination classes from their ``ViewSet``. If the response is then marked as a list, the ``pagination_class`` kicks in. Since actions are handled manually by the user, this behavior is usually not immediately obvious. To make your intentions clear to *drf-spectacular*, you need to clear the offending classes in the action decorator, e.g. setting ``pagination_class=None``. Users of *django-filter* might also see unwanted query parameters. Since the same mechanics apply here too, you can remove those parameters by resetting the filter backends with ``@action(...,filter_backends=[])``. .. code-block:: python class XViewset(viewsets.ModelViewSet): queryset = SimpleModel.objects.all() pagination_class = pagination.LimitOffsetPagination @extend_schema(responses=SimpleSerializer(many=True)) @action(methods=['GET'], detail=False, pagination_class=None) def custom_action(self): pass How do I wrap my responses? / My endpoints are wrapped in a generic envelope ---------------------------------------------------------------------------- This non-native behavior can be conveniently modeled with a simple helper function. You simply need to wrap the actual serializer with your envelope serializer and provide it to ``@extend_schema``. Here is an example on how to build an ``enveloper`` helper function. In this example, the actual serializer is put into the ``data`` field, while ``status`` is some arbitrary envelope field. Adapt to your specific requirements. .. code-block:: python def enveloper(serializer_class, many): component_name = 'Enveloped{}{}'.format( serializer_class.__name__.replace("Serializer", ""), "List" if many else "", ) @extend_schema_serializer(many=False, component_name=component_name) class EnvelopeSerializer(serializers.Serializer): status = serializers.BooleanField() # some arbitrary envelope field data = serializer_class(many=many) # the enveloping part return EnvelopeSerializer class XViewset(GenericViewSet): @extend_schema(responses=enveloper(XSerializer, True)) def list(self, request, *args, **kwargs): ... How can I have multiple ``SpectacularAPIView`` with differing settings ---------------------------------------------------------------------- First, define your base settings in ``settings.py`` with ``SPECTACULAR_SETTINGS``. Then, if you need another schema with different settings, you can provide scoped overrides by providing a ``custom_settings`` argument. ``custom_settings`` expects a ``dict`` and only allows keys that represent valid setting names. Beware that using this mechanic is not thread-safe at the moment. Also note that overriding ``SERVE_*`` or ``DEFAULT_GENERATOR_CLASS`` in ``custom_settings`` is not allowed. ``SpectacularAPIView`` has dedicated arguments for overriding these settings. .. code-block:: python urlpatterns = [ path('api/schema/', SpectacularAPIView.as_view(), path('api/schema-custom/', SpectacularAPIView.as_view( custom_settings={ 'TITLE': 'your custom title', 'SCHEMA_PATH_PREFIX': 'your custom regex', ... } ), name='schema-custom'), ] How to correctly annotate function-based views that use ``@api_view()`` ----------------------------------------------------------------------- DRF provides a convenient way to write function-based views. ``@api_view()`` in essence wraps a regular function and implicitly converts it to a ``APIView`` class. For single-method cases, simply use :py:func:`@extend_schema ` just as you would with a normal view method. .. code-block:: python @extend_schema(request=XSerializer, responses=XSerializer) @api_view(['POST']) def view_func(request, format=None): return ... For functions that provide multiple methods, its advisable to use :py:func:`@extend_schema_view ` and break down each case separately. .. code-block:: python @extend_schema_view( get=extend_schema(description='get desc', responses=XSerializer), post=extend_schema(description='post desc', request=None, responses=OpenApiTypes.UUID), ) @api_view(['GET', 'POST']) def view_func(request, format=None): return ... My ``get_queryset()`` depends on some attributes not available at schema generation time ---------------------------------------------------------------------------------------- In certain situations we need to call ``get_serializer``, which in turn calls ``get_queryset``. If your ``get_queryset`` (or ``get_serializer_class``) depends on attributes not available at schema generation time (e.g. ``request.user.is_authenticated``), you need to provide a fallback that allows us to call that method. While the schema is generated, you can check for the view attribute ``swagger_fake_view`` and simply return an empty queryset of the correct model. .. code-block:: python class XViewset(viewsets.ModelViewset): ... def get_queryset(self): if getattr(self, 'swagger_fake_view', False): # drf-yasg comp return YourModel.objects.none() # your usual logic How to serve in-memory generated files or files in general outside ``FileField`` -------------------------------------------------------------------------------- DRF provides a convenient ``FileField`` for storing files persistently within a ``Model``. ``drf-spectacular`` handles these correctly by default. But to serve binary files that are *generated in-memory*, follow the following recipe. This example uses the method `recommended by Django `_ for treating a ``Response`` as a file and sets up an appropriate ``Renderer`` that will handle the client ``Accept`` header for this response content type. ``responses=bytes`` expresses that the response is a binary blob without further details on its structure. .. code-block:: python from django.http import HttpResponse from rest_framework.renderers import BaseRenderer class BinaryRenderer(BaseRenderer): media_type = "application/octet-stream" format = "bin" class FileViewSet(RetrieveModelMixin, GenericViewSet): ... renderer_classes = [BinaryRenderer] @extend_schema(responses=bytes) def retrieve(self, request, *args, **kwargs): export_data = b"..." return HttpResponse( export_data, content_type=BinaryRenderer.media_type, headers={ "Content-Disposition": "attachment; filename=out.bin", }, ) My ``ViewSet`` ``list`` does not return a list, but a single object. -------------------------------------------------------------------- Generally, it is bad practice to use a ``ViewSet.list`` method to return single object, because DRF specifically does a list conversion in the background for this method and only this method. Using ``ApiView`` or ``GenericAPIView`` for this use-case would be cleaner. However, if you insist on this behavior, you can circumvent the list detection by creating a one-off copy of your serializer and marking it as forced non-list. It is important to create a **copy** as :py:func:`@extend_schema_serializer ` modifies the given serializer. .. code-block:: python from drf_spectacular.helpers import forced_singular_serializer class YourViewSet(viewsets.ModelViewSet): serializer_class = SimpleSerializer queryset = SimpleModel.objects.none() @extend_schema(responses=forced_singular_serializer(SimpleSerializer)) def list(self): pass drf-spectacular-0.27.0/docs/index.rst000066400000000000000000000022311453572150400174540ustar00rootroot00000000000000drf-spectacular =============== *Sane and flexible OpenAPI 3 schema generation for Django REST framework.* Documentation is an integral part of API development and OpenAPI 3 is finally here to make that process easier. By using `drf-spectacular `_ with `Django REST Framework (DRF) `_, your schema and therefore your documentation & client will always stay close to your API. drf-spectacular works well out of the box, but also provides you with several easy ways to customize the generated OpenAPI 3 schema. It is explicitly designed to work well for documentation (`SwaggerUI `_, `ReDoc `_) and automatic `client generation `_. Table of Contents ----------------- .. toctree:: :maxdepth: 3 readme.rst settings.rst customization.rst client_generation.rst faq.rst blueprints.rst drf_yasg.rst contributing.rst changelog.rst license.rst drf_spectacular.rst Indices and Tables ------------------ * :ref:`genindex` * :ref:`modindex` * :ref:`search` drf-spectacular-0.27.0/docs/license.rst000066400000000000000000000000601453572150400177650ustar00rootroot00000000000000License ======= .. literalinclude:: ../LICENSE drf-spectacular-0.27.0/docs/make.bat000066400000000000000000000014331453572150400172230ustar00rootroot00000000000000@ECHO OFF pushd %~dp0 REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=sphinx-build ) set SOURCEDIR=. set BUILDDIR=_build if "%1" == "" goto help %SPHINXBUILD% >NUL 2>NUL if errorlevel 9009 ( echo. echo.The 'sphinx-build' command was not found. Make sure you have Sphinx echo.installed, then set the SPHINXBUILD environment variable to point echo.to the full path of the 'sphinx-build' executable. Alternatively you echo.may add the Sphinx directory to PATH. echo. echo.If you don't have Sphinx installed, grab it from echo.http://sphinx-doc.org/ exit /b 1 ) %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% goto end :help %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% :end popd drf-spectacular-0.27.0/docs/readme.rst000066400000000000000000000000501453572150400175770ustar00rootroot00000000000000.. _readme: .. include:: ../README.rst drf-spectacular-0.27.0/docs/settings.rst000066400000000000000000000041331453572150400202100ustar00rootroot00000000000000.. _settings: Settings ======== Settings are configurable in ``settings.py`` in the scope ``SPECTACULAR_SETTINGS``. You can override any setting, otherwise the defaults below are used. .. literalinclude:: ../drf_spectacular/settings.py :start-after: APISettings :end-before: IMPORT_STRINGS Django Rest Framework settings ------------------------------ Some of the `Django Rest Framework settings `_ also impact the schema generation. Refer to the documentation for the version that you are using. Settings which effect the processing of requests and data types of responses will usually be effective. There is explicit use of these settings: - ``DEFAULT_SCHEMA_CLASS`` - ``COERCE_DECIMAL_TO_STRING`` - ``UPLOADED_FILES_USE_URL`` - ``URL_FORMAT_OVERRIDE`` - ``FORMAT_SUFFIX_KWARG`` The following settings are ignored: - ``SCHEMA_COERCE_METHOD_NAMES`` The following are known to be effective: - ``SCHEMA_COERCE_PATH_PK`` Example: SwaggerUI settings --------------------------- We currently support passing through all basic SwaggerUI `configuration parameters `_. For more customization options (e.g. CSS, JS functions), you can extend or override the `SwaggerUI template `_ in your project files. .. code:: python SPECTACULAR_SETTINGS = { ... # available SwaggerUI configuration parameters # https://swagger.io/docs/open-source-tools/swagger-ui/usage/configuration/ "SWAGGER_UI_SETTINGS": { "deepLinking": True, "persistAuthorization": True, "displayOperationId": True, ... }, # available SwaggerUI versions: https://github.com/swagger-api/swagger-ui/releases "SWAGGER_UI_DIST": "//unpkg.com/swagger-ui-dist@3.35.1", # default "SWAGGER_UI_FAVICON_HREF": settings.STATIC_URL + "your_company_favicon.png", # default is swagger favicon ... } drf-spectacular-0.27.0/drf_spectacular/000077500000000000000000000000001453572150400200265ustar00rootroot00000000000000drf-spectacular-0.27.0/drf_spectacular/__init__.py000066400000000000000000000002051453572150400221340ustar00rootroot00000000000000import django __version__ = '0.27.0' if django.VERSION < (3, 2): default_app_config = 'drf_spectacular.apps.SpectacularConfig' drf-spectacular-0.27.0/drf_spectacular/apps.py000066400000000000000000000003241453572150400213420ustar00rootroot00000000000000from django.apps import AppConfig class SpectacularConfig(AppConfig): name = 'drf_spectacular' verbose_name = "drf-spectacular" def ready(self): import drf_spectacular.checks # noqa: F401 drf-spectacular-0.27.0/drf_spectacular/authentication.py000066400000000000000000000023221453572150400234160ustar00rootroot00000000000000from django.conf import settings from drf_spectacular.extensions import OpenApiAuthenticationExtension from drf_spectacular.plumbing import build_bearer_security_scheme_object class SessionScheme(OpenApiAuthenticationExtension): target_class = 'rest_framework.authentication.SessionAuthentication' name = 'cookieAuth' priority = -1 def get_security_definition(self, auto_schema): return { 'type': 'apiKey', 'in': 'cookie', 'name': settings.SESSION_COOKIE_NAME, } class BasicScheme(OpenApiAuthenticationExtension): target_class = 'rest_framework.authentication.BasicAuthentication' name = 'basicAuth' priority = -1 def get_security_definition(self, auto_schema): return { 'type': 'http', 'scheme': 'basic', } class TokenScheme(OpenApiAuthenticationExtension): target_class = 'rest_framework.authentication.TokenAuthentication' name = 'tokenAuth' match_subclasses = True priority = -1 def get_security_definition(self, auto_schema): return build_bearer_security_scheme_object( header_name='Authorization', token_prefix=self.target.keyword, ) drf-spectacular-0.27.0/drf_spectacular/checks.py000066400000000000000000000017471453572150400216510ustar00rootroot00000000000000from django.core.checks import Error, Warning, register @register(deploy=True) def schema_check(app_configs, **kwargs): """ Perform dummy generation and emit warnings/errors as part of Django's check framework """ from drf_spectacular.drainage import GENERATOR_STATS from drf_spectacular.settings import spectacular_settings if not spectacular_settings.ENABLE_DJANGO_DEPLOY_CHECK: return [] errors = [] try: with GENERATOR_STATS.silence(): spectacular_settings.DEFAULT_GENERATOR_CLASS().get_schema(request=None, public=True) except Exception as exc: errors.append( Error(f'Schema generation threw exception "{exc}"', id='drf_spectacular.E001') ) if GENERATOR_STATS: for w in GENERATOR_STATS._warn_cache: errors.append(Warning(w, id='drf_spectacular.W001')) for e in GENERATOR_STATS._error_cache: errors.append(Warning(e, id='drf_spectacular.W002')) return errors drf-spectacular-0.27.0/drf_spectacular/contrib/000077500000000000000000000000001453572150400214665ustar00rootroot00000000000000drf-spectacular-0.27.0/drf_spectacular/contrib/__init__.py000066400000000000000000000005131453572150400235760ustar00rootroot00000000000000__all__ = [ 'django_oauth_toolkit', 'djangorestframework_camel_case', 'rest_auth', 'rest_framework', 'rest_polymorphic', 'rest_framework_dataclasses', 'rest_framework_jwt', 'rest_framework_simplejwt', 'django_filters', 'rest_framework_recursive', 'rest_framework_gis', 'pydantic', ] drf-spectacular-0.27.0/drf_spectacular/contrib/django_filters.py000066400000000000000000000327311453572150400250400ustar00rootroot00000000000000from django.db import models from drf_spectacular.drainage import add_trace_message, get_override, has_override, warn from drf_spectacular.extensions import OpenApiFilterExtension from drf_spectacular.plumbing import ( build_array_type, build_basic_type, build_choice_description_list, build_parameter_type, follow_field_source, force_instance, get_manager, get_type_hints, get_view_model, is_basic_type, is_field, ) from drf_spectacular.settings import spectacular_settings from drf_spectacular.types import OpenApiTypes from drf_spectacular.utils import OpenApiParameter _NoHint = object() class DjangoFilterExtension(OpenApiFilterExtension): """ Extensions that specifically deals with ``django-filter`` fields. The introspection attempts to estimate the underlying model field types to generate filter types. However, there are under-specified filter fields for which heuristics need to be performed. This serves as an explicit list of all partially-handled filter fields: - ``AllValuesFilter``: skip choices to prevent DB query - ``AllValuesMultipleFilter``: skip choices to prevent DB query, multi handled though - ``ChoiceFilter``: enum handled, type under-specified - ``DateRangeFilter``: N/A - ``LookupChoiceFilter``: N/A - ``ModelChoiceFilter``: enum handled - ``ModelMultipleChoiceFilter``: enum, multi handled - ``MultipleChoiceFilter``: enum, multi handled - ``RangeFilter``: min/max handled, type under-specified - ``TypedChoiceFilter``: enum handled - ``TypedMultipleChoiceFilter``: enum, multi handled In case of warnings or incorrect filter types, you can manually override the underlying field type with a manual ``extend_schema_field`` decoration. Alternatively, if you have a filter method for your filter field, you can attach ``extend_schema_field`` to that filter method. .. code-block:: class SomeFilter(FilterSet): some_field = extend_schema_field(OpenApiTypes.NUMBER)( RangeFilter(field_name='some_manually_annotated_field_in_qs') ) """ target_class = 'django_filters.rest_framework.DjangoFilterBackend' match_subclasses = True def get_schema_operation_parameters(self, auto_schema, *args, **kwargs): model = get_view_model(auto_schema.view) if not model: return [] filterset_class = self.target.get_filterset_class(auto_schema.view, get_manager(model).none()) if not filterset_class: return [] result = [] with add_trace_message(filterset_class): for field_name, filter_field in filterset_class.base_filters.items(): result += self.resolve_filter_field( auto_schema, model, filterset_class, field_name, filter_field ) return result def resolve_filter_field(self, auto_schema, model, filterset_class, field_name, filter_field): from django_filters import filters unambiguous_mapping = { filters.CharFilter: OpenApiTypes.STR, filters.BooleanFilter: OpenApiTypes.BOOL, filters.DateFilter: OpenApiTypes.DATE, filters.DateTimeFilter: OpenApiTypes.DATETIME, filters.IsoDateTimeFilter: OpenApiTypes.DATETIME, filters.TimeFilter: OpenApiTypes.TIME, filters.UUIDFilter: OpenApiTypes.UUID, filters.DurationFilter: OpenApiTypes.DURATION, filters.OrderingFilter: OpenApiTypes.STR, filters.TimeRangeFilter: OpenApiTypes.TIME, filters.DateFromToRangeFilter: OpenApiTypes.DATE, filters.IsoDateTimeFromToRangeFilter: OpenApiTypes.DATETIME, filters.DateTimeFromToRangeFilter: OpenApiTypes.DATETIME, } filter_method = self._get_filter_method(filterset_class, filter_field) filter_method_hint = self._get_filter_method_hint(filter_method) filter_choices = self._get_explicit_filter_choices(filter_field) schema_from_override = False if has_override(filter_field, 'field') or has_override(filter_method, 'field'): schema_from_override = True annotation = ( get_override(filter_field, 'field') or get_override(filter_method, 'field') ) if is_basic_type(annotation): schema = build_basic_type(annotation) elif isinstance(annotation, dict): # allow injecting raw schema via @extend_schema_field decorator schema = annotation.copy() elif is_field(annotation): schema = auto_schema._map_serializer_field(force_instance(annotation), "request") else: warn( f"Unsupported annotation {annotation} on filter field {field_name}. defaulting to string." ) schema = build_basic_type(OpenApiTypes.STR) elif filter_method_hint is not _NoHint: if is_basic_type(filter_method_hint): schema = build_basic_type(filter_method_hint) else: schema = build_basic_type(OpenApiTypes.STR) elif isinstance(filter_field, tuple(unambiguous_mapping)): for cls in filter_field.__class__.__mro__: if cls in unambiguous_mapping: schema = build_basic_type(unambiguous_mapping[cls]) break elif isinstance(filter_field, (filters.NumberFilter, filters.NumericRangeFilter)): # NumberField is underspecified by itself. try to find the # type that makes the most sense or default to generic NUMBER model_field = self._get_model_field(filter_field, model) if isinstance(model_field, (models.IntegerField, models.AutoField)): schema = build_basic_type(OpenApiTypes.INT) elif isinstance(model_field, models.FloatField): schema = build_basic_type(OpenApiTypes.FLOAT) elif isinstance(model_field, models.DecimalField): schema = build_basic_type(OpenApiTypes.NUMBER) # TODO may be improved else: schema = build_basic_type(OpenApiTypes.NUMBER) elif isinstance(filter_field, (filters.ChoiceFilter, filters.MultipleChoiceFilter)): try: schema = self._get_schema_from_model_field(auto_schema, filter_field, model) except Exception: if filter_choices and is_basic_type(type(filter_choices[0])): # fallback to type guessing from first choice element schema = build_basic_type(type(filter_choices[0])) else: warn( f'Unable to guess choice types from values, filter method\'s type hint ' f'or find "{field_name}" in model. Defaulting to string.' ) schema = build_basic_type(OpenApiTypes.STR) else: # the last resort is to look up the type via the model or queryset field # and emit a warning if we were unsuccessful. try: schema = self._get_schema_from_model_field(auto_schema, filter_field, model) except Exception as exc: # pragma: no cover warn( f'Exception raised while trying resolve model field for django-filter ' f'field "{field_name}". Defaulting to string (Exception: {exc})' ) schema = build_basic_type(OpenApiTypes.STR) # primary keys are usually non-editable (readOnly=True) and map_model_field correctly # signals that attribute. however this does not apply in this context. schema.pop('readOnly', None) # enrich schema with additional info from filter_field enum = schema.pop('enum', None) # explicit filter choices may disable enum retrieved from model if not schema_from_override and filter_choices is not None: enum = filter_choices description = schema.pop('description', None) if not schema_from_override: description = self._get_field_description(filter_field, description) # parameter style variations based on filter base class if isinstance(filter_field, filters.BaseCSVFilter): schema = build_array_type(schema) field_names = [field_name] explode = False style = 'form' elif isinstance(filter_field, filters.MultipleChoiceFilter): schema = build_array_type(schema) field_names = [field_name] explode = True style = 'form' elif isinstance(filter_field, (filters.RangeFilter, filters.NumericRangeFilter)): try: suffixes = filter_field.field_class.widget.suffixes except AttributeError: suffixes = ['min', 'max'] field_names = [ f'{field_name}_{suffix}' if suffix else field_name for suffix in suffixes ] explode = None style = None else: field_names = [field_name] explode = None style = None return [ build_parameter_type( name=field_name, required=filter_field.extra['required'], location=OpenApiParameter.QUERY, description=description, schema=schema, enum=enum, explode=explode, style=style ) for field_name in field_names ] def _get_filter_method(self, filterset_class, filter_field): if callable(filter_field.method): return filter_field.method elif isinstance(filter_field.method, str): return getattr(filterset_class, filter_field.method) else: return None def _get_filter_method_hint(self, filter_method): try: return get_type_hints(filter_method)['value'] except: # noqa: E722 return _NoHint def _get_explicit_filter_choices(self, filter_field): if 'choices' not in filter_field.extra: return None elif callable(filter_field.extra['choices']): # choices function may utilize the DB, so refrain from actually calling it. return [] else: return [c for c, _ in filter_field.extra['choices']] def _get_model_field(self, filter_field, model): if not filter_field.field_name: return None path = filter_field.field_name.split('__') return follow_field_source(model, path, emit_warnings=False) def _get_schema_from_model_field(self, auto_schema, filter_field, model): # Has potential to throw exceptions. Needs to be wrapped in try/except! # # first search for the field in the model as this has the least amount of # potential side effects. Only after that fails, attempt to call # get_queryset() to check for potential query annotations. model_field = self._get_model_field(filter_field, model) # this is a cross feature between rest-framework-gis and django-filter. Regular # behavior needs to be sidestepped as the model information is lost down the line. # TODO for now this will be just a string to cover WKT, WKB, and urlencoded GeoJSON # build_geo_schema(model_field) would yield the correct result if self._is_gis(model_field): return build_basic_type(OpenApiTypes.STR) if not isinstance(model_field, models.Field): qs = auto_schema.view.get_queryset() model_field = qs.query.annotations[filter_field.field_name].field return auto_schema._map_model_field(model_field, direction=None) def _get_field_description(self, filter_field, description): # Try to improve description beyond auto-generated model description if filter_field.extra.get('help_text', None): description = filter_field.extra['help_text'] elif filter_field.label is not None: description = filter_field.label choices = filter_field.extra.get('choices') if choices and callable(choices): # remove auto-generated enum list, since choices come from a callable if '\n\n*' in (description or ''): description, _, _ = description.partition('\n\n*') elif (description or '').startswith('* `'): description = '' return description choice_description = '' if spectacular_settings.ENUM_GENERATE_CHOICE_DESCRIPTION and choices and not callable(choices): choice_description = build_choice_description_list(choices) if not choices: return description if not description: return choice_description if '\n\n*' in description: description, _, _ = description.partition('\n\n*') return description + '\n\n' + choice_description if description.startswith('* `'): return choice_description return description + '\n\n' + choice_description @classmethod def _is_gis(cls, field): if not getattr(cls, '_has_gis', True): return False try: from django.contrib.gis.db.models import GeometryField from rest_framework_gis.filters import GeometryFilter return isinstance(field, (GeometryField, GeometryFilter)) except: # noqa cls._has_gis = False return False drf-spectacular-0.27.0/drf_spectacular/contrib/django_oauth_toolkit.py000066400000000000000000000042411453572150400262500ustar00rootroot00000000000000from drf_spectacular.extensions import OpenApiAuthenticationExtension class DjangoOAuthToolkitScheme(OpenApiAuthenticationExtension): target_class = 'oauth2_provider.contrib.rest_framework.OAuth2Authentication' name = 'oauth2' def get_security_requirement(self, auto_schema): from oauth2_provider.contrib.rest_framework import ( IsAuthenticatedOrTokenHasScope, TokenHasScope, TokenMatchesOASRequirements, ) view = auto_schema.view request = view.request for permission in auto_schema.view.get_permissions(): if isinstance(permission, TokenMatchesOASRequirements): alt_scopes = permission.get_required_alternate_scopes(request, view) alt_scopes = alt_scopes.get(auto_schema.method, []) return [{self.name: group} for group in alt_scopes] if isinstance(permission, IsAuthenticatedOrTokenHasScope): return {self.name: TokenHasScope().get_scopes(request, view)} if isinstance(permission, TokenHasScope): # catch-all for subclasses of TokenHasScope like TokenHasReadWriteScope return {self.name: permission.get_scopes(request, view)} def get_security_definition(self, auto_schema): from oauth2_provider.scopes import get_scopes_backend from drf_spectacular.settings import spectacular_settings flows = {} for flow_type in spectacular_settings.OAUTH2_FLOWS: flows[flow_type] = {} if flow_type in ('implicit', 'authorizationCode'): flows[flow_type]['authorizationUrl'] = spectacular_settings.OAUTH2_AUTHORIZATION_URL if flow_type in ('password', 'clientCredentials', 'authorizationCode'): flows[flow_type]['tokenUrl'] = spectacular_settings.OAUTH2_TOKEN_URL if spectacular_settings.OAUTH2_REFRESH_URL: flows[flow_type]['refreshUrl'] = spectacular_settings.OAUTH2_REFRESH_URL scope_backend = get_scopes_backend() flows[flow_type]['scopes'] = scope_backend.get_all_scopes() return { 'type': 'oauth2', 'flows': flows } drf-spectacular-0.27.0/drf_spectacular/contrib/djangorestframework_camel_case.py000066400000000000000000000050711453572150400302550ustar00rootroot00000000000000import re from typing import Optional from django.utils.module_loading import import_string def camelize_serializer_fields(result, generator, request, public): from django.conf import settings from djangorestframework_camel_case.settings import api_settings from djangorestframework_camel_case.util import camelize_re, underscore_to_camel # prunes subtrees from camelization based on owning field name ignore_fields = api_settings.JSON_UNDERSCOREIZE.get("ignore_fields") or () # ignore certain field names while camelizing ignore_keys = api_settings.JSON_UNDERSCOREIZE.get("ignore_keys") or () def has_middleware_installed(): try: from djangorestframework_camel_case.middleware import CamelCaseMiddleWare except ImportError: return False for middleware in [import_string(m) for m in settings.MIDDLEWARE]: try: if issubclass(CamelCaseMiddleWare, middleware): return True except TypeError: pass def camelize_str(key: str) -> str: new_key = re.sub(camelize_re, underscore_to_camel, key) if "_" in key else key if key in ignore_keys or new_key in ignore_keys: return key return new_key def camelize_component(schema: dict, name: Optional[str] = None) -> dict: if name is not None and (name in ignore_fields or camelize_str(name) in ignore_fields): return schema elif schema.get('type') == 'object': if 'properties' in schema: schema['properties'] = { camelize_str(field_name): camelize_component(field_schema, field_name) for field_name, field_schema in schema['properties'].items() } if 'required' in schema: schema['required'] = [camelize_str(field) for field in schema['required']] elif schema.get('type') == 'array': camelize_component(schema['items']) return schema for (_, component_type), component in generator.registry._components.items(): if component_type == 'schemas': camelize_component(component.schema) if has_middleware_installed(): for url_schema in result["paths"].values(): for method_schema in url_schema.values(): for parameter in method_schema.get("parameters", []): parameter["name"] = camelize_str(parameter["name"]) # inplace modification of components also affect result dict, so regeneration is not necessary return result drf-spectacular-0.27.0/drf_spectacular/contrib/pydantic.py000066400000000000000000000041431453572150400236550ustar00rootroot00000000000000from drf_spectacular.drainage import set_override, warn from drf_spectacular.extensions import OpenApiSerializerExtension from drf_spectacular.plumbing import ResolvedComponent, build_basic_type from drf_spectacular.types import OpenApiTypes class PydanticExtension(OpenApiSerializerExtension): """ Allows using pydantic models on @extend_schema(request=..., response=...) to describe your API. We only have partial support for pydantic's version of dataclass, due to the way they are designed. The outermost class (the @extend_schema argument) has to be a subclass of pydantic.BaseModel. Inside this outermost BaseModel, any combination of dataclass and BaseModel can be used. """ target_class = "pydantic.BaseModel" match_subclasses = True def get_name(self, auto_schema, direction): # due to the fact that it is complicated to pull out every field member BaseModel class # of the entry model, we simply use the class name as string for object. This hack may # create false positive warnings, so turn it off. However, this may suppress correct # warnings involving the entry class. set_override(self.target, 'suppress_collision_warning', True) return self.target.__name__ def map_serializer(self, auto_schema, direction): # let pydantic generate a JSON schema try: from pydantic.json_schema import model_json_schema except ImportError: warn("Only pydantic >= 2 is supported. defaulting to generic object.") return build_basic_type(OpenApiTypes.OBJECT) schema = model_json_schema(self.target, ref_template="#/components/schemas/{model}") # pull out potential sub-schemas and put them into component section for sub_name, sub_schema in schema.pop("$defs", {}).items(): component = ResolvedComponent( name=sub_name, type=ResolvedComponent.SCHEMA, object=sub_name, schema=sub_schema, ) auto_schema.registry.register_on_missing(component) return schema drf-spectacular-0.27.0/drf_spectacular/contrib/rest_auth.py000066400000000000000000000130711453572150400240400ustar00rootroot00000000000000from django.conf import settings from django.utils.version import get_version_tuple from rest_framework import serializers from drf_spectacular.contrib.rest_framework_simplejwt import ( SimpleJWTScheme, TokenRefreshSerializerExtension, ) from drf_spectacular.drainage import warn from drf_spectacular.extensions import OpenApiSerializerExtension, OpenApiViewExtension from drf_spectacular.utils import extend_schema def get_dj_rest_auth_setting(class_name, setting_name): from dj_rest_auth.__version__ import __version__ if get_version_tuple(__version__) < (3, 0, 0): from dj_rest_auth import app_settings return getattr(app_settings, class_name) else: from dj_rest_auth.app_settings import api_settings return getattr(api_settings, setting_name) def get_token_serializer_class(): from dj_rest_auth.__version__ import __version__ if get_version_tuple(__version__) < (3, 0, 0): use_jwt = getattr(settings, 'REST_USE_JWT', False) else: from dj_rest_auth.app_settings import api_settings use_jwt = api_settings.USE_JWT if use_jwt: return get_dj_rest_auth_setting('JWTSerializer', 'JWT_SERIALIZER') else: return get_dj_rest_auth_setting('TokenSerializer', 'TOKEN_SERIALIZER') class RestAuthDetailSerializer(serializers.Serializer): detail = serializers.CharField(read_only=True, required=False) class RestAuthDefaultResponseView(OpenApiViewExtension): def view_replacement(self): class Fixed(self.target_class): @extend_schema(responses=RestAuthDetailSerializer) def post(self, request, *args, **kwargs): pass # pragma: no cover return Fixed class RestAuthLoginView(OpenApiViewExtension): target_class = 'dj_rest_auth.views.LoginView' def view_replacement(self): class Fixed(self.target_class): @extend_schema(responses=get_token_serializer_class()) def post(self, request, *args, **kwargs): pass # pragma: no cover return Fixed class RestAuthLogoutView(OpenApiViewExtension): target_class = 'dj_rest_auth.views.LogoutView' def view_replacement(self): if getattr(settings, 'ACCOUNT_LOGOUT_ON_GET', None): get_schema_params = {'responses': RestAuthDetailSerializer} else: get_schema_params = {'exclude': True} class Fixed(self.target_class): @extend_schema(**get_schema_params) def get(self, request, *args, **kwargs): pass # pragma: no cover @extend_schema(request=None, responses=RestAuthDetailSerializer) def post(self, request, *args, **kwargs): pass # pragma: no cover return Fixed class RestAuthPasswordChangeView(RestAuthDefaultResponseView): target_class = 'dj_rest_auth.views.PasswordChangeView' class RestAuthPasswordResetView(RestAuthDefaultResponseView): target_class = 'dj_rest_auth.views.PasswordResetView' class RestAuthPasswordResetConfirmView(RestAuthDefaultResponseView): target_class = 'dj_rest_auth.views.PasswordResetConfirmView' class RestAuthVerifyEmailView(RestAuthDefaultResponseView): target_class = 'dj_rest_auth.registration.views.VerifyEmailView' optional = True class RestAuthResendEmailVerificationView(RestAuthDefaultResponseView): target_class = 'dj_rest_auth.registration.views.ResendEmailVerificationView' optional = True class RestAuthJWTSerializer(OpenApiSerializerExtension): target_class = 'dj_rest_auth.serializers.JWTSerializer' def map_serializer(self, auto_schema, direction): class Fixed(self.target_class): user = get_dj_rest_auth_setting('UserDetailsSerializer', 'USER_DETAILS_SERIALIZER')() return auto_schema._map_serializer(Fixed, direction) class CookieTokenRefreshSerializerExtension(TokenRefreshSerializerExtension): target_class = 'dj_rest_auth.jwt_auth.CookieTokenRefreshSerializer' optional = True def get_name(self): return 'TokenRefresh' class RestAuthRegisterView(OpenApiViewExtension): target_class = 'dj_rest_auth.registration.views.RegisterView' optional = True def view_replacement(self): from allauth.account.app_settings import EMAIL_VERIFICATION, EmailVerificationMethod if EMAIL_VERIFICATION == EmailVerificationMethod.MANDATORY: response_serializer = RestAuthDetailSerializer else: response_serializer = get_token_serializer_class() class Fixed(self.target_class): @extend_schema(responses=response_serializer) def post(self, request, *args, **kwargs): pass # pragma: no cover return Fixed class SimpleJWTCookieScheme(SimpleJWTScheme): target_class = 'dj_rest_auth.jwt_auth.JWTCookieAuthentication' optional = True name = ['jwtHeaderAuth', 'jwtCookieAuth'] # type: ignore def get_security_requirement(self, auto_schema): return [{name: []} for name in self.name] def get_security_definition(self, auto_schema): cookie_name = get_dj_rest_auth_setting('JWT_AUTH_COOKIE', 'JWT_AUTH_COOKIE') if not cookie_name: cookie_name = 'jwt-auth' warn( f'"JWT_AUTH_COOKIE" setting required for JWTCookieAuthentication. ' f'defaulting to {cookie_name}' ) return [ super().get_security_definition(auto_schema), # JWT from header { 'type': 'apiKey', 'in': 'cookie', 'name': cookie_name, } ] drf-spectacular-0.27.0/drf_spectacular/contrib/rest_framework.py000066400000000000000000000022751453572150400251000ustar00rootroot00000000000000from drf_spectacular.extensions import OpenApiViewExtension class ObtainAuthTokenView(OpenApiViewExtension): target_class = 'rest_framework.authtoken.views.ObtainAuthToken' match_subclasses = True def view_replacement(self): """ Prior to DRF 3.12.0, usage of ObtainAuthToken resulted in AssertionError Incompatible AutoSchema used on View "ObtainAuthToken". Is DRF's DEFAULT_SCHEMA_CLASS ... This is because DRF had a bug which made it NOT honor DEFAULT_SCHEMA_CLASS and instead injected an unsolicited coreschema class for this view and this view only. This extension fixes the view before the wrong schema class is used. Bug in DRF that was fixed in later versions: https://github.com/encode/django-rest-framework/blob/4121b01b912668c049b26194a9a107c27a332429/rest_framework/authtoken/views.py#L16 """ from rest_framework import VERSION from drf_spectacular.openapi import AutoSchema # no intervention needed if VERSION >= '3.12': return self.target class FixedObtainAuthToken(self.target): schema = AutoSchema() return FixedObtainAuthToken drf-spectacular-0.27.0/drf_spectacular/contrib/rest_framework_dataclasses.py000066400000000000000000000032201453572150400274360ustar00rootroot00000000000000from drf_spectacular.drainage import get_override, has_override from drf_spectacular.extensions import OpenApiSerializerExtension from drf_spectacular.plumbing import get_doc from drf_spectacular.utils import Direction class OpenApiDataclassSerializerExtensions(OpenApiSerializerExtension): target_class = "rest_framework_dataclasses.serializers.DataclassSerializer" match_subclasses = True def get_name(self): """Use the dataclass name in the schema, instead of the serializer prefix (which can be just Dataclass).""" if has_override(self.target, 'component_name'): return get_override(self.target, 'component_name') if getattr(getattr(self.target, 'Meta', None), 'ref_name', None) is not None: return self.target.Meta.ref_name if has_override(self.target.dataclass_definition.dataclass_type, 'component_name'): return get_override(self.target.dataclass_definition.dataclass_type, 'component_name') return self.target.dataclass_definition.dataclass_type.__name__ def strip_library_doc(self, schema): """Strip the DataclassSerializer library documentation from the schema.""" from rest_framework_dataclasses.serializers import DataclassSerializer if 'description' in schema and schema['description'] == get_doc(DataclassSerializer): del schema['description'] return schema def map_serializer(self, auto_schema, direction: Direction): """"Generate the schema for a DataclassSerializer.""" schema = auto_schema._map_serializer(self.target, direction, bypass_extensions=True) return self.strip_library_doc(schema) drf-spectacular-0.27.0/drf_spectacular/contrib/rest_framework_gis.py000066400000000000000000000173421453572150400257430ustar00rootroot00000000000000from rest_framework.utils.model_meta import get_field_info from drf_spectacular.drainage import warn from drf_spectacular.extensions import OpenApiSerializerExtension, OpenApiSerializerFieldExtension from drf_spectacular.plumbing import ( ResolvedComponent, build_array_type, build_object_type, follow_field_source, get_doc, ) def build_point_schema(): return { "type": "array", "items": {"type": "number", "format": "float"}, "example": [12.9721, 77.5933], "minItems": 2, "maxItems": 3, } def build_linestring_schema(): return { "type": "array", "items": build_point_schema(), "example": [[22.4707, 70.0577], [12.9721, 77.5933]], "minItems": 2, } def build_polygon_schema(): return { "type": "array", "items": {**build_linestring_schema(), "minItems": 4}, "example": [ [ [0.0, 0.0], [0.0, 50.0], [50.0, 50.0], [50.0, 0.0], [0.0, 0.0], ], ] } def build_geo_container_schema(name, coords): return build_object_type( properties={ "type": {"type": "string", "enum": [name]}, "coordinates": coords, } ) def build_point_geo_schema(): return build_geo_container_schema("Point", build_point_schema()) def build_linestring_geo_schema(): return build_geo_container_schema("LineString", build_linestring_schema()) def build_polygon_geo_schema(): return build_geo_container_schema("Polygon", build_polygon_schema()) def build_geometry_geo_schema(): return { 'oneOf': [ build_point_geo_schema(), build_linestring_geo_schema(), build_polygon_geo_schema(), ] } def build_bbox_schema(): return { "type": "array", "items": {"type": "number"}, "minItems": 4, "maxItems": 4, "example": [12.9721, 77.5933, 12.9721, 77.5933], } def build_geo_schema(model_field): from django.contrib.gis.db import models if isinstance(model_field, models.PointField): return build_point_geo_schema() elif isinstance(model_field, models.LineStringField): return build_linestring_geo_schema() elif isinstance(model_field, models.PolygonField): return build_polygon_geo_schema() elif isinstance(model_field, models.MultiPointField): return build_geo_container_schema( "MultiPoint", build_array_type(build_point_schema()) ) elif isinstance(model_field, models.MultiLineStringField): return build_geo_container_schema( "MultiLineString", build_array_type(build_linestring_schema()) ) elif isinstance(model_field, models.MultiPolygonField): return build_geo_container_schema( "MultiPolygon", build_array_type(build_polygon_schema()) ) elif isinstance(model_field, models.GeometryCollectionField): return build_geo_container_schema( "GeometryCollection", build_array_type(build_geometry_geo_schema()) ) elif isinstance(model_field, models.GeometryField): return build_geometry_geo_schema() else: warn("Encountered unknown GIS geometry field") return {} def map_geo_field(serializer, geo_field_name): from rest_framework_gis.fields import GeometrySerializerMethodField field = serializer.fields[geo_field_name] if isinstance(field, GeometrySerializerMethodField): warn("Geometry generation for GeometrySerializerMethodField is not supported.") return {} model_field = get_field_info(serializer.Meta.model).fields[geo_field_name] return build_geo_schema(model_field) def _inject_enum_collision_fix(collection): from drf_spectacular.settings import spectacular_settings if not collection and 'GisFeatureEnum' not in spectacular_settings.ENUM_NAME_OVERRIDES: spectacular_settings.ENUM_NAME_OVERRIDES['GisFeatureEnum'] = ('Feature',) if collection and 'GisFeatureCollectionEnum' not in spectacular_settings.ENUM_NAME_OVERRIDES: spectacular_settings.ENUM_NAME_OVERRIDES['GisFeatureCollectionEnum'] = ('FeatureCollection',) class GeoFeatureModelSerializerExtension(OpenApiSerializerExtension): target_class = 'rest_framework_gis.serializers.GeoFeatureModelSerializer' match_subclasses = True def map_serializer(self, auto_schema, direction): _inject_enum_collision_fix(collection=False) base_schema = auto_schema._map_serializer(self.target, direction, bypass_extensions=True) return self.map_geo_feature_model_serializer(self.target, base_schema) def map_geo_feature_model_serializer(self, serializer, base_schema): from rest_framework_gis.serializers import GeoFeatureModelSerializer geo_properties = { "type": {"type": "string", "enum": ["Feature"]} } if serializer.Meta.id_field: geo_properties["id"] = base_schema["properties"].pop(serializer.Meta.id_field) geo_properties["geometry"] = map_geo_field(serializer, serializer.Meta.geo_field) base_schema["properties"].pop(serializer.Meta.geo_field) if serializer.Meta.auto_bbox or serializer.Meta.bbox_geo_field: geo_properties["bbox"] = build_bbox_schema() base_schema["properties"].pop(serializer.Meta.bbox_geo_field, None) # only expose if description comes from the user description = base_schema.pop('description', None) if description == get_doc(GeoFeatureModelSerializer): description = None # ignore this aspect for now base_schema.pop('required', None) # nest remaining fields under property "properties" geo_properties["properties"] = base_schema return build_object_type( properties=geo_properties, description=description, ) class GeoFeatureModelListSerializerExtension(OpenApiSerializerExtension): target_class = 'rest_framework_gis.serializers.GeoFeatureModelListSerializer' def map_serializer(self, auto_schema, direction): _inject_enum_collision_fix(collection=True) # build/retrieve feature component generated by GeoFeatureModelSerializerExtension. # wrap the ref in the special list structure and build another component based on that. feature_component = auto_schema.resolve_serializer(self.target.child, direction) collection_schema = build_object_type( properties={ "type": {"type": "string", "enum": ["FeatureCollection"]}, "features": build_array_type(feature_component.ref) } ) list_component = ResolvedComponent( name=f'{feature_component.name}List', type=ResolvedComponent.SCHEMA, object=self.target.child, schema=collection_schema ) auto_schema.registry.register_on_missing(list_component) return list_component.ref class GeometryFieldExtension(OpenApiSerializerFieldExtension): target_class = 'rest_framework_gis.fields.GeometryField' match_subclasses = True def map_serializer_field(self, auto_schema, direction): # running this extension for GeoFeatureModelSerializer's geo_field is superfluous # as above extension already handles that individually. We run it anyway because # robustly checking the proper condition is harder. try: model = self.target.parent.Meta.model model_field = follow_field_source(model, self.target.source.split('.')) return build_geo_schema(model_field) except: # noqa: E722 warn(f'Encountered an issue resolving field {self.target}. defaulting to generic object.') return {} drf-spectacular-0.27.0/drf_spectacular/contrib/rest_framework_jwt.py000066400000000000000000000011411453572150400257530ustar00rootroot00000000000000from drf_spectacular.extensions import OpenApiAuthenticationExtension from drf_spectacular.plumbing import build_bearer_security_scheme_object class JWTScheme(OpenApiAuthenticationExtension): target_class = 'rest_framework_jwt.authentication.JSONWebTokenAuthentication' name = 'jwtAuth' def get_security_definition(self, auto_schema): from rest_framework_jwt.settings import api_settings return build_bearer_security_scheme_object( header_name='AUTHORIZATION', token_prefix=api_settings.JWT_AUTH_HEADER_PREFIX, bearer_format='JWT' ) drf-spectacular-0.27.0/drf_spectacular/contrib/rest_framework_recursive.py000066400000000000000000000012141453572150400271570ustar00rootroot00000000000000from drf_spectacular.extensions import OpenApiSerializerFieldExtension from drf_spectacular.plumbing import build_array_type, is_list_serializer class RecursiveFieldExtension(OpenApiSerializerFieldExtension): target_class = "rest_framework_recursive.fields.RecursiveField" def map_serializer_field(self, auto_schema, direction): proxied = self.target.proxied if is_list_serializer(proxied): component = auto_schema.resolve_serializer(proxied.child, direction) return build_array_type(component.ref) component = auto_schema.resolve_serializer(proxied, direction) return component.ref drf-spectacular-0.27.0/drf_spectacular/contrib/rest_framework_simplejwt.py000066400000000000000000000066421453572150400272000ustar00rootroot00000000000000from rest_framework import serializers from drf_spectacular.drainage import warn from drf_spectacular.extensions import OpenApiAuthenticationExtension, OpenApiSerializerExtension from drf_spectacular.plumbing import build_bearer_security_scheme_object from drf_spectacular.utils import inline_serializer class TokenObtainPairSerializerExtension(OpenApiSerializerExtension): target_class = 'rest_framework_simplejwt.serializers.TokenObtainPairSerializer' def map_serializer(self, auto_schema, direction): Fixed = inline_serializer('Fixed', fields={ self.target_class.username_field: serializers.CharField(write_only=True), 'password': serializers.CharField(write_only=True), 'access': serializers.CharField(read_only=True), 'refresh': serializers.CharField(read_only=True), }) return auto_schema._map_serializer(Fixed, direction) class TokenObtainSlidingSerializerExtension(OpenApiSerializerExtension): target_class = 'rest_framework_simplejwt.serializers.TokenObtainSlidingSerializer' def map_serializer(self, auto_schema, direction): Fixed = inline_serializer('Fixed', fields={ self.target_class.username_field: serializers.CharField(write_only=True), 'password': serializers.CharField(write_only=True), 'token': serializers.CharField(read_only=True), }) return auto_schema._map_serializer(Fixed, direction) class TokenRefreshSerializerExtension(OpenApiSerializerExtension): target_class = 'rest_framework_simplejwt.serializers.TokenRefreshSerializer' def map_serializer(self, auto_schema, direction): from rest_framework_simplejwt.settings import api_settings if api_settings.ROTATE_REFRESH_TOKENS: class Fixed(serializers.Serializer): access = serializers.CharField(read_only=True) refresh = serializers.CharField() else: class Fixed(serializers.Serializer): access = serializers.CharField(read_only=True) refresh = serializers.CharField(write_only=True) return auto_schema._map_serializer(Fixed, direction) class TokenVerifySerializerExtension(OpenApiSerializerExtension): target_class = 'rest_framework_simplejwt.serializers.TokenVerifySerializer' def map_serializer(self, auto_schema, direction): Fixed = inline_serializer('Fixed', fields={ 'token': serializers.CharField(write_only=True), }) return auto_schema._map_serializer(Fixed, direction) class SimpleJWTScheme(OpenApiAuthenticationExtension): target_class = 'rest_framework_simplejwt.authentication.JWTAuthentication' name = 'jwtAuth' def get_security_definition(self, auto_schema): from rest_framework_simplejwt.settings import api_settings if len(api_settings.AUTH_HEADER_TYPES) > 1: warn( f'OpenAPI3 can only have one "bearerFormat". JWT Settings specify ' f'{api_settings.AUTH_HEADER_TYPES}. Using the first one.' ) return build_bearer_security_scheme_object( header_name=getattr(api_settings, 'AUTH_HEADER_NAME', 'HTTP_AUTHORIZATION'), token_prefix=api_settings.AUTH_HEADER_TYPES[0], bearer_format='JWT' ) class SimpleJWTTokenUserScheme(SimpleJWTScheme): target_class = 'rest_framework_simplejwt.authentication.JWTTokenUserAuthentication' drf-spectacular-0.27.0/drf_spectacular/contrib/rest_polymorphic.py000066400000000000000000000064241453572150400254500ustar00rootroot00000000000000from drf_spectacular.drainage import warn from drf_spectacular.extensions import OpenApiSerializerExtension from drf_spectacular.plumbing import ( ResolvedComponent, build_basic_type, build_object_type, is_patched_serializer, ) from drf_spectacular.settings import spectacular_settings from drf_spectacular.types import OpenApiTypes class PolymorphicSerializerExtension(OpenApiSerializerExtension): target_class = 'rest_polymorphic.serializers.PolymorphicSerializer' match_subclasses = True def map_serializer(self, auto_schema, direction): sub_components = [] serializer = self.target for sub_model in serializer.model_serializer_mapping: sub_serializer = serializer._get_serializer_from_model_or_instance(sub_model) sub_serializer.partial = serializer.partial resource_type = serializer.to_resource_type(sub_model) component = auto_schema.resolve_serializer(sub_serializer, direction) if not component: # rebuild a virtual schema-less component to model empty serializers component = ResolvedComponent( name=auto_schema._get_serializer_name(sub_serializer, direction), type=ResolvedComponent.SCHEMA, object='virtual' ) typed_component = self.build_typed_component( auto_schema=auto_schema, component=component, resource_type_field_name=serializer.resource_type_field_name, patched=is_patched_serializer(sub_serializer, direction) ) sub_components.append((resource_type, typed_component.ref)) if not resource_type: warn( f'discriminator mapping key is empty for {sub_serializer.__class__}. ' f'this might lead to code generation issues.' ) return { 'oneOf': [ref for _, ref in sub_components], 'discriminator': { 'propertyName': serializer.resource_type_field_name, 'mapping': {resource_type: ref['$ref'] for resource_type, ref in sub_components}, } } def build_typed_component(self, auto_schema, component, resource_type_field_name, patched): if spectacular_settings.COMPONENT_SPLIT_REQUEST and component.name.endswith('Request'): typed_component_name = component.name[:-len('Request')] + 'TypedRequest' else: typed_component_name = f'{component.name}Typed' resource_type_schema = build_object_type( properties={resource_type_field_name: build_basic_type(OpenApiTypes.STR)}, required=None if patched else [resource_type_field_name] ) # if sub-serializer has an empty schema, only expose the resource_type field part if component.schema: schema = {'allOf': [resource_type_schema, component.ref]} else: schema = resource_type_schema component_typed = ResolvedComponent( name=typed_component_name, type=ResolvedComponent.SCHEMA, object=component.object, schema=schema, ) auto_schema.registry.register_on_missing(component_typed) return component_typed drf-spectacular-0.27.0/drf_spectacular/drainage.py000066400000000000000000000163321453572150400221570ustar00rootroot00000000000000import contextlib import functools import inspect import sys from collections import defaultdict from typing import Any, Callable, DefaultDict, List, Optional, Tuple, TypeVar if sys.version_info >= (3, 8): from typing import ( # type: ignore[attr-defined] # noqa: F401 Final, Literal, TypedDict, _TypedDictMeta, ) else: from typing_extensions import Final, Literal, TypedDict, _TypedDictMeta # noqa: F401 if sys.version_info >= (3, 10): from typing import TypeGuard # noqa: F401 else: from typing_extensions import TypeGuard # noqa: F401 F = TypeVar('F', bound=Callable[..., Any]) class GeneratorStats: _warn_cache: DefaultDict[str, int] = defaultdict(int) _error_cache: DefaultDict[str, int] = defaultdict(int) _traces: List[Tuple[Optional[str], Optional[str], str]] = [] _trace_lineno = False _blue = '' _red = '' _yellow = '' _clear = '' def __getattr__(self, name): if 'silent' not in self.__dict__: from drf_spectacular.settings import spectacular_settings self.silent = spectacular_settings.DISABLE_ERRORS_AND_WARNINGS try: return self.__dict__[name] except KeyError: raise AttributeError(name) def __bool__(self): return bool(self._warn_cache or self._error_cache) @contextlib.contextmanager def silence(self): self.silent, tmp = True, self.silent try: yield finally: self.silent = tmp def reset(self) -> None: self._warn_cache.clear() self._error_cache.clear() def enable_color(self) -> None: self._blue = '\033[0;34m' self._red = '\033[0;31m' self._yellow = '\033[0;33m' self._clear = '\033[0m' def enable_trace_lineno(self) -> None: self._trace_lineno = True def _get_current_trace(self) -> Tuple[Optional[str], str]: source_locations = [t for t in self._traces if t[0]] if source_locations: sourcefile, lineno, _ = source_locations[-1] source_location = f'{sourcefile}:{lineno}' if lineno else sourcefile else: source_location = '' breadcrumbs = ' > '.join(t[2] for t in self._traces) return source_location, breadcrumbs def emit(self, msg: str, severity: str) -> None: assert severity in ['warning', 'error'] cache = self._warn_cache if severity == 'warning' else self._error_cache source_location, breadcrumbs = self._get_current_trace() prefix = f'{self._blue}{source_location}: ' if source_location else '' prefix += self._yellow if severity == 'warning' else self._red prefix += f'{severity.capitalize()}' prefix += f' [{breadcrumbs}]: ' if breadcrumbs else ': ' msg = prefix + self._clear + str(msg) if not self.silent and msg not in cache: print(msg, file=sys.stderr) cache[msg] += 1 def emit_summary(self) -> None: if not self.silent and (self._warn_cache or self._error_cache): print( f'\nSchema generation summary:\n' f'Warnings: {sum(self._warn_cache.values())} ({len(self._warn_cache)} unique)\n' f'Errors: {sum(self._error_cache.values())} ({len(self._error_cache)} unique)\n', file=sys.stderr ) GENERATOR_STATS = GeneratorStats() def warn(msg: str, delayed: Any = None) -> None: if delayed: warnings = get_override(delayed, 'warnings', []) warnings.append(msg) set_override(delayed, 'warnings', warnings) else: GENERATOR_STATS.emit(msg, 'warning') def error(msg: str, delayed: Any = None) -> None: if delayed: errors = get_override(delayed, 'errors', []) errors.append(msg) set_override(delayed, 'errors', errors) else: GENERATOR_STATS.emit(msg, 'error') def reset_generator_stats() -> None: GENERATOR_STATS.reset() @contextlib.contextmanager def add_trace_message(obj): """ Adds a message to be used as a prefix when emitting warnings and errors. """ sourcefile, lineno = _get_source_location(obj) GENERATOR_STATS._traces.append((sourcefile, lineno, obj.__name__)) yield GENERATOR_STATS._traces.pop() @functools.lru_cache(maxsize=1000) def _get_source_location(obj): try: sourcefile = inspect.getsourcefile(obj) except: # noqa: E722 sourcefile = None try: # This is a rather expensive operation. Only do it when explicitly enabled (CLI) # and cache results to speed up some recurring objects like serializers. lineno = inspect.getsourcelines(obj)[1] if GENERATOR_STATS._trace_lineno else None except: # noqa: E722 lineno = None return sourcefile, lineno def has_override(obj: Any, prop: str) -> bool: if isinstance(obj, functools.partial): obj = obj.func if not hasattr(obj, '_spectacular_annotation'): return False if prop not in obj._spectacular_annotation: return False return True def get_override(obj: Any, prop: str, default: Any = None) -> Any: if isinstance(obj, functools.partial): obj = obj.func if not has_override(obj, prop): return default return obj._spectacular_annotation[prop] def set_override(obj: Any, prop: str, value: Any) -> Any: if not hasattr(obj, '_spectacular_annotation'): obj._spectacular_annotation = {} elif '_spectacular_annotation' not in obj.__dict__: obj._spectacular_annotation = obj._spectacular_annotation.copy() obj._spectacular_annotation[prop] = value return obj def get_view_method_names(view, schema=None) -> List[str]: schema = schema or view.schema return [ item for item in dir(view) if callable(getattr(view, item)) and ( item in view.http_method_names or item in schema.method_mapping.values() or item == 'list' or hasattr(getattr(view, item), 'mapping') ) ] def isolate_view_method(view, method_name): """ Prevent modifying a view method which is derived from other views. Changes to a derived method would leak into the view where the method originated from. Break derivation by wrapping the method and explicitly setting it on the view. """ method = getattr(view, method_name) # no isolation is required if the view method is not derived. # @api_view is a special case that also breaks isolation. It proxies all view # methods through a single handler function, which then also requires isolation. if method_name in view.__dict__ and method.__name__ != 'handler': return method @functools.wraps(method) def wrapped_method(self, request, *args, **kwargs): return method(self, request, *args, **kwargs) # wraps() will only create a shallow copy of method.__dict__. Updates to "kwargs" # via @extend_schema would leak to the original method. Isolate by creating a copy. if hasattr(method, 'kwargs'): wrapped_method.kwargs = method.kwargs.copy() setattr(view, method_name, wrapped_method) return wrapped_method def cache(user_function: F) -> F: """ simple polyfill for python < 3.9 """ return functools.lru_cache(maxsize=None)(user_function) # type: ignore drf-spectacular-0.27.0/drf_spectacular/extensions.py000066400000000000000000000137471453572150400226130ustar00rootroot00000000000000from abc import abstractmethod from typing import TYPE_CHECKING, Any, Dict, List, Optional, Type, Union from drf_spectacular.plumbing import OpenApiGeneratorExtension from drf_spectacular.utils import Direction if TYPE_CHECKING: from rest_framework.views import APIView from drf_spectacular.openapi import AutoSchema _SchemaType = Dict[str, Any] class OpenApiAuthenticationExtension(OpenApiGeneratorExtension['OpenApiAuthenticationExtension']): """ Extension for specifying authentication schemes. The common use-case usually consists of setting a ``name`` string and returning a dict from ``get_security_definition``. To model a group of headers that go together, set a list of names and return a corresponding list of definitions from ``get_security_definition``. The view class is available via ``auto_schema.view``, while the original authentication class can be accessed via ``self.target``. If you want to override an included extension, be sure to set a higher matching priority by setting the class attribute ``priority = 1`` or higher. get_security_requirement is expected to return a dict with security object names as keys and a scope list as value (usually just []). More than one key in the dict means that each entry is required (AND). If you need alternate variations (OR), return a list of those dicts instead. ``get_security_definition()`` is expected to return a valid `OpenAPI security scheme object `_ """ _registry: List[Type['OpenApiAuthenticationExtension']] = [] name: Union[str, List[str]] def get_security_requirement( self, auto_schema: 'AutoSchema' ) -> Union[Dict[str, List[Any]], List[Dict[str, List[Any]]]]: assert self.name, 'name(s) must be specified' if isinstance(self.name, str): return {self.name: []} else: return {name: [] for name in self.name} @abstractmethod def get_security_definition(self, auto_schema: 'AutoSchema') -> Union[_SchemaType, List[_SchemaType]]: pass # pragma: no cover class OpenApiSerializerExtension(OpenApiGeneratorExtension['OpenApiSerializerExtension']): """ Extension for replacing an insufficient or specifying an unknown Serializer schema. The existing implementation of ``map_serializer()`` will generate the same result as *drf-spectacular* would. Either augment or replace the generated schema. The view instance is available via ``auto_schema.view``, while the original serializer can be accessed via ``self.target``. ``map_serializer()`` is expected to return a valid `OpenAPI schema object `_. """ _registry: List[Type['OpenApiSerializerExtension']] = [] def get_name(self, auto_schema: 'AutoSchema', direction: Direction) -> Optional[str]: """ return str for overriding default name extraction """ return None def map_serializer(self, auto_schema: 'AutoSchema', direction: Direction) -> _SchemaType: """ override for customized serializer mapping """ return auto_schema._map_serializer(self.target_class, direction, bypass_extensions=True) class OpenApiSerializerFieldExtension(OpenApiGeneratorExtension['OpenApiSerializerFieldExtension']): """ Extension for replacing an insufficient or specifying an unknown SerializerField schema. To augment the default schema, you can get what *drf-spectacular* would generate with ``auto_schema._map_serializer_field(self.target, direction, bypass_extensions=True)``. and edit the returned schema at your discretion. Beware that this may still emit warnings, in which case manual construction is advisable. ``map_serializer_field()`` is expected to return a valid `OpenAPI schema object `_. """ _registry: List[Type['OpenApiSerializerFieldExtension']] = [] def get_name(self) -> Optional[str]: """ return str for breaking out field schema into separate named component """ return None @abstractmethod def map_serializer_field(self, auto_schema: 'AutoSchema', direction: Direction) -> _SchemaType: """ override for customized serializer field mapping """ pass # pragma: no cover class OpenApiViewExtension(OpenApiGeneratorExtension['OpenApiViewExtension']): """ Extension for replacing discovered views with a more schema-appropriate/annotated version. ``view_replacement()`` is expected to return a subclass of ``APIView`` (which includes ``ViewSet`` et al.). The discovered original view instance can be accessed with ``self.target`` and be subclassed if desired. """ _registry: List[Type['OpenApiViewExtension']] = [] @classmethod def _load_class(cls): super()._load_class() # special case @api_view: view class is nested in the cls attr of the function object if hasattr(cls.target_class, 'cls'): cls.target_class = cls.target_class.cls @abstractmethod def view_replacement(self) -> 'Type[APIView]': pass # pragma: no cover class OpenApiFilterExtension(OpenApiGeneratorExtension['OpenApiFilterExtension']): """ Extension for specifying a list of filter parameters for a given ``FilterBackend``. The original filter class object can be accessed via ``self.target``. The attached view is accessible via ``auto_schema.view``. ``get_schema_operation_parameters()`` is expected to return either an empty list or a list of valid raw `OpenAPI parameter objects `_. Using ``drf_spectacular.plumbing.build_parameter_type`` is recommended to generate the appropriate raw dict objects. """ _registry: List[Type['OpenApiFilterExtension']] = [] @abstractmethod def get_schema_operation_parameters(self, auto_schema: 'AutoSchema', *args, **kwargs) -> List[_SchemaType]: pass # pragma: no cover drf-spectacular-0.27.0/drf_spectacular/generators.py000066400000000000000000000310451453572150400225540ustar00rootroot00000000000000import os import re from django.urls import URLPattern, URLResolver from rest_framework import views, viewsets from rest_framework.schemas.generators import BaseSchemaGenerator from rest_framework.schemas.generators import EndpointEnumerator as BaseEndpointEnumerator from rest_framework.settings import api_settings from drf_spectacular.drainage import ( add_trace_message, error, get_override, reset_generator_stats, warn, ) from drf_spectacular.extensions import OpenApiViewExtension from drf_spectacular.openapi import AutoSchema from drf_spectacular.plumbing import ( ComponentRegistry, alpha_operation_sorter, build_root_object, camelize_operation, get_class, is_versioning_supported, modify_for_versioning, normalize_result_object, operation_matches_version, sanitize_result_object, ) from drf_spectacular.settings import spectacular_settings class EndpointEnumerator(BaseEndpointEnumerator): def get_api_endpoints(self, patterns=None, prefix=''): api_endpoints = self._get_api_endpoints(patterns, prefix) for hook in spectacular_settings.PREPROCESSING_HOOKS: api_endpoints = hook(endpoints=api_endpoints) api_endpoints_deduplicated = {} for path, path_regex, method, callback in api_endpoints: if (path, method) not in api_endpoints_deduplicated: api_endpoints_deduplicated[path, method] = (path, path_regex, method, callback) api_endpoints = list(api_endpoints_deduplicated.values()) if callable(spectacular_settings.SORT_OPERATIONS): return sorted(api_endpoints, key=spectacular_settings.SORT_OPERATIONS) elif spectacular_settings.SORT_OPERATIONS: return sorted(api_endpoints, key=alpha_operation_sorter) else: return api_endpoints def get_path_from_regex(self, path_regex): path = super().get_path_from_regex(path_regex) # bugfix oversight in DRF regex stripping path = path.replace('\\.', '.') return path def _get_api_endpoints(self, patterns, prefix): """ Return a list of all available API endpoints by inspecting the URL conf. Only modification the DRF version is passing through the path_regex. """ if patterns is None: patterns = self.patterns api_endpoints = [] for pattern in patterns: path_regex = prefix + str(pattern.pattern) if isinstance(pattern, URLPattern): path = self.get_path_from_regex(path_regex) callback = pattern.callback if self.should_include_endpoint(path, callback): for method in self.get_allowed_methods(callback): endpoint = (path, path_regex, method, callback) api_endpoints.append(endpoint) elif isinstance(pattern, URLResolver): nested_endpoints = self._get_api_endpoints( patterns=pattern.url_patterns, prefix=path_regex ) api_endpoints.extend(nested_endpoints) return api_endpoints def get_allowed_methods(self, callback): if hasattr(callback, 'actions'): actions = set(callback.actions) http_method_names = set(callback.cls.http_method_names) methods = [method.upper() for method in actions & http_method_names] else: # pass to constructor allowed method names to get valid ones kwargs = {} if 'http_method_names' in callback.initkwargs: kwargs['http_method_names'] = callback.initkwargs['http_method_names'] methods = callback.cls(**kwargs).allowed_methods return [ method for method in methods if method not in ('OPTIONS', 'HEAD', 'TRACE', 'CONNECT') ] class SchemaGenerator(BaseSchemaGenerator): endpoint_inspector_cls = EndpointEnumerator def __init__(self, *args, **kwargs): self.registry = ComponentRegistry() self.api_version = kwargs.pop('api_version', None) self.inspector = None super().__init__(*args, **kwargs) def coerce_path(self, path, method, view): """ Customized coerce_path which also considers the `_pk` suffix in URL paths of nested routers. """ path = super().coerce_path(path, method, view) # take care of {pk} if spectacular_settings.SCHEMA_COERCE_PATH_PK_SUFFIX: path = re.sub(pattern=r'{(\w+)_pk}', repl=r'{\1_id}', string=path) return path def create_view(self, callback, method, request=None): """ customized create_view which is called when all routes are traversed. part of this is instantiating views with default params. in case of custom routes (@action) the custom AutoSchema is injected properly through 'initkwargs' on view. However, when decorating plain views like retrieve, this initialization logic is not running. Therefore forcefully set the schema if @extend_schema decorator was used. """ override_view = OpenApiViewExtension.get_match(callback.cls) if override_view: original_cls = callback.cls callback.cls = override_view.view_replacement() # we refrain from passing request and deal with it ourselves in parse() view = super().create_view(callback, method, None) # drf-yasg compatibility feature. makes the view aware that we are running # schema generation and not a real request. view.swagger_fake_view = True # callback.cls is hosted in urlpatterns and is therefore not an ephemeral modification. # restore after view creation so potential revisits have a clean state as basis. if override_view: callback.cls = original_cls if isinstance(view, viewsets.ViewSetMixin): action = getattr(view, view.action) elif isinstance(view, views.APIView): action = getattr(view, method.lower()) else: error( 'Using not supported View class. Class must be derived from APIView ' 'or any of its subclasses like GenericApiView, GenericViewSet.' ) return view action_schema = getattr(action, 'kwargs', {}).get('schema', None) if not action_schema: # there is no method/action customized schema so we are done here. return view # action_schema is either a class or instance. when @extend_schema is used, it # is always a class to prevent the weakref reverse "schema.view" bug for multi # annotations. The bug is prevented by delaying the instantiation of the schema # class until create_view (here) and not doing it immediately in @extend_schema. action_schema_class = get_class(action_schema) view_schema_class = get_class(callback.cls.schema) if not issubclass(action_schema_class, view_schema_class): # this handles the case of having a manually set custom AutoSchema on the # view together with extend_schema. In most cases, the decorator mechanics # prevent extend_schema from having access to the view's schema class. So # extend_schema is forced to use DEFAULT_SCHEMA_CLASS as fallback base class # instead of the correct base class set in view. We remedy this chicken-egg # problem here by rearranging the class hierarchy. mro = tuple( cls for cls in action_schema_class.__mro__ if cls not in api_settings.DEFAULT_SCHEMA_CLASS.__mro__ ) + view_schema_class.__mro__ action_schema_class = type('ExtendedRearrangedSchema', mro, {}) view.schema = action_schema_class() return view def _initialise_endpoints(self): if self.endpoints is None: self.inspector = self.endpoint_inspector_cls(self.patterns, self.urlconf) self.endpoints = self.inspector.get_api_endpoints() def _get_paths_and_endpoints(self): """ Generate (path, method, view) given (path, method, callback) for paths. """ view_endpoints = [] for path, path_regex, method, callback in self.endpoints: view = self.create_view(callback, method) path = self.coerce_path(path, method, view) view_endpoints.append((path, path_regex, method, view)) return view_endpoints def parse(self, input_request, public): """ Iterate endpoints generating per method path operations. """ result = {} self._initialise_endpoints() endpoints = self._get_paths_and_endpoints() if spectacular_settings.SCHEMA_PATH_PREFIX is None: # estimate common path prefix if none was given. only use it if we encountered more # than one view to prevent emission of erroneous and unnecessary fallback names. non_trivial_prefix = len(set([view.__class__ for _, _, _, view in endpoints])) > 1 if non_trivial_prefix: path_prefix = os.path.commonpath([path for path, _, _, _ in endpoints]) path_prefix = re.escape(path_prefix) # guard for RE special chars in path else: path_prefix = '/' else: path_prefix = spectacular_settings.SCHEMA_PATH_PREFIX if not path_prefix.startswith('^'): path_prefix = '^' + path_prefix # make sure regex only matches from the start for path, path_regex, method, view in endpoints: # emit queued up warnings/error that happened prior to generation (decoration) for w in get_override(view, 'warnings', []): warn(w) for e in get_override(view, 'errors', []): error(e) view.request = spectacular_settings.GET_MOCK_REQUEST(method, path, view, input_request) if not (public or self.has_view_permissions(path, method, view)): continue if view.versioning_class and not is_versioning_supported(view.versioning_class): warn( f'using unsupported versioning class "{view.versioning_class}". view will be ' f'processed as unversioned view.' ) elif view.versioning_class: version = ( self.api_version # explicit version from CLI, SpecView or SpecView request or view.versioning_class.default_version # fallback ) if not version: continue path = modify_for_versioning(self.inspector.patterns, method, path, view, version) if not operation_matches_version(view, version): continue assert isinstance(view.schema, AutoSchema), ( f'Incompatible AutoSchema used on View {view.__class__}. Is DRF\'s ' f'DEFAULT_SCHEMA_CLASS pointing to "drf_spectacular.openapi.AutoSchema" ' f'or any other drf-spectacular compatible AutoSchema?' ) with add_trace_message(getattr(view, '__class__', view)): operation = view.schema.get_operation( path, path_regex, path_prefix, method, self.registry ) # operation was manually removed via @extend_schema if not operation: continue if spectacular_settings.SCHEMA_PATH_PREFIX_TRIM: path = re.sub(pattern=path_prefix, repl='', string=path, flags=re.IGNORECASE) if spectacular_settings.SCHEMA_PATH_PREFIX_INSERT: path = spectacular_settings.SCHEMA_PATH_PREFIX_INSERT + path if not path.startswith('/'): path = '/' + path if spectacular_settings.CAMELIZE_NAMES: path, operation = camelize_operation(path, operation) result.setdefault(path, {}) result[path][method.lower()] = operation return result def get_schema(self, request=None, public=False): """ Generate a OpenAPI schema. """ reset_generator_stats() result = build_root_object( paths=self.parse(request, public), components=self.registry.build(spectacular_settings.APPEND_COMPONENTS), version=self.api_version or getattr(request, 'version', None), ) for hook in spectacular_settings.POSTPROCESSING_HOOKS: result = hook(result=result, generator=self, request=request, public=public) return sanitize_result_object(normalize_result_object(result)) drf-spectacular-0.27.0/drf_spectacular/helpers.py000066400000000000000000000025311453572150400220430ustar00rootroot00000000000000from django.utils.module_loading import import_string def lazy_serializer(path: str): """ simulate initiated object but actually load class and init on first usage """ class LazySerializer: def __init__(self, *args, **kwargs): self.lazy_args, self.lazy_kwargs, self.lazy_obj = args, kwargs, None def __getattr__(self, item): if not self.lazy_obj: self.lazy_obj = import_string(path)(*self.lazy_args, **self.lazy_kwargs) return getattr(self.lazy_obj, item) @property # type: ignore def __class__(self): return self.__getattr__('__class__') @property def __dict__(self): return self.__getattr__('__dict__') def __str__(self): return self.__getattr__('__str__')() def __repr__(self): return self.__getattr__('__repr__')() return LazySerializer def forced_singular_serializer(serializer_class): from drf_spectacular.drainage import set_override from drf_spectacular.utils import extend_schema_serializer patched_serializer_class = type(serializer_class.__name__, (serializer_class,), {}) extend_schema_serializer(many=False)(patched_serializer_class) set_override(patched_serializer_class, 'suppress_collision_warning', True) return patched_serializer_class drf-spectacular-0.27.0/drf_spectacular/hooks.py000066400000000000000000000233511453572150400215270ustar00rootroot00000000000000import re from collections import defaultdict from inflection import camelize from rest_framework.settings import api_settings from drf_spectacular.drainage import warn from drf_spectacular.plumbing import ( ResolvedComponent, list_hash, load_enum_name_overrides, safe_ref, ) from drf_spectacular.settings import spectacular_settings def postprocess_schema_enums(result, generator, **kwargs): """ simple replacement of Enum/Choices that globally share the same name and have the same choices. Aids client generation to not generate a separate enum for every occurrence. only takes effect when replacement is guaranteed to be correct. """ def iter_prop_containers(schema, component_name=None): if not component_name: for component_name, schema in schema.items(): if spectacular_settings.COMPONENT_SPLIT_PATCH: component_name = re.sub('^Patched(.+)', r'\1', component_name) if spectacular_settings.COMPONENT_SPLIT_REQUEST: component_name = re.sub('(.+)Request$', r'\1', component_name) yield from iter_prop_containers(schema, component_name) elif isinstance(schema, list): for item in schema: yield from iter_prop_containers(item, component_name) elif isinstance(schema, dict): if schema.get('properties'): yield component_name, schema['properties'] yield from iter_prop_containers(schema.get('oneOf', []), component_name) yield from iter_prop_containers(schema.get('allOf', []), component_name) yield from iter_prop_containers(schema.get('anyOf', []), component_name) def create_enum_component(name, schema): component = ResolvedComponent( name=name, type=ResolvedComponent.SCHEMA, schema=schema, object=name, ) generator.registry.register_on_missing(component) return component def extract_hash(schema): if 'x-spec-enum-id' in schema: # try to use the injected enum hash first as it generated from (name, value) tuples, # which prevents collisions on choice sets only differing in labels not values. return schema['x-spec-enum-id'] else: # fall back to actual list hashing when we encounter enums not generated by us. # remove blank/null entry for hashing. will be reconstructed in the last step return list_hash([(i, i) for i in schema['enum'] if i not in ('', None)]) schemas = result.get('components', {}).get('schemas', {}) overrides = load_enum_name_overrides() prop_hash_mapping = defaultdict(set) hash_name_mapping = defaultdict(set) # collect all enums, their names and choice sets for component_name, props in iter_prop_containers(schemas): for prop_name, prop_schema in props.items(): if prop_schema.get('type') == 'array': prop_schema = prop_schema.get('items', {}) if 'enum' not in prop_schema: continue prop_enum_cleaned_hash = extract_hash(prop_schema) prop_hash_mapping[prop_name].add(prop_enum_cleaned_hash) hash_name_mapping[prop_enum_cleaned_hash].add((component_name, prop_name)) # traverse all enum properties and generate a name for the choice set. naming collisions # are resolved and a warning is emitted. giving a choice set multiple names is technically # correct but potentially unwanted. also emit a warning there to make the user aware. enum_name_mapping = {} for prop_name, prop_hash_set in prop_hash_mapping.items(): for prop_hash in prop_hash_set: if prop_hash in overrides: enum_name = overrides[prop_hash] elif len(prop_hash_set) == 1: # prop_name has been used exclusively for one choice set (best case) enum_name = f'{camelize(prop_name)}Enum' elif len(hash_name_mapping[prop_hash]) == 1: # prop_name has multiple choice sets, but each one limited to one component only component_name, _ = next(iter(hash_name_mapping[prop_hash])) enum_name = f'{camelize(component_name)}{camelize(prop_name)}Enum' else: enum_name = f'{camelize(prop_name)}{prop_hash[:3].capitalize()}Enum' warn( f'enum naming encountered a non-optimally resolvable collision for fields ' f'named "{prop_name}". The same name has been used for multiple choice sets ' f'in multiple components. The collision was resolved with "{enum_name}". ' f'add an entry to ENUM_NAME_OVERRIDES to fix the naming.' ) if enum_name_mapping.get(prop_hash, enum_name) != enum_name: warn( f'encountered multiple names for the same choice set ({enum_name}). This ' f'may be unwanted even though the generated schema is technically correct. ' f'Add an entry to ENUM_NAME_OVERRIDES to fix the naming.' ) del enum_name_mapping[prop_hash] else: enum_name_mapping[prop_hash] = enum_name enum_name_mapping[(prop_hash, prop_name)] = enum_name # replace all enum occurrences with a enum schema component. cut out the # enum, replace it with a reference and add a corresponding component. for _, props in iter_prop_containers(schemas): for prop_name, prop_schema in props.items(): is_array = prop_schema.get('type') == 'array' if is_array: prop_schema = prop_schema.get('items', {}) if 'enum' not in prop_schema: continue prop_enum_original_list = prop_schema['enum'] prop_schema['enum'] = [i for i in prop_schema['enum'] if i not in ['', None]] prop_hash = extract_hash(prop_schema) # when choice sets are reused under multiple names, the generated name cannot be # resolved from the hash alone. fall back to prop_name and hash for resolution. enum_name = enum_name_mapping.get(prop_hash) or enum_name_mapping[prop_hash, prop_name] # split property into remaining property and enum component parts enum_schema = {k: v for k, v in prop_schema.items() if k in ['type', 'enum']} prop_schema = {k: v for k, v in prop_schema.items() if k not in ['type', 'enum', 'x-spec-enum-id']} # separate actual description from name-value tuples if spectacular_settings.ENUM_GENERATE_CHOICE_DESCRIPTION: if prop_schema.get('description', '').startswith('*'): enum_schema['description'] = prop_schema.pop('description') elif '\n\n*' in prop_schema.get('description', ''): _, _, post = prop_schema['description'].partition('\n\n*') enum_schema['description'] = '*' + post components = [ create_enum_component(enum_name, schema=enum_schema) ] if spectacular_settings.ENUM_ADD_EXPLICIT_BLANK_NULL_CHOICE: if '' in prop_enum_original_list: components.append(create_enum_component('BlankEnum', schema={'enum': ['']})) if None in prop_enum_original_list: if spectacular_settings.OAS_VERSION.startswith('3.1'): components.append(create_enum_component('NullEnum', schema={'type': 'null'})) else: components.append(create_enum_component('NullEnum', schema={'enum': [None]})) # undo OAS 3.1 type list NULL construction as we cover this in a separate component already if spectacular_settings.OAS_VERSION.startswith('3.1') and isinstance(enum_schema['type'], list): enum_schema['type'] = [t for t in enum_schema['type'] if t != 'null'][0] if len(components) == 1: prop_schema.update(components[0].ref) else: prop_schema.update({'oneOf': [c.ref for c in components]}) if is_array: props[prop_name]['items'] = safe_ref(prop_schema) else: props[prop_name] = safe_ref(prop_schema) # sort again with additional components result['components'] = generator.registry.build(spectacular_settings.APPEND_COMPONENTS) # remove remaining ids that were not part of this hook (operation parameters mainly) postprocess_schema_enum_id_removal(result, generator) return result def postprocess_schema_enum_id_removal(result, generator, **kwargs): """ Iterative modifying approach to scanning the whole schema and removing the temporary helper ids that allowed us to distinguish similar enums. """ def clean(sub_result): if isinstance(sub_result, dict): for key in list(sub_result): if key == 'x-spec-enum-id': del sub_result['x-spec-enum-id'] else: clean(sub_result[key]) elif isinstance(sub_result, (list, tuple)): for item in sub_result: clean(item) clean(result) return result def preprocess_exclude_path_format(endpoints, **kwargs): """ preprocessing hook that filters out {format} suffixed paths, in case format_suffix_patterns is used and {format} path params are unwanted. """ format_path = f'{{{api_settings.FORMAT_SUFFIX_KWARG}}}' return [ (path, path_regex, method, callback) for path, path_regex, method, callback in endpoints if not (path.endswith(format_path) or path.endswith(format_path + '/')) ] drf-spectacular-0.27.0/drf_spectacular/management/000077500000000000000000000000001453572150400221425ustar00rootroot00000000000000drf-spectacular-0.27.0/drf_spectacular/management/__init__.py000066400000000000000000000000001453572150400242410ustar00rootroot00000000000000drf-spectacular-0.27.0/drf_spectacular/management/commands/000077500000000000000000000000001453572150400237435ustar00rootroot00000000000000drf-spectacular-0.27.0/drf_spectacular/management/commands/__init__.py000066400000000000000000000000001453572150400260420ustar00rootroot00000000000000drf-spectacular-0.27.0/drf_spectacular/management/commands/spectacular.py000066400000000000000000000075031453572150400266300ustar00rootroot00000000000000from textwrap import dedent from django.core.management.base import BaseCommand, CommandError from django.utils import translation from django.utils.module_loading import import_string from drf_spectacular.drainage import GENERATOR_STATS from drf_spectacular.renderers import OpenApiJsonRenderer, OpenApiYamlRenderer from drf_spectacular.settings import patched_settings, spectacular_settings from drf_spectacular.validation import validate_schema class SchemaGenerationError(CommandError): pass class SchemaValidationError(CommandError): pass class Command(BaseCommand): help = dedent(""" Generate a spectacular OpenAPI3-compliant schema for your API. The warnings serve as a indicator for where your API could not be properly resolved. @extend_schema and @extend_schema_field are your friends. The spec should be valid in any case. If not, please open an issue on github: https://github.com/tfranzel/drf-spectacular/issues Remember to configure your APIs meta data like servers, version, url, documentation and so on in your SPECTACULAR_SETTINGS." """) def add_arguments(self, parser): parser.add_argument('--format', dest="format", choices=['openapi', 'openapi-json'], default='openapi', type=str) parser.add_argument('--urlconf', dest="urlconf", default=None, type=str) parser.add_argument('--generator-class', dest="generator_class", default=None, type=str) parser.add_argument('--file', dest="file", default=None, type=str) parser.add_argument('--fail-on-warn', dest="fail_on_warn", default=False, action='store_true') parser.add_argument('--validate', dest="validate", default=False, action='store_true') parser.add_argument('--api-version', dest="api_version", default=None, type=str) parser.add_argument('--lang', dest="lang", default=None, type=str) parser.add_argument('--color', dest="color", default=False, action='store_true') parser.add_argument('--custom-settings', dest="custom_settings", default=None, type=str) def handle(self, *args, **options): if options['generator_class']: generator_class = import_string(options['generator_class']) else: generator_class = spectacular_settings.DEFAULT_GENERATOR_CLASS GENERATOR_STATS.enable_trace_lineno() if options['color']: GENERATOR_STATS.enable_color() generator = generator_class( urlconf=options['urlconf'], api_version=options['api_version'], ) if options['custom_settings']: custom_settings = import_string(options['custom_settings']) else: custom_settings = None with patched_settings(custom_settings): if options['lang']: with translation.override(options['lang']): schema = generator.get_schema(request=None, public=True) else: schema = generator.get_schema(request=None, public=True) GENERATOR_STATS.emit_summary() if options['fail_on_warn'] and GENERATOR_STATS: raise SchemaGenerationError('Failing as requested due to warnings') if options['validate']: try: validate_schema(schema) except Exception as e: raise SchemaValidationError(e) renderer = self.get_renderer(options['format']) output = renderer.render(schema, renderer_context={}) if options['file']: with open(options['file'], 'wb') as f: f.write(output) else: self.stdout.write(output.decode()) def get_renderer(self, format): renderer_cls = { 'openapi': OpenApiYamlRenderer, 'openapi-json': OpenApiJsonRenderer, }[format] return renderer_cls() drf-spectacular-0.27.0/drf_spectacular/openapi.py000066400000000000000000002206261453572150400220430ustar00rootroot00000000000000import copy import functools import itertools import re from collections import defaultdict from typing import Any, Dict, List, Optional, Union import uritemplate from django.core import exceptions as django_exceptions from django.core import validators from django.db import models from django.utils.translation import gettext_lazy as _ from rest_framework import permissions, renderers, serializers from rest_framework.fields import _UnvalidatedField, empty from rest_framework.generics import CreateAPIView, GenericAPIView, ListCreateAPIView from rest_framework.mixins import ListModelMixin from rest_framework.schemas.inspectors import ViewInspector from rest_framework.schemas.utils import get_pk_description from rest_framework.settings import api_settings from rest_framework.utils.model_meta import get_field_info from rest_framework.views import APIView import drf_spectacular.authentication # noqa: F403, F401 import drf_spectacular.serializers # noqa: F403, F401 from drf_spectacular.contrib import * # noqa: F403, F401 from drf_spectacular.drainage import add_trace_message, error, get_override, has_override, warn from drf_spectacular.extensions import ( OpenApiAuthenticationExtension, OpenApiFilterExtension, OpenApiSerializerExtension, OpenApiSerializerFieldExtension, ) from drf_spectacular.plumbing import ( ComponentRegistry, ResolvedComponent, UnableToProceedError, append_meta, assert_basic_serializer, build_array_type, build_basic_type, build_choice_field, build_examples_list, build_generic_type, build_listed_example_value, build_media_type_object, build_mocked_view, build_object_type, build_parameter_type, build_serializer_context, filter_supported_arguments, follow_field_source, follow_model_field_lookup, force_instance, get_doc, get_list_serializer, get_manager, get_type_hints, get_view_model, is_basic_serializer, is_basic_type, is_field, is_list_serializer, is_list_serializer_customized, is_patched_serializer, is_serializer, is_trivial_string_variation, modify_media_types_for_versioning, resolve_django_path_parameter, resolve_regex_path_parameter, resolve_type_hint, safe_ref, sanitize_specification_extensions, whitelisted, ) from drf_spectacular.settings import spectacular_settings from drf_spectacular.types import OpenApiTypes from drf_spectacular.utils import ( Direction, OpenApiCallback, OpenApiExample, OpenApiParameter, OpenApiRequest, OpenApiResponse, _SchemaType, _SerializerType, ) class AutoSchema(ViewInspector): method_mapping = { 'get': 'retrieve', 'post': 'create', 'put': 'update', 'patch': 'partial_update', 'delete': 'destroy', } def get_operation( self, path: str, path_regex: str, path_prefix: str, method: str, registry: ComponentRegistry ) -> Optional[_SchemaType]: self.registry = registry self.path = path self.path_regex = path_regex self.path_prefix = path_prefix self.method = method.upper() if self.is_excluded(): return None operation: _SchemaType = {'operationId': self.get_operation_id()} description = self.get_description() if description: operation['description'] = description summary = self.get_summary() if summary: operation['summary'] = summary external_docs = self._get_external_docs() if external_docs: operation['externalDocs'] = external_docs parameters = self._get_parameters() if parameters: operation['parameters'] = parameters tags = self.get_tags() if tags: operation['tags'] = tags request_body = self._get_request_body() if request_body: operation['requestBody'] = request_body auth = self.get_auth() if auth: operation['security'] = auth deprecated = self.is_deprecated() if deprecated: operation['deprecated'] = deprecated operation['responses'] = self._get_response_bodies() extensions = self.get_extensions() if extensions: operation.update(sanitize_specification_extensions(extensions)) callbacks = self._get_callbacks() if callbacks: operation['callbacks'] = callbacks return operation def is_excluded(self) -> bool: """ override this for custom behaviour """ return False def _is_list_view(self, serializer: Optional[_SerializerType] = None) -> bool: """ partially heuristic approach to determine if a view yields an object or a list of objects. used for operationId naming, array building and pagination. defaults to False if all introspection fail. """ if serializer is None: serializer = self.get_response_serializers() if isinstance(serializer, dict) and serializer: # extract likely main serializer from @extend_schema override serializer = {str(code): s for code, s in serializer.items()} serializer = serializer[min(serializer)] if is_list_serializer(serializer): return True if is_basic_type(serializer): return False if hasattr(self.view, 'action'): return self.view.action == 'list' # list responses are "usually" only returned by GET if self.method != 'GET': return False if isinstance(self.view, ListModelMixin): return True # primary key/lookup variable in path is a strong indicator for retrieve if isinstance(self.view, GenericAPIView): lookup_url_kwarg = self.view.lookup_url_kwarg or self.view.lookup_field if lookup_url_kwarg in uritemplate.variables(self.path): return False return False def _is_create_operation(self) -> bool: if self.method != 'POST': return False if getattr(self.view, 'action', None) == 'create': return True if isinstance(self.view, (ListCreateAPIView, CreateAPIView)): return True return False def get_override_parameters(self) -> List[Union[OpenApiParameter, _SerializerType]]: """ override this for custom behaviour """ return [] def _process_override_parameters(self, direction='request'): result = {} for parameter in self.get_override_parameters(): if isinstance(parameter, OpenApiParameter): if parameter.response: continue if is_basic_type(parameter.type): schema = build_basic_type(parameter.type) elif is_basic_serializer(parameter.type): schema = self.resolve_serializer(parameter.type, direction).ref elif isinstance(parameter.type, dict): schema = parameter.type else: warn(f'unsupported type for parameter "{parameter.name}". Skipping.') continue if parameter.many: if is_basic_type(parameter.type): schema = build_array_type(schema) else: warn( f'parameter "{parameter.name}" has many=True and is not a basic type. ' f'many=True only makes sense for basic types. Ignoring.' ) if parameter.exclude: result[parameter.name, parameter.location] = None else: result[parameter.name, parameter.location] = build_parameter_type( name=parameter.name, schema=schema, location=parameter.location, required=parameter.required, description=parameter.description, enum=parameter.enum, pattern=parameter.pattern, deprecated=parameter.deprecated, style=parameter.style, explode=parameter.explode, default=parameter.default, allow_blank=parameter.allow_blank, examples=build_examples_list(parameter.examples), extensions=parameter.extensions, ) elif is_basic_serializer(parameter): # explode serializer into separate parameters. defaults to QUERY location parameter = force_instance(parameter) mapped = self._map_serializer(parameter, 'request') for property_name, property_schema in mapped['properties'].items(): field = parameter.fields.get(property_name) result[property_name, OpenApiParameter.QUERY] = build_parameter_type( name=property_name, schema=property_schema, description=property_schema.pop('description', None), location=OpenApiParameter.QUERY, allow_blank=getattr(field, 'allow_blank', True), required=field.required, ) else: warn(f'could not resolve parameter annotation {parameter}. Skipping.') return result def _get_format_parameters(self) -> List[_SchemaType]: parameters = [] formats = self.map_renderers('format') if api_settings.URL_FORMAT_OVERRIDE and len(formats) > 1: parameters.append(build_parameter_type( name=api_settings.URL_FORMAT_OVERRIDE, schema=build_basic_type(OpenApiTypes.STR), # type: ignore location=OpenApiParameter.QUERY, enum=formats )) return parameters def _get_parameters(self) -> List[_SchemaType]: def dict_helper(parameters): return {(p['name'], p['in']): p for p in parameters} override_parameters = self._process_override_parameters() # remove overridden path parameters beforehand so that there are no irrelevant warnings. path_variables = [ v for v in uritemplate.variables(self.path) if (v, 'path') not in override_parameters ] parameters = { **dict_helper(self._resolve_path_parameters(path_variables)), **dict_helper(self._get_filter_parameters()), **dict_helper(self._get_pagination_parameters()), **dict_helper(self._get_format_parameters()), } # override/add/remove @extend_schema parameters for key, parameter in override_parameters.items(): if parameter is None: # either omit or explicitly remove parameter if key in parameters: del parameters[key] else: parameters[key] = parameter # collect independently specified parameter examples from @extend_schema. # Append to both discovered and manually specified parameters. examples_by_key = defaultdict(list) for example in self.get_examples(): if example.parameter_only: examples_by_key[example.parameter_only].append(example) for key, examples in examples_by_key.items(): if key in parameters: parameters[key].setdefault('examples', {}) parameters[key]['examples'].update(build_examples_list(examples)) if callable(spectacular_settings.SORT_OPERATION_PARAMETERS): return sorted(parameters.values(), key=spectacular_settings.SORT_OPERATION_PARAMETERS) elif spectacular_settings.SORT_OPERATION_PARAMETERS: return sorted(parameters.values(), key=lambda p: p['name']) else: return list(parameters.values()) def get_description(self) -> str: # type: ignore[override] """ override this for custom behaviour """ action_or_method = getattr(self.view, getattr(self.view, 'action', self.method.lower()), None) view_doc = get_doc(self.view.__class__) action_doc = get_doc(action_or_method) return action_doc or view_doc def get_summary(self) -> Optional[str]: """ override this for custom behaviour """ return None def _get_external_docs(self) -> Optional[Dict[str, str]]: external_docs = self.get_external_docs() if isinstance(external_docs, str): return {'url': external_docs} else: return external_docs def get_external_docs(self) -> Optional[Union[Dict[str, str], str]]: """ override this for custom behaviour """ return None def get_auth(self) -> List[_SchemaType]: """ Obtains authentication classes and permissions from view. If authentication is known, resolve security requirement for endpoint and security definition for the component section. For custom authentication subclass ``OpenApiAuthenticationExtension``. """ auths = [] for authenticator in self.view.get_authenticators(): if not whitelisted(authenticator, spectacular_settings.AUTHENTICATION_WHITELIST, True): continue scheme = OpenApiAuthenticationExtension.get_match(authenticator) if not scheme: warn( f'could not resolve authenticator {authenticator.__class__}. There ' f'was no OpenApiAuthenticationExtension registered for that class. ' f'Try creating one by subclassing it. Ignoring for now.' ) continue security_requirements = scheme.get_security_requirement(self) if security_requirements is not None: if isinstance(security_requirements, dict): auths.append(security_requirements) else: auths.extend(security_requirements) if isinstance(scheme.name, str): names, definitions = [scheme.name], [scheme.get_security_definition(self)] else: names, definitions = scheme.name, scheme.get_security_definition(self) # type: ignore[assignment] for name, definition in zip(names, definitions): self.registry.register_on_missing( ResolvedComponent( name=name, type=ResolvedComponent.SECURITY_SCHEMA, object=authenticator.__class__, schema=definition ) ) if spectacular_settings.SECURITY: auths.extend(spectacular_settings.SECURITY) perms = [p.__class__ for p in self.view.get_permissions()] if permissions.AllowAny in perms: auths.append({}) elif permissions.IsAuthenticatedOrReadOnly in perms and self.method in permissions.SAFE_METHODS: auths.append({}) return auths def get_request_serializer(self) -> Optional[_SerializerType]: """ override this for custom behaviour """ return self._get_serializer() def get_response_serializers(self) -> Optional[_SerializerType]: """ override this for custom behaviour """ return self._get_serializer() def get_tags(self) -> List[str]: """ override this for custom behaviour """ tokenized_path = self._tokenize_path() # use first non-parameter path part as tag return tokenized_path[:1] def get_extensions(self) -> _SchemaType: return {} def _get_callbacks(self): """ Creates a mocked view for every callback. The given extend_schema decorator then specifies the expectations on the receiving end of the callback. Effectively simulates a sub-schema from the opposing perspective via a virtual view definition. """ result = {} for callback in self.get_callbacks(): if isinstance(callback.decorator, dict): methods = callback.decorator else: methods = {'post': callback.decorator} path_items = {} for method, decorator in methods.items(): # a dict indicates a raw schema; use directly if isinstance(decorator, dict): path_items[method.lower()] = decorator continue mocked_view = build_mocked_view( method=method, path=callback.path, extend_schema_decorator=decorator, registry=self.registry ) operation = {} description = mocked_view.schema.get_description() if description: operation['description'] = description summary = mocked_view.schema.get_summary() if summary: operation['summary'] = summary request_body = mocked_view.schema._get_request_body('response') if request_body: operation['requestBody'] = request_body deprecated = mocked_view.schema.is_deprecated() if deprecated: operation['deprecated'] = deprecated operation['responses'] = mocked_view.schema._get_response_bodies('request') extensions = mocked_view.schema.get_extensions() if extensions: operation.update(sanitize_specification_extensions(extensions)) path_items[method.lower()] = operation result[callback.name] = {callback.path: path_items} return result def get_callbacks(self) -> List[OpenApiCallback]: """ override this for custom behaviour """ return [] def get_operation_id(self) -> str: """ override this for custom behaviour """ tokenized_path = self._tokenize_path() # replace dashes as they can be problematic later in code generation tokenized_path = [t.replace('-', '_') for t in tokenized_path] if self.method == 'GET' and self._is_list_view(): action = 'list' else: action = self.method_mapping[self.method.lower()] if not tokenized_path: tokenized_path.append('root') if re.search(r'', self.path_regex): tokenized_path.append('formatted') return '_'.join(tokenized_path + [action]) def is_deprecated(self) -> bool: """ override this for custom behaviour """ return False def _tokenize_path(self) -> List[str]: # remove path prefix path = re.sub( pattern=self.path_prefix, repl='', string=self.path, flags=re.IGNORECASE ) # remove path variables path = re.sub(pattern=r'\{[\w\-]+\}', repl='', string=path) # cleanup and tokenize remaining parts. tokenized_path = path.rstrip('/').lstrip('/').split('/') return [t for t in tokenized_path if t] def _resolve_path_parameters(self, variables): model = get_view_model(self.view, emit_warnings=False) parameters = [] for variable in variables: schema = build_basic_type(OpenApiTypes.STR) description = '' resolved_parameter = resolve_django_path_parameter( self.path_regex, variable, self.map_renderers('format'), ) if not resolved_parameter: resolved_parameter = resolve_regex_path_parameter(self.path_regex, variable) if resolved_parameter: schema = resolved_parameter['schema'] elif model is None: warn( f'could not derive type of path parameter "{variable}" because it ' f'is untyped and obtaining queryset from the viewset failed. ' f'Consider adding a type to the path (e.g. ) or annotating ' f'the parameter type with @extend_schema. Defaulting to "string".' ) else: try: if getattr(self.view, 'lookup_url_kwarg', None) == variable: model_field_name = getattr(self.view, 'lookup_field', variable) elif variable.endswith('_pk'): # Django naturally coins foreign keys *_id. improve chances to match a field model_field_name = f'{variable[:-3]}_id' else: model_field_name = variable model_field = follow_model_field_lookup(model, model_field_name) schema = self._map_model_field(model_field, direction=None) if 'description' not in schema and model_field.primary_key: description = get_pk_description(model, model_field) except django_exceptions.FieldError: warn( f'could not derive type of path parameter "{variable}" because model ' f'"{model.__module__}.{model.__name__}" contained no such field. Consider ' f'annotating parameter with @extend_schema. Defaulting to "string".' ) parameters.append(build_parameter_type( name=variable, location=OpenApiParameter.PATH, description=description, schema=schema )) return parameters def get_filter_backends(self) -> List[Any]: """ override this for custom behaviour """ if not self._is_list_view(): return [] return getattr(self.view, 'filter_backends', []) def _get_filter_parameters(self): parameters = [] for filter_backend in self.get_filter_backends(): filter_extension = OpenApiFilterExtension.get_match(filter_backend()) if filter_extension: parameters += filter_extension.get_schema_operation_parameters(self) else: parameters += filter_backend().get_schema_operation_parameters(self.view) return parameters def _get_pagination_parameters(self): if not self._is_list_view(): return [] paginator = self._get_paginator() if not paginator: return [] filter_extension = OpenApiFilterExtension.get_match(paginator) if filter_extension: return filter_extension.get_schema_operation_parameters(self) else: return paginator.get_schema_operation_parameters(self.view) def _map_model_field(self, model_field, direction): assert isinstance(model_field, models.Field) # to get a fully initialized serializer field we use DRF's own init logic try: field_cls, field_kwargs = serializers.ModelSerializer().build_field( field_name=model_field.name, info=get_field_info(model_field.model), model_class=model_field.model, nested_depth=0, ) field = field_cls(**field_kwargs) field.field_name = model_field.name except: # noqa field = None # For some cases, the DRF init logic either breaks (custom field with internal type) or # the resulting field is underspecified with regards to the schema (ReadOnlyField). if field and isinstance(field, serializers.PrimaryKeyRelatedField): # special case handling only for _resolve_path_parameters() where neither queryset nor # parent is set by build_field. patch in queryset as _map_serializer_field requires it if not field.queryset: field.queryset = get_manager(model_field.related_model).none() return self._map_serializer_field(field, direction) elif isinstance(field, serializers.ManyRelatedField): # special case handling similar to the case above. "parent.parent" on child_relation # is None and there is no queryset. patch in as _map_serializer_field requires one. if not field.child_relation.queryset: field.child_relation.queryset = get_manager(model_field.related_model).none() return self._map_serializer_field(field, direction) elif field and not isinstance(field, (serializers.ReadOnlyField, serializers.ModelField)): return self._map_serializer_field(field, direction) elif isinstance(model_field, models.ForeignKey): return self._map_model_field(model_field.target_field, direction) elif hasattr(models, 'JSONField') and isinstance(model_field, models.JSONField): # fix for DRF==3.11 with django>=3.1 as it is not yet represented in the field_mapping return build_basic_type(OpenApiTypes.ANY) elif isinstance(model_field, models.BinaryField): return build_basic_type(OpenApiTypes.BYTE) elif hasattr(models, model_field.get_internal_type()): # be graceful when the model field is not explicitly mapped to a serializer internal_type = getattr(models, model_field.get_internal_type()) field_cls = serializers.ModelSerializer.serializer_field_mapping.get(internal_type) if not field_cls: warn( f'model field "{model_field.get_internal_type()}" has no mapping in ' f'ModelSerializer. It may be a deprecated field. Defaulting to "string"' ) return build_basic_type(OpenApiTypes.STR) return self._map_serializer_field(field_cls(), direction) else: error( f'could not resolve model field "{model_field}". Failed to resolve through ' f'serializer_field_mapping, get_internal_type(), or any override mechanism. ' f'Defaulting to "string"' ) return build_basic_type(OpenApiTypes.STR) def _map_serializer_field(self, field, direction, bypass_extensions=False): meta = self._get_serializer_field_meta(field, direction) if has_override(field, 'field'): override = get_override(field, 'field') if is_basic_type(override): schema = build_basic_type(override) if schema is None: return None elif isinstance(override, dict): schema = override else: schema = self._map_serializer_field(force_instance(override), direction) field_component_name = get_override(field, 'field_component_name') if field_component_name: component = ResolvedComponent( name=field_component_name, type=ResolvedComponent.SCHEMA, schema=schema, object=field, ) self.registry.register_on_missing(component) return append_meta(component.ref, meta) else: return append_meta(schema, meta) serializer_field_extension = OpenApiSerializerFieldExtension.get_match(field) if serializer_field_extension and not bypass_extensions: schema = serializer_field_extension.map_serializer_field(self, direction) if schema is None: return None elif serializer_field_extension.get_name(): component = ResolvedComponent( name=serializer_field_extension.get_name(), type=ResolvedComponent.SCHEMA, schema=schema, object=field, ) self.registry.register_on_missing(component) return append_meta(component.ref, meta) else: return append_meta(schema, meta) # nested serializer with many=True gets automatically replaced with ListSerializer if is_list_serializer(field): schema = self._unwrap_list_serializer(field, direction) return append_meta(schema, meta) if schema else None # nested serializer if is_serializer(field): component = self.resolve_serializer(field, direction) return append_meta(component.ref, meta) if component else None # Related fields. if isinstance(field, serializers.ManyRelatedField): schema = self._map_serializer_field(field.child_relation, direction) # remove hand-over initkwargs applying only to outer scope schema.pop('readOnly', None) if meta.get('description') == schema.get('description'): schema.pop('description', None) return append_meta(build_array_type(schema), meta) if isinstance(field, (serializers.PrimaryKeyRelatedField, serializers.SlugRelatedField)): # SlugRelatedField is essentially a non-pk version of PrimaryKeyRelatedField. is_slug = isinstance(field, serializers.SlugRelatedField) # read_only fields do not have a Manager by design. go around and get field # from parent. also avoid calling Manager. __bool__ as it might be customized # to hit the database. if getattr(field, 'queryset', None) is not None: if is_slug: model = field.queryset.model source = [field.slug_field] model_field = follow_field_source(model, source, default=models.TextField()) else: model_field = field.queryset.model._meta.pk else: if isinstance(field.parent, serializers.ManyRelatedField): model = field.parent.parent.Meta.model source = field.parent.source.split('.') elif hasattr(field.parent, 'Meta'): model = field.parent.Meta.model source = field.source.split('.') else: warn( f'Could not derive type for under-specified {field.__class__.__name__} ' f'"{field.field_name}". The serializer has no associated model (Meta class) ' f'and this particular field has no type without a model association. Consider ' f'changing the field or adding a Meta class. defaulting to string.' ) return append_meta(build_basic_type(OpenApiTypes.STR), meta) if is_slug: source.append(field.slug_field) # estimates the relating model field and jumps to its target model PK field. # also differentiate as source can be direct (pk) or relation field (model). # be graceful and default to string. model_field = follow_field_source(model, source, default=models.TextField()) # Special case: SlugRelatedField also allows to point to a callable @property. if callable(model_field): schema = self._map_response_type_hint(model_field) elif isinstance(model_field, models.Field): schema = self._map_model_field(model_field, direction) else: assert False, f'Field "{field.field_name}" must point to either a property or a model field.' # primary keys are usually non-editable (readOnly=True) and map_model_field correctly # signals that attribute. however this does not apply in the context of relations. schema.pop('readOnly', None) return append_meta(schema, meta) if isinstance(field, serializers.StringRelatedField): return append_meta(build_basic_type(OpenApiTypes.STR), meta) if isinstance(field, serializers.HyperlinkedIdentityField): return append_meta(build_basic_type(OpenApiTypes.URI), meta) if isinstance(field, serializers.HyperlinkedRelatedField): return append_meta(build_basic_type(OpenApiTypes.URI), meta) if isinstance(field, serializers.MultipleChoiceField): return append_meta(build_array_type(build_choice_field(field)), meta) if isinstance(field, serializers.ChoiceField): schema = build_choice_field(field) if 'description' in meta and 'description' in schema: meta['description'] = meta['description'] + '\n\n' + schema.pop('description') return append_meta(schema, meta) if isinstance(field, serializers.ListField): if isinstance(field.child, _UnvalidatedField): return append_meta(build_array_type(build_basic_type(OpenApiTypes.ANY)), meta) elif is_basic_serializer(field.child): component = self.resolve_serializer(field.child, direction) return append_meta(build_array_type(component.ref), meta) if component else None else: schema = self._map_serializer_field(field.child, direction) self._insert_field_validators(field.child, schema) # remove automatically attached but redundant title if is_trivial_string_variation(field.field_name, schema.get('title')): schema.pop('title', None) return append_meta(build_array_type(schema), meta) # DateField and DateTimeField type is string if isinstance(field, serializers.DateField): return append_meta(build_basic_type(OpenApiTypes.DATE), meta) if isinstance(field, serializers.DateTimeField): return append_meta(build_basic_type(OpenApiTypes.DATETIME), meta) if isinstance(field, serializers.TimeField): return append_meta(build_basic_type(OpenApiTypes.TIME), meta) if isinstance(field, serializers.EmailField): return append_meta(build_basic_type(OpenApiTypes.EMAIL), meta) if isinstance(field, serializers.URLField): return append_meta(build_basic_type(OpenApiTypes.URI), meta) if isinstance(field, serializers.UUIDField): return append_meta(build_basic_type(OpenApiTypes.UUID), meta) if isinstance(field, serializers.DurationField): return append_meta(build_basic_type(OpenApiTypes.STR), meta) if isinstance(field, serializers.IPAddressField): # TODO this might be a DRF bug. protocol is not propagated to serializer although it # should have been. results in always 'both' (thus no format) if 'ipv4' == field.protocol.lower(): schema = build_basic_type(OpenApiTypes.IP4) elif 'ipv6' == field.protocol.lower(): schema = build_basic_type(OpenApiTypes.IP6) else: schema = build_basic_type(OpenApiTypes.STR) return append_meta(schema, meta) # DecimalField has multipleOf based on decimal_places if isinstance(field, serializers.DecimalField): if getattr(field, 'coerce_to_string', api_settings.COERCE_DECIMAL_TO_STRING): content = {**build_basic_type(OpenApiTypes.STR), 'format': 'decimal'} if field.max_whole_digits: content['pattern'] = ( fr'^-?\d{{0,{field.max_whole_digits}}}' fr'(?:\.\d{{0,{field.decimal_places}}})?$' ) else: content = build_basic_type(OpenApiTypes.DECIMAL) if field.max_whole_digits: value = 10 ** field.max_whole_digits content.update({ 'maximum': value, 'minimum': -value, 'exclusiveMaximum': True, 'exclusiveMinimum': True, }) self._insert_min_max(field, content) return append_meta(content, meta) if isinstance(field, serializers.FloatField): content = build_basic_type(OpenApiTypes.DOUBLE) self._insert_min_max(field, content) return append_meta(content, meta) if isinstance(field, serializers.IntegerField): content = build_basic_type(OpenApiTypes.INT) self._insert_min_max(field, content) # Use int64 for format if value outside the 32-bit signed integer range [-2,147,483,648 to 2,147,483,647]. if not all(-2147483648 <= int(content.get(key, 0)) <= 2147483647 for key in ('maximum', 'minimum')): content['format'] = 'int64' return append_meta(content, meta) if isinstance(field, serializers.FileField): if spectacular_settings.COMPONENT_SPLIT_REQUEST and direction == 'request': content = build_basic_type(OpenApiTypes.BINARY) else: use_url = getattr(field, 'use_url', api_settings.UPLOADED_FILES_USE_URL) content = build_basic_type(OpenApiTypes.URI if use_url else OpenApiTypes.STR) return append_meta(content, meta) if isinstance(field, serializers.SerializerMethodField): method = getattr(field.parent, field.method_name, None) if method is None: error( f'SerializerMethodField "{field.field_name}" is missing required method ' f'"{field.method_name}". defaulting to "string".' ) return append_meta(build_basic_type(OpenApiTypes.STR), meta) return append_meta(self._map_response_type_hint(method), meta) # NullBooleanField was removed in 3.14. Since 3.12.0 NullBooleanField was a subclass of BooleanField if hasattr(serializers, "NullBooleanField"): boolean_field_classes = (serializers.BooleanField, serializers.NullBooleanField) else: boolean_field_classes = (serializers.BooleanField,) if isinstance(field, boolean_field_classes): return append_meta(build_basic_type(OpenApiTypes.BOOL), meta) if isinstance(field, serializers.JSONField): return append_meta(build_basic_type(OpenApiTypes.ANY), meta) if isinstance(field, (serializers.DictField, serializers.HStoreField)): content = build_basic_type(OpenApiTypes.OBJECT) if not isinstance(field.child, _UnvalidatedField): content['additionalProperties'] = self._map_serializer_field(field.child, direction) self._insert_field_validators(field.child, content['additionalProperties']) return append_meta(content, meta) if isinstance(field, serializers.CharField): return append_meta(build_basic_type(OpenApiTypes.STR), meta) if isinstance(field, serializers.ReadOnlyField): # when field is nested inside a ListSerializer, the Meta class is 2 steps removed if is_list_serializer(field.parent): model = getattr(getattr(field.parent.parent, 'Meta', None), 'model', None) source = field.parent.source_attrs else: model = getattr(getattr(field.parent, 'Meta', None), 'model', None) source = field.source_attrs if model is None: warn( f'Could not derive type for ReadOnlyField "{field.field_name}" because the ' f'serializer class has no associated model (Meta class). Consider using some ' f'other field like CharField(read_only=True) instead. defaulting to string.' ) return append_meta(build_basic_type(OpenApiTypes.STR), meta) target = follow_field_source(model, source) if callable(target): schema = self._map_response_type_hint(target) elif isinstance(target, models.Field): schema = self._map_model_field(target, direction) else: assert False, f'ReadOnlyField target "{field}" must be property or model field' return append_meta(schema, meta) # DRF was not able to match the model field to an explicit SerializerField and therefore # used its generic fallback serializer field that simply wraps the model field. if isinstance(field, serializers.ModelField): schema = self._map_model_field(field.model_field, direction) return append_meta(schema, meta) warn(f'could not resolve serializer field "{field}". Defaulting to "string"') return append_meta(build_basic_type(OpenApiTypes.STR), meta) def _insert_min_max(self, field: Any, content: _SchemaType) -> None: if field.max_value is not None: content['maximum'] = field.max_value if 'exclusiveMaximum' in content: del content['exclusiveMaximum'] if field.min_value is not None: content['minimum'] = field.min_value if 'exclusiveMinimum' in content: del content['exclusiveMinimum'] def _map_serializer(self, serializer, direction, bypass_extensions=False): serializer = force_instance(serializer) serializer_extension = OpenApiSerializerExtension.get_match(serializer) if serializer_extension and not bypass_extensions: schema = serializer_extension.map_serializer(self, direction) else: schema = self._map_basic_serializer(serializer, direction) extensions = get_override(serializer, 'extensions', {}) if extensions: schema.update(sanitize_specification_extensions(extensions)) return self._postprocess_serializer_schema(schema, serializer, direction) def _postprocess_serializer_schema(self, schema, serializer, direction): """ postprocess generated schema for component splitting, if enabled. does only apply to direct component schemas and not intermediate schemas like components composed of sub-component via e.g. oneOf. """ if not spectacular_settings.COMPONENT_SPLIT_REQUEST: return schema properties = schema.get('properties', []) required = schema.get('required', []) for prop_name in list(properties): if direction == 'request' and properties[prop_name].get('readOnly'): del schema['properties'][prop_name] if prop_name in required: required.remove(prop_name) if direction == 'response' and properties[prop_name].get('writeOnly'): del schema['properties'][prop_name] if prop_name in required: required.remove(prop_name) # remove empty listing as it violates schema specification if 'required' in schema and not required: del schema['required'] return schema def _get_serializer_field_meta(self, field, direction): if not isinstance(field, serializers.Field): return {} meta = {} if field.read_only: meta['readOnly'] = True if field.write_only: meta['writeOnly'] = True if field.allow_null: # this will be converted later in case of OAS 3.1 meta['nullable'] = True if isinstance(field, serializers.CharField) and not field.allow_blank: # blank check only applies to inbound requests if spectacular_settings.COMPONENT_SPLIT_REQUEST: if direction == 'request': meta['minLength'] = 1 elif spectacular_settings.ENFORCE_NON_BLANK_FIELDS: if not field.read_only: meta['minLength'] = 1 if field.default is not None and field.default != empty and not callable(field.default): if isinstance( field, ( serializers.ModelField, serializers.SerializerMethodField, serializers.PrimaryKeyRelatedField, serializers.SlugRelatedField, ), ): # Skip coercion for lack of a better solution. These are special in that they require # a model instance or object (which we don't have) instead of a plain value. default = field.default else: try: # gracefully attempt to transform value or just use as plain on error default = field.to_representation(field.default) except: # noqa: E722 default = field.default if isinstance(default, set): default = list(default) meta['default'] = default if field.label and not is_trivial_string_variation(field.label, field.field_name): meta['title'] = str(field.label) if field.help_text: meta['description'] = str(field.help_text) return meta def _map_basic_serializer(self, serializer, direction): assert_basic_serializer(serializer) serializer = force_instance(serializer) # serializers provided through @extend_schema will not receive the mock context # via _get_serializer(). Establish behavioral symmetry for those use-cases. if not serializer.context: serializer.context.update(build_serializer_context(self.view)) required = set() properties = {} for field in serializer.fields.values(): if isinstance(field, serializers.HiddenField): continue if field.field_name in get_override(serializer, 'exclude_fields', []): continue schema = self._map_serializer_field(field, direction) # skip field if there is no schema for the direction if schema is None: continue add_to_required = ( field.required or (schema.get('readOnly') and not spectacular_settings.COMPONENT_NO_READ_ONLY_REQUIRED) ) if add_to_required: required.add(field.field_name) self._insert_field_validators(field, schema) if field.field_name in get_override(serializer, 'deprecate_fields', []): schema['deprecated'] = True properties[field.field_name] = safe_ref(schema) if is_patched_serializer(serializer, direction): required = [] return build_object_type( properties=properties, required=required, description=get_doc(serializer.__class__), ) def _insert_field_validators(self, field, schema): schema_type = schema.get('type') def update_constraint(schema, key, function, value, *, exclusive=False): if callable(value): value = value() current_value = schema.get(key) if current_value is not None: new_value = function(current_value, value) else: new_value = value schema[key] = new_value if key in ('maximum', 'minimum'): exclusive_key = f'exclusive{key.title()}' if exclusive: if new_value != current_value: schema[exclusive_key] = True elif exclusive_key in schema: del schema[exclusive_key] for v in field.validators: if schema_type == 'string': if isinstance(v, validators.EmailValidator): if 'format' not in schema: schema['format'] = 'email' elif isinstance(v, validators.URLValidator): if 'format' not in schema: schema['format'] = 'uri' elif isinstance(v, validators.RegexValidator): if 'pattern' not in schema: pattern = v.regex.pattern.encode('ascii', 'backslashreplace').decode() pattern = pattern.replace(r'\x', r'\u00') # unify escaping pattern = pattern.replace(r'\Z', '$').replace(r'\A', '^') # ECMA anchors schema['pattern'] = pattern elif isinstance(v, validators.MaxLengthValidator): update_constraint(schema, 'maxLength', min, v.limit_value) elif isinstance(v, validators.MinLengthValidator): update_constraint(schema, 'minLength', max, v.limit_value) elif isinstance(v, validators.FileExtensionValidator) and v.allowed_extensions: if 'pattern' not in schema: schema['pattern'] = '(?:%s)$' % '|'.join([re.escape(extn) for extn in v.allowed_extensions]) elif schema_type in ('integer', 'number'): if isinstance(v, validators.MaxValueValidator): update_constraint(schema, 'maximum', min, v.limit_value) elif isinstance(v, validators.MinValueValidator): update_constraint(schema, 'minimum', max, v.limit_value) elif isinstance(v, validators.DecimalValidator) and v.max_digits: value = 10 ** (v.max_digits - (v.decimal_places or 0)) update_constraint(schema, 'maximum', min, value, exclusive=True) update_constraint(schema, 'minimum', max, -value, exclusive=True) elif schema_type == 'array': if isinstance(v, validators.MaxLengthValidator): update_constraint(schema, 'maxItems', min, v.limit_value) elif isinstance(v, validators.MinLengthValidator): update_constraint(schema, 'minItems', max, v.limit_value) elif schema_type == 'object': if isinstance(v, validators.MaxLengthValidator): update_constraint(schema, 'maxProperties', min, v.limit_value) elif isinstance(v, validators.MinLengthValidator): update_constraint(schema, 'minProperties', max, v.limit_value) def _map_response_type_hint(self, method): hint = get_override(method, 'field') or get_type_hints(method).get('return') if is_serializer(hint) or is_field(hint): return self._map_serializer_field(force_instance(hint), 'response') if isinstance(hint, dict): return hint try: schema = resolve_type_hint(hint) except UnableToProceedError: warn( f'unable to resolve type hint for function "{method.__name__}". Consider ' f'using a type hint or @extend_schema_field. Defaulting to string.' ) return build_basic_type(OpenApiTypes.STR) description = get_doc( method.func if isinstance(method, functools.partial) else method ) if description: schema['description'] = description return schema def _get_paginator(self): pagination_class = getattr(self.view, 'pagination_class', None) if pagination_class: return pagination_class() return None def get_paginated_name(self, serializer_name: str) -> str: return f'Paginated{serializer_name}List' def map_parsers(self) -> List[Any]: return list(dict.fromkeys([ p.media_type for p in self.view.get_parsers() if whitelisted(p, spectacular_settings.PARSER_WHITELIST) ])) def map_renderers(self, attribute: str) -> List[Any]: assert attribute in ['media_type', 'format'] # Either use whitelist or default back to old behavior by excluding BrowsableAPIRenderer def use_renderer(r): if spectacular_settings.RENDERER_WHITELIST is not None: return whitelisted(r, spectacular_settings.RENDERER_WHITELIST) else: return not isinstance(r, renderers.BrowsableAPIRenderer) return list(dict.fromkeys([ getattr(r, attribute).split(';')[0] for r in self.view.get_renderers() if use_renderer(r) and hasattr(r, attribute) ])) def _get_serializer(self): view = self.view context = build_serializer_context(view) try: if isinstance(view, GenericAPIView): # try to circumvent queryset issues with calling get_serializer. if view has NOT # overridden get_serializer, its safe to use get_serializer_class. if view.__class__.get_serializer == GenericAPIView.get_serializer: return view.get_serializer_class()(context=context) return view.get_serializer(context=context) elif isinstance(view, APIView): # APIView does not implement the required interface, but be lenient and make # good guesses before giving up and emitting a warning. if callable(getattr(view, 'get_serializer', None)): return view.get_serializer(context=context) elif callable(getattr(view, 'get_serializer_class', None)): return view.get_serializer_class()(context=context) elif hasattr(view, 'serializer_class'): return view.serializer_class else: error( 'unable to guess serializer. This is graceful fallback handling for APIViews. ' 'Consider using GenericAPIView as view base class, if view is under your control. ' 'Either way you may want to add a serializer_class (or method). Ignoring view for now.' ) else: error('Encountered unknown view base class. Please report this issue. Ignoring for now') except Exception as exc: error( f'exception raised while getting serializer. Hint: ' f'Is get_serializer_class() returning None or is get_queryset() not working without ' f'a request? Ignoring the view for now. (Exception: {exc})' ) def get_examples(self) -> List[OpenApiExample]: """ override this for custom behaviour """ return [] def _get_examples(self, serializer, direction, media_type, status_code=None, extras=None): """ Handles examples for request/response. purposefully ignores parameter examples """ # don't let the parameter examples influence the serializer example retrieval examples = [e for e in self.get_examples() if not e.parameter_only] # Examples from Serializers via @extend_schema_serializer are only considered, if # there were no higher priority examples directly from view annotation. if not examples: if is_list_serializer(serializer): examples = get_override(serializer.child, 'examples', []) elif is_serializer(serializer): examples = get_override(serializer, 'examples', []) # additional examples provided via OpenApiResponse/Request are merged with the other methods extras = extras or [] filtered_examples = [] for example in examples + extras: if direction == 'request' and example.response_only: continue if direction == 'response' and example.request_only: continue # default to 'application/json' unless nested in OpenApiResponse, in which case inherit if not example.media_type: example_media_type = media_type if (example in extras) else 'application/json' else: example_media_type = example.media_type if media_type and media_type != example_media_type: continue # default to [200, 201] unless nested in OpenApiResponse, in which case inherit if not example.status_codes: example_status_codes = (status_code,) if (example in extras) else ('200', '201') else: example_status_codes = tuple(map(str, example.status_codes)) if status_code and status_code not in example_status_codes: continue if ( self._is_list_view(serializer) and get_override(serializer, 'many') is not False and ( direction == 'request' or '200' <= status_code < '300' or spectacular_settings.ENABLE_LIST_MECHANICS_ON_NON_2XX ) ): # contain modification to this context so "listing" is not propagated elsewhere example = copy.copy(example) example.value = build_listed_example_value(example.value, self._get_paginator(), direction) filtered_examples.append(example) return build_examples_list(filtered_examples) def _get_request_body(self, direction='request'): # only unsafe methods can have a body if self.method not in ('PUT', 'PATCH', 'POST'): return None request_serializer = self.get_request_serializer() request_body_required = True content = [] # either implicit media-types via available parsers or manual list via decoration if isinstance(request_serializer, dict): media_types_iter = request_serializer.items() else: media_types_iter = zip(self.map_parsers(), itertools.repeat(request_serializer)) for media_type, serializer in media_types_iter: if isinstance(serializer, OpenApiRequest): serializer, examples, encoding = serializer.request, serializer.examples, serializer.encoding else: encoding, examples = None, None if ( encoding and media_type != "application/x-www-form-urlencoded" and not media_type.startswith('multipart') ): warn( 'Encodings object on media types other than "application/x-www-form-urlencoded" ' 'or "multipart/*" have undefined behavior.' ) examples = self._get_examples(serializer, direction, media_type, None, examples) schema, partial_request_body_required = self._get_request_for_media_type(serializer, direction) if schema is not None: content.append((media_type, schema, examples, encoding)) request_body_required &= partial_request_body_required if not content: return None request_body = { 'content': { media_type: build_media_type_object(schema, examples, encoding) for media_type, schema, examples, encoding in content } } if request_body_required: request_body['required'] = request_body_required return request_body def _get_request_for_media_type(self, serializer, direction='request'): serializer = force_instance(serializer) if is_list_serializer(serializer): schema = self._unwrap_list_serializer(serializer, direction) request_body_required = bool(schema) elif is_serializer(serializer): if self.method == 'PATCH': # we simulate what DRF is doing: the entry serializer is set to partial # for PATCH requests. serializer instances received via extend_schema # may be reused; prevent race conditions by modifying a copy. serializer = copy.copy(serializer) serializer.partial = True component = self.resolve_serializer(serializer, direction) if not component: # serializer is empty so skip content enumeration return None, False schema = component.ref # request body is only required if any required property is not read-only readonly_props = [ p for p, s in component.schema.get('properties', {}).items() if s.get('readOnly') ] required_props = component.schema.get('required', []) request_body_required = any(req not in readonly_props for req in required_props) elif is_basic_type(serializer): schema = build_basic_type(serializer) request_body_required = False elif isinstance(serializer, dict): # bypass processing and use given schema directly schema = serializer request_body_required = False else: warn( f'could not resolve request body for {self.method} {self.path}. Defaulting to generic ' 'free-form object. (Maybe annotate a Serializer class?)' ) schema = build_generic_type() schema['description'] = 'Unspecified request body' request_body_required = False return schema, request_body_required def _get_response_bodies(self, direction: Direction = 'response') -> _SchemaType: response_serializers = self.get_response_serializers() if ( is_serializer(response_serializers) or is_basic_type(response_serializers) or isinstance(response_serializers, OpenApiResponse) ): if self.method == 'DELETE': return {'204': {'description': _('No response body')}} if self._is_create_operation(): return {'201': self._get_response_for_code(response_serializers, '201', direction=direction)} return {'200': self._get_response_for_code(response_serializers, '200', direction=direction)} elif isinstance(response_serializers, dict): # custom handling for overriding default return codes with @extend_schema responses = {} for code, serializer in response_serializers.items(): if isinstance(code, tuple): code, media_types = str(code[0]), code[1:] else: code, media_types = str(code), None content_response = self._get_response_for_code(serializer, code, media_types, direction) if code in responses: responses[code]['content'].update(content_response['content']) else: responses[code] = content_response return responses else: warn( f'could not resolve "{response_serializers}" for {self.method} {self.path}. ' f'Expected either a serializer or some supported override mechanism. ' f'Defaulting to generic free-form object.' ) schema = build_basic_type(OpenApiTypes.OBJECT) schema['description'] = _('Unspecified response body') # type: ignore return {'200': self._get_response_for_code(schema, '200', direction=direction)} def _unwrap_list_serializer(self, serializer, direction: Direction) -> Optional[_SchemaType]: if is_field(serializer): return self._map_serializer_field(serializer, direction) elif is_basic_serializer(serializer): component = self.resolve_serializer(serializer, direction) return component.ref if component else None elif is_list_serializer(serializer): result = self._unwrap_list_serializer(serializer.child, direction) return build_array_type(result) if result else None else: assert False, 'Serializer is of unknown type.' def _get_response_for_code(self, serializer, status_code, media_types=None, direction='response'): if isinstance(serializer, OpenApiResponse): serializer, description, examples = ( serializer.response, serializer.description, serializer.examples ) else: description, examples = '', [] serializer = force_instance(serializer) headers = self._get_response_headers_for_code(status_code, direction) headers = {'headers': headers} if headers else {} if not serializer: return {**headers, 'description': description or _('No response body')} elif is_list_serializer(serializer): schema = self._unwrap_list_serializer(serializer.child, direction) if not schema: return {**headers, 'description': description or _('No response body')} elif is_serializer(serializer): component = self.resolve_serializer(serializer, direction) if not component: return {**headers, 'description': description or _('No response body')} schema = component.ref elif is_basic_type(serializer): schema = build_basic_type(serializer) elif isinstance(serializer, dict): # bypass processing and use given schema directly schema = serializer # prevent invalid dict case in _is_list_view() as this not a status_code dict. serializer = None else: warn( f'could not resolve "{serializer}" for {self.method} {self.path}. Expected either ' f'a serializer or some supported override mechanism. Defaulting to ' f'generic free-form object.' ) schema = build_basic_type(OpenApiTypes.OBJECT) schema['description'] = _('Unspecified response body') if ( self._is_list_view(serializer) and get_override(serializer, 'many') is not False and ('200' <= status_code < '300' or spectacular_settings.ENABLE_LIST_MECHANICS_ON_NON_2XX) ): # In case of a non-default ListSerializer, check for matching extension and # bypass regular list wrapping by delegating handling to extension. if ( is_list_serializer_customized(serializer) and OpenApiSerializerExtension.get_match(get_list_serializer(serializer)) ): schema = self._map_serializer(get_list_serializer(serializer), direction) else: schema = build_array_type(schema) paginator = self._get_paginator() if ( paginator and is_serializer(serializer) and (not is_list_serializer(serializer) or is_serializer(serializer.child)) ): paginated_name = self.get_paginated_name(self._get_serializer_name(serializer, "response")) component = ResolvedComponent( name=paginated_name, type=ResolvedComponent.SCHEMA, schema=paginator.get_paginated_response_schema(schema), object=serializer.child if is_list_serializer(serializer) else serializer, ) self.registry.register_on_missing(component) schema = component.ref elif paginator: schema = paginator.get_paginated_response_schema(schema) if not media_types: media_types = self.map_renderers('media_type') media_types = modify_media_types_for_versioning(self.view, media_types) return { **headers, 'content': { media_type: build_media_type_object( schema, self._get_examples(serializer, direction, media_type, status_code, examples) ) for media_type in media_types }, 'description': description } def _get_response_headers_for_code(self, status_code, direction='response') -> _SchemaType: result = {} for parameter in self.get_override_parameters(): if not isinstance(parameter, OpenApiParameter): continue if not parameter.response: continue if ( isinstance(parameter.response, list) and status_code not in [str(code) for code in parameter.response] ): continue if is_basic_type(parameter.type): schema = build_basic_type(parameter.type) elif is_serializer(parameter.type): schema = self.resolve_serializer(parameter.type, direction).ref else: schema = parameter.type # type: ignore if not schema: warn(f'response parameter {parameter.name} requires non-empty schema') continue if parameter.location not in [OpenApiParameter.HEADER, OpenApiParameter.COOKIE]: warn(f'incompatible location type ignored for response parameter {parameter.name}') parameter_type = build_parameter_type( name=parameter.name, schema=schema, location=parameter.location, required=parameter.required, description=parameter.description, enum=parameter.enum, pattern=parameter.pattern, deprecated=parameter.deprecated, style=parameter.style, explode=parameter.explode, default=parameter.default, allow_blank=parameter.allow_blank, examples=build_examples_list(parameter.examples), extensions=parameter.extensions, ) del parameter_type['name'] del parameter_type['in'] result[parameter.name] = parameter_type return result def get_serializer_name(self, serializer: serializers.Serializer, direction: Direction) -> str: return serializer.__class__.__name__ def _get_serializer_name(self, serializer, direction, bypass_extensions=False) -> str: serializer_extension = OpenApiSerializerExtension.get_match(serializer) if serializer_extension and not bypass_extensions: custom_name = serializer_extension.get_name(**filter_supported_arguments( serializer_extension.get_name, auto_schema=self, direction=direction )) else: custom_name = None if custom_name: name = custom_name elif has_override(serializer, 'component_name'): name = get_override(serializer, 'component_name') elif getattr(getattr(serializer, 'Meta', None), 'ref_name', None) is not None: # local override mechanisms. for compatibility with drf-yasg we support meta ref_name, # though we do not support the serializer inlining feature. # https://drf-yasg.readthedocs.io/en/stable/custom_spec.html#serializer-meta-nested-class name = serializer.Meta.ref_name elif is_list_serializer(serializer): return self._get_serializer_name(serializer.child, direction) else: name = self.get_serializer_name(serializer, direction) assert name if name.endswith('Serializer'): name = name[:-10] if is_patched_serializer(serializer, direction): name = 'Patched' + name if direction == 'request' and spectacular_settings.COMPONENT_SPLIT_REQUEST: name = name + 'Request' if not re.match(r'^[\w.-]+$', name): warn( f'Component name "{name}" contains illegal characters. Only "A-Z a-z 0-9 - . _" ' f'are allowed. Furthermore, "-" and "." are discoursed due to potential tooling ' f'issues. This likely leads to an invalid schema.' ) return name def resolve_serializer( self, serializer: _SerializerType, direction: Direction, bypass_extensions=False ) -> ResolvedComponent: assert_basic_serializer(serializer) serializer = force_instance(serializer) with add_trace_message(serializer.__class__): component = ResolvedComponent( name=self._get_serializer_name(serializer, direction, bypass_extensions), type=ResolvedComponent.SCHEMA, object=serializer, ) if component in self.registry: return self.registry[component] # return component with schema self.registry.register(component) component.schema = self._map_serializer(serializer, direction, bypass_extensions) discard_component = ( # components with empty schemas serve no purpose not component.schema # concrete component without properties are likely only transactional so discard or ( component.schema.get('type') == 'object' and not component.schema.get('properties') and 'additionalProperties' not in component.schema ) ) if discard_component: del self.registry[component] return ResolvedComponent(None, None) # sentinel return component drf-spectacular-0.27.0/drf_spectacular/plumbing.py000066400000000000000000001471751453572150400222340ustar00rootroot00000000000000import collections import functools import hashlib import inspect import json import re import sys import types import typing import urllib.parse from abc import ABCMeta from collections import OrderedDict, defaultdict from decimal import Decimal from enum import Enum from typing import ( Any, DefaultDict, Dict, Generic, List, Optional, Sequence, Tuple, Type, TypeVar, Union, ) if sys.version_info >= (3, 10): from typing import TypeGuard # noqa: F401 else: from typing_extensions import TypeGuard # noqa: F401 import inflection import uritemplate from django.apps import apps from django.db.models.constants import LOOKUP_SEP from django.db.models.fields.related_descriptors import ( ForwardManyToOneDescriptor, ManyToManyDescriptor, ReverseManyToOneDescriptor, ReverseOneToOneDescriptor, ) from django.db.models.fields.reverse_related import ForeignObjectRel from django.db.models.sql.query import Query from django.urls.converters import get_converters from django.urls.resolvers import ( # type: ignore[attr-defined] _PATH_PARAMETER_COMPONENT_RE, RegexPattern, Resolver404, RoutePattern, URLPattern, URLResolver, get_resolver, ) from django.utils.functional import Promise, cached_property from django.utils.module_loading import import_string from django.utils.translation import gettext_lazy as _ from rest_framework import exceptions, fields, mixins, serializers, versioning from rest_framework.compat import unicode_http_header from rest_framework.fields import empty from rest_framework.settings import api_settings from rest_framework.test import APIRequestFactory from rest_framework.utils.encoders import JSONEncoder from rest_framework.utils.mediatypes import _MediaType from rest_framework.utils.serializer_helpers import ReturnDict, ReturnList from uritemplate import URITemplate from drf_spectacular.drainage import cache, error, get_override, warn from drf_spectacular.settings import spectacular_settings from drf_spectacular.types import ( DJANGO_PATH_CONVERTER_MAPPING, OPENAPI_TYPE_MAPPING, PYTHON_TYPE_MAPPING, OpenApiTypes, _KnownPythonTypes, ) from drf_spectacular.utils import ( OpenApiExample, OpenApiParameter, _FieldType, _ListSerializerType, _ParameterLocationType, _SchemaType, _SerializerType, ) try: from django.db.models.enums import Choices # only available in Django>3 except ImportError: class Choices: # type: ignore pass # types.UnionType was added in Python 3.10 for new PEP 604 pipe union syntax if hasattr(types, 'UnionType'): UNION_TYPES: Tuple[Any, ...] = (Union, types.UnionType) else: UNION_TYPES = (Union,) LITERAL_TYPES: Tuple[Any, ...] = () TYPED_DICT_META_TYPES: Tuple[Any, ...] = () if sys.version_info >= (3, 8): from typing import Literal as _PyLiteral from typing import _TypedDictMeta as _PyTypedDictMeta # type: ignore[attr-defined] LITERAL_TYPES += (_PyLiteral,) TYPED_DICT_META_TYPES += (_PyTypedDictMeta,) try: from typing_extensions import Literal as _PxLiteral from typing_extensions import _TypedDictMeta as _PxTypedDictMeta # type: ignore[attr-defined] LITERAL_TYPES += (_PxLiteral,) TYPED_DICT_META_TYPES += (_PxTypedDictMeta,) except ImportError: pass if sys.version_info >= (3, 8): CACHED_PROPERTY_FUNCS = (functools.cached_property, cached_property) else: CACHED_PROPERTY_FUNCS = (cached_property,) T = TypeVar('T') class UnableToProceedError(Exception): pass def get_class(obj) -> type: return obj if inspect.isclass(obj) else obj.__class__ def force_instance(serializer_or_field): if not inspect.isclass(serializer_or_field): return serializer_or_field elif issubclass(serializer_or_field, (serializers.BaseSerializer, fields.Field)): return serializer_or_field() else: return serializer_or_field def is_serializer(obj, strict=False) -> TypeGuard[_SerializerType]: from drf_spectacular.extensions import OpenApiSerializerExtension return ( isinstance(force_instance(obj), serializers.BaseSerializer) or (bool(OpenApiSerializerExtension.get_match(obj)) and not strict) ) def is_list_serializer(obj: Any) -> TypeGuard[_ListSerializerType]: return isinstance(force_instance(obj), serializers.ListSerializer) def get_list_serializer(obj: Any): return force_instance(obj) if is_list_serializer(obj) else get_class(obj)(many=True, context=obj.context) def is_list_serializer_customized(obj) -> bool: return ( is_serializer(obj, strict=True) and get_class(get_list_serializer(obj)).to_representation # type: ignore is not serializers.ListSerializer.to_representation ) def is_basic_serializer(obj: Any) -> TypeGuard[_SerializerType]: return is_serializer(obj) and not is_list_serializer(obj) def is_field(obj: Any) -> TypeGuard[_FieldType]: # make sure obj is a serializer field and nothing else. # guard against serializers because BaseSerializer(Field) return isinstance(force_instance(obj), fields.Field) and not is_serializer(obj) def is_basic_type(obj: Any, allow_none=True) -> TypeGuard[_KnownPythonTypes]: if not isinstance(obj, collections.abc.Hashable): return False if not allow_none and (obj is None or obj is OpenApiTypes.NONE): return False return obj in get_openapi_type_mapping() or obj in PYTHON_TYPE_MAPPING def is_patched_serializer(serializer, direction) -> bool: return bool( spectacular_settings.COMPONENT_SPLIT_PATCH and getattr(serializer, 'partial', None) and not getattr(serializer, 'read_only', None) and not (spectacular_settings.COMPONENT_SPLIT_REQUEST and direction == 'response') ) def is_trivial_string_variation(a: str, b: str) -> bool: a = (a or '').strip().lower().replace(' ', '_').replace('-', '_') b = (b or '').strip().lower().replace(' ', '_').replace('-', '_') return a == b def assert_basic_serializer(serializer) -> None: assert is_basic_serializer(serializer), ( f'internal assumption violated because we expected a basic serializer here and ' f'instead got a "{serializer}". This may be the result of another app doing ' f'some unexpected magic or an invalid internal call. Feel free to report this ' f'as a bug at https://github.com/tfranzel/drf-spectacular/issues' ) @cache def get_lib_doc_excludes(): # do not import on package level due to potential import recursion when loading # extensions as recommended: USER's settings.py -> USER EXTENSIONS -> extensions.py # -> plumbing.py -> DRF views -> DRF DefaultSchema -> openapi.py - plumbing.py -> Loop from rest_framework import generics, views, viewsets return [ object, dict, views.APIView, *[getattr(serializers, c) for c in dir(serializers) if c.endswith('Serializer')], *[getattr(viewsets, c) for c in dir(viewsets) if c.endswith('ViewSet')], *[getattr(generics, c) for c in dir(generics) if c.endswith('APIView')], *[getattr(mixins, c) for c in dir(mixins) if c.endswith('Mixin')], ] def get_view_model(view, emit_warnings=True): """ obtain model from view via view's queryset. try safer view attribute first before going through get_queryset(), which may perform arbitrary operations. """ model = getattr(getattr(view, 'queryset', None), 'model', None) if model is not None: return model try: return view.get_queryset().model except Exception as exc: if emit_warnings: warn( f'Failed to obtain model through view\'s queryset due to raised exception. ' f'Prevent this either by setting "queryset = Model.objects.none()" on the ' f'view, checking for "getattr(self, "swagger_fake_view", False)" in ' f'get_queryset() or by simply using @extend_schema. (Exception: {exc})' ) def get_doc(obj) -> str: """ get doc string with fallback on obj's base classes (ignoring DRF documentation). """ def post_cleanup(doc: str) -> str: # also clean up trailing whitespace for each line return '\n'.join(line.rstrip() for line in doc.rstrip().split('\n')) if not inspect.isclass(obj): return post_cleanup(inspect.getdoc(obj) or '') def safe_index(lst, item): try: return lst.index(item) except ValueError: return float("inf") lib_barrier = min( safe_index(obj.__mro__, c) for c in spectacular_settings.GET_LIB_DOC_EXCLUDES() ) for cls in obj.__mro__[:lib_barrier]: if cls.__doc__: return post_cleanup(inspect.cleandoc(cls.__doc__)) return '' def get_type_hints(obj) -> Dict[str, Any]: """ unpack wrapped partial object and use actual func object """ if isinstance(obj, functools.partial): obj = obj.func return typing.get_type_hints(obj) @cache def get_openapi_type_mapping(): return { **OPENAPI_TYPE_MAPPING, OpenApiTypes.OBJECT: build_generic_type(), } def get_manager(model): if not hasattr(model, spectacular_settings.DEFAULT_QUERY_MANAGER): error( f'Failed to obtain queryset from model "{model.__name__}" because manager ' f'"{spectacular_settings.DEFAULT_QUERY_MANAGER}" was not found. You may ' f'need to change the DEFAULT_QUERY_MANAGER setting. bailing.' ) return getattr(model, spectacular_settings.DEFAULT_QUERY_MANAGER) def build_generic_type(): if spectacular_settings.GENERIC_ADDITIONAL_PROPERTIES is None: return {'type': 'object'} elif spectacular_settings.GENERIC_ADDITIONAL_PROPERTIES == 'bool': return {'type': 'object', 'additionalProperties': True} else: return {'type': 'object', 'additionalProperties': {}} def build_basic_type(obj: Union[_KnownPythonTypes, OpenApiTypes]) -> Optional[_SchemaType]: """ resolve either enum or actual type and yield schema template for modification """ openapi_type_mapping = get_openapi_type_mapping() if obj is None or type(obj) is None or obj is OpenApiTypes.NONE: return None elif obj in openapi_type_mapping: return dict(openapi_type_mapping[obj]) elif obj in PYTHON_TYPE_MAPPING: return dict(openapi_type_mapping[PYTHON_TYPE_MAPPING[obj]]) else: warn(f'could not resolve type for "{obj}". defaulting to "string"') return dict(openapi_type_mapping[OpenApiTypes.STR]) def build_array_type(schema: _SchemaType, min_length=None, max_length=None) -> _SchemaType: schema = {'type': 'array', 'items': schema} if min_length is not None: schema['minLength'] = min_length if max_length is not None: schema['maxLength'] = max_length return schema def build_object_type( properties: Optional[_SchemaType] = None, required=None, description: Optional[str] = None, **kwargs ) -> _SchemaType: schema: _SchemaType = {'type': 'object'} if description: schema['description'] = description.strip() if properties: schema['properties'] = properties if 'additionalProperties' in kwargs: schema['additionalProperties'] = kwargs.pop('additionalProperties') if required: schema['required'] = sorted(required) schema.update(kwargs) return schema def build_media_type_object(schema, examples=None, encoding=None) -> _SchemaType: media_type_object = {'schema': schema} if examples: media_type_object['examples'] = examples if encoding: media_type_object['encoding'] = encoding return media_type_object def build_examples_list(examples: Sequence[OpenApiExample]) -> _SchemaType: schema = {} for example in examples: normalized_name = inflection.camelize(example.name.replace(' ', '_')) sub_schema = {} if example.value is not empty: sub_schema['value'] = example.value if example.external_value: sub_schema['externalValue'] = example.external_value if example.summary: sub_schema['summary'] = example.summary elif normalized_name != example.name: sub_schema['summary'] = example.name if example.description: sub_schema['description'] = example.description schema[normalized_name] = sub_schema return schema def build_parameter_type( name: str, schema: _SchemaType, location: _ParameterLocationType, required=False, description=None, enum=None, pattern=None, deprecated=False, explode=None, style=None, default=None, allow_blank=True, examples=None, extensions=None, ) -> _SchemaType: irrelevant_field_meta = ['readOnly', 'writeOnly'] if location == OpenApiParameter.PATH: irrelevant_field_meta += ['nullable', 'default'] schema = { 'in': location, 'name': name, 'schema': {k: v for k, v in schema.items() if k not in irrelevant_field_meta}, } if description: schema['description'] = description if required or location == 'path': schema['required'] = True if deprecated: schema['deprecated'] = True if explode is not None: schema['explode'] = explode if style is not None: schema['style'] = style if enum: # in case of array schema, enum makes little sense on the array itself if schema['schema'].get('type') == 'array': schema['schema']['items']['enum'] = sorted(enum, key=str) else: schema['schema']['enum'] = sorted(enum, key=str) if pattern is not None: # in case of array schema, pattern only makes sense on the items if schema['schema'].get('type') == 'array': schema['schema']['items']['pattern'] = pattern else: schema['schema']['pattern'] = pattern if default is not None and 'default' not in irrelevant_field_meta: schema['schema']['default'] = default if not allow_blank and schema['schema'].get('type') == 'string': schema['schema']['minLength'] = schema['schema'].get('minLength', 1) if examples: schema['examples'] = examples if extensions: schema.update(sanitize_specification_extensions(extensions)) return schema def build_choice_field(field) -> _SchemaType: choices = list(OrderedDict.fromkeys(field.choices)) # preserve order and remove duplicates if all(isinstance(choice, bool) for choice in choices): type: Optional[str] = 'boolean' elif all(isinstance(choice, int) for choice in choices): type = 'integer' elif all(isinstance(choice, (int, float, Decimal)) for choice in choices): # `number` includes `integer` # Ref: https://tools.ietf.org/html/draft-wright-json-schema-validation-00#section-5.21 type = 'number' elif all(isinstance(choice, str) for choice in choices): type = 'string' else: type = None if field.allow_blank and '' not in choices: choices.append('') if field.allow_null and None not in choices: choices.append(None) schema: _SchemaType = { # The value of `enum` keyword MUST be an array and SHOULD be unique. # Ref: https://tools.ietf.org/html/draft-wright-json-schema-validation-00#section-5.20 'enum': choices } # If We figured out `type` then and only then we should set it. It must be a string. # Ref: https://swagger.io/docs/specification/data-models/data-types/#mixed-type # It is optional but it can not be null. # Ref: https://tools.ietf.org/html/draft-wright-json-schema-validation-00#section-5.21 if type: schema['type'] = type if spectacular_settings.ENUM_GENERATE_CHOICE_DESCRIPTION: schema['description'] = build_choice_description_list(field.choices.items()) schema['x-spec-enum-id'] = list_hash([(k, v) for k, v in field.choices.items() if k not in ('', None)]) return schema def build_choice_description_list(choices) -> str: return '\n'.join(f'* `{value}` - {label}' for value, label in choices) def build_bearer_security_scheme_object(header_name, token_prefix, bearer_format=None): """ Either build a bearer scheme or a fallback due to OpenAPI 3.0.3 limitations """ # normalize Django header quirks if header_name.startswith('HTTP_'): header_name = header_name[5:] header_name = header_name.replace('_', '-').capitalize() if token_prefix == 'Bearer' and header_name == 'Authorization': return { 'type': 'http', 'scheme': 'bearer', **({'bearerFormat': bearer_format} if bearer_format else {}), } else: return { 'type': 'apiKey', 'in': 'header', 'name': header_name, 'description': _( 'Token-based authentication with required prefix "%s"' ) % token_prefix } def build_root_object(paths, components, version) -> _SchemaType: settings = spectacular_settings if settings.VERSION and version: version = f'{settings.VERSION} ({version})' else: version = settings.VERSION or version or '' root = { 'openapi': settings.OAS_VERSION, 'info': { 'title': settings.TITLE, 'version': version, **sanitize_specification_extensions(settings.EXTENSIONS_INFO), }, 'paths': {**paths, **settings.APPEND_PATHS}, 'components': components, **sanitize_specification_extensions(settings.EXTENSIONS_ROOT), } if settings.DESCRIPTION: root['info']['description'] = settings.DESCRIPTION if settings.TOS: root['info']['termsOfService'] = settings.TOS if settings.CONTACT: root['info']['contact'] = settings.CONTACT if settings.LICENSE: root['info']['license'] = settings.LICENSE if settings.SERVERS: root['servers'] = settings.SERVERS if settings.TAGS: root['tags'] = settings.TAGS if settings.EXTERNAL_DOCS: root['externalDocs'] = settings.EXTERNAL_DOCS return root def safe_ref(schema: _SchemaType) -> _SchemaType: """ ensure that $ref has its own context and does not remove potential sibling entries when $ref is substituted. also remove useless singular "allOf" . """ if '$ref' in schema and len(schema) > 1: return {'allOf': [{'$ref': schema.pop('$ref')}], **schema} if 'allOf' in schema and len(schema) == 1 and len(schema['allOf']) == 1: return schema['allOf'][0] return schema def append_meta(schema: _SchemaType, meta: _SchemaType) -> _SchemaType: if spectacular_settings.OAS_VERSION.startswith('3.1'): schema_nullable = meta.pop('nullable', None) meta_nullable = schema.pop('nullable', None) if schema_nullable or meta_nullable: if 'type' in schema: schema['type'] = [schema['type'], 'null'] elif '$ref' in schema: schema = {'oneOf': [schema, {'type': 'null'}]} else: assert False, 'Invalid nullable case' # pragma: no cover # these two aspects were merged in OpenAPI 3.1 if "exclusiveMinimum" in schema and "minimum" in schema: schema["exclusiveMinimum"] = schema.pop("minimum") if "exclusiveMaximum" in schema and "maximum" in schema: schema["exclusiveMaximum"] = schema.pop("maximum") return safe_ref({**schema, **meta}) def _follow_field_source(model, path: List[str]): """ navigate through root model via given navigation path. supports forward/reverse relations. """ field_or_property = getattr(model, path[0], None) if len(path) == 1: # end of traversal if isinstance(field_or_property, property): return field_or_property.fget elif isinstance(field_or_property, CACHED_PROPERTY_FUNCS): return field_or_property.func elif callable(field_or_property): return field_or_property elif isinstance(field_or_property, ManyToManyDescriptor): if field_or_property.reverse: return field_or_property.rel.target_field # m2m reverse else: return field_or_property.field.target_field # m2m forward elif isinstance(field_or_property, ReverseOneToOneDescriptor): return field_or_property.related.target_field # o2o reverse elif isinstance(field_or_property, ReverseManyToOneDescriptor): return field_or_property.rel.target_field # foreign reverse elif isinstance(field_or_property, ForwardManyToOneDescriptor): return field_or_property.field.target_field # o2o & foreign forward else: field = model._meta.get_field(path[0]) if isinstance(field, ForeignObjectRel): # case only occurs when relations are traversed in reverse and # not via the related_name (default: X_set) but the model name. return field.target_field else: return field else: if ( isinstance(field_or_property, (property,) + CACHED_PROPERTY_FUNCS) or callable(field_or_property) ): if isinstance(field_or_property, property): target_model = _follow_return_type(field_or_property.fget) elif isinstance(field_or_property, CACHED_PROPERTY_FUNCS): target_model = _follow_return_type(field_or_property.func) else: target_model = _follow_return_type(field_or_property) if not target_model: raise UnableToProceedError( f'could not follow field source through intermediate property "{path[0]}" ' f'on model {model}. Please add a type hint on the model\'s property/function ' f'to enable traversal of the source path "{".".join(path)}".' ) return _follow_field_source(target_model, path[1:]) else: target_model = model._meta.get_field(path[0]).related_model return _follow_field_source(target_model, path[1:]) def _follow_return_type(a_callable): target_type = get_type_hints(a_callable).get('return') if target_type is None: return target_type origin, args = _get_type_hint_origin(target_type) if origin in UNION_TYPES: type_args = [arg for arg in args if arg is not type(None)] # noqa: E721 if len(type_args) > 1: warn( f'could not traverse Union type, because we don\'t know which type to choose ' f'from {type_args}. Consider terminating "source" on a custom property ' f'that indicates the expected Optional/Union type. Defaulting to "string"' ) return target_type # Optional: return type_args[0] return target_type def follow_field_source(model, path, default=None, emit_warnings=True): """ a model traversal chain "foreignkey.foreignkey.value" can either end with an actual model field instance "value" or a model property function named "value". differentiate the cases. :return: models.Field or function object """ try: return _follow_field_source(model, path) except UnableToProceedError as e: if emit_warnings: warn(e) except Exception as exc: if emit_warnings: warn( f'could not resolve field on model {model} with path "{".".join(path)}". ' f'This is likely a custom field that does some unknown magic. Maybe ' f'consider annotating the field/property? Defaulting to "string". (Exception: {exc})' ) def dummy_property(obj) -> str: # type: ignore pass # pragma: no cover return default or dummy_property def follow_model_field_lookup(model, lookup): """ Follow a model lookup `foreignkey__foreignkey__field` in the same way that Django QuerySet.filter() does, returning the final models.Field. """ query = Query(model) lookup_splitted = lookup.split(LOOKUP_SEP) _, field, _, _ = query.names_to_path(lookup_splitted, query.get_meta()) return field def alpha_operation_sorter(endpoint): """ sort endpoints first alphanumerically by path, then by method order """ path, path_regex, method, callback = endpoint method_priority = { 'GET': 0, 'POST': 1, 'PUT': 2, 'PATCH': 3, 'DELETE': 4 }.get(method, 5) # Sort foo{arg} after foo/, but before foo/bar if path.endswith('/'): path = path[:-1] + ' ' path = path.replace('{', '!') return path, method_priority class ResolvedComponent: SCHEMA = 'schemas' SECURITY_SCHEMA = 'securitySchemes' def __init__(self, name, type, schema=None, object=None): self.name = name self.type = type self.schema = schema self.object = object def __bool__(self): return bool(self.name and self.type and self.object) @property def key(self) -> Tuple[str, str]: return self.name, self.type @property def ref(self) -> _SchemaType: assert self.__bool__() return {'$ref': f'#/components/{self.type}/{self.name}'} class ComponentRegistry: def __init__(self) -> None: self._components: Dict[Tuple[str, str], ResolvedComponent] = {} def register(self, component: ResolvedComponent) -> None: if component in self: warn( f'trying to re-register a {component.type} component with name ' f'{self._components[component.key].name}. this might lead to ' f'a incorrect schema. Look out for reused names' ) self._components[component.key] = component def register_on_missing(self, component: ResolvedComponent) -> None: if component not in self: self._components[component.key] = component def __contains__(self, component): if component.key not in self._components: return False query_obj = component.object registry_obj = self._components[component.key].object query_class = query_obj if inspect.isclass(query_obj) else query_obj.__class__ registry_class = query_obj if inspect.isclass(registry_obj) else registry_obj.__class__ suppress_collision_warning = ( get_override(registry_class, 'suppress_collision_warning', False) or get_override(query_class, 'suppress_collision_warning', False) ) if query_class != registry_class and not suppress_collision_warning: warn( f'Encountered 2 components with identical names "{component.name}" and ' f'different classes {query_class} and {registry_class}. This will very ' f'likely result in an incorrect schema. Try renaming one.' ) return True def __getitem__(self, key) -> ResolvedComponent: if isinstance(key, ResolvedComponent): key = key.key return self._components[key] def __delitem__(self, key): if isinstance(key, ResolvedComponent): key = key.key del self._components[key] def build(self, extra_components) -> _SchemaType: output: DefaultDict[str, _SchemaType] = defaultdict(dict) # build tree from flat registry for component in self._components.values(): output[component.type][component.name] = component.schema # add/override extra components for extra_type, extra_component_dict in extra_components.items(): for component_name, component_schema in extra_component_dict.items(): output[extra_type][component_name] = component_schema # sort by component type then by name return { type: {name: output[type][name] for name in sorted(output[type].keys())} for type in sorted(output.keys()) } class OpenApiGeneratorExtension(Generic[T], metaclass=ABCMeta): _registry: List[Type[T]] = [] target_class: Union[None, str, Type[object]] = None match_subclasses = False priority = 0 optional = False def __init_subclass__(cls, **kwargs): super().__init_subclass__(**kwargs) cls._registry.append(cls) def __init__(self, target): self.target = target @classmethod def _load_class(cls): try: cls.target_class = import_string(cls.target_class) except ImportError: installed_apps = apps.app_configs.keys() if any(cls.target_class.startswith(app + '.') for app in installed_apps) and not cls.optional: warn( f'registered extensions {cls.__name__} for "{cls.target_class}" ' f'has an installed app but target class was not found.' ) cls.target_class = None except Exception as e: # pragma: no cover installed_apps = apps.app_configs.keys() if any(cls.target_class.startswith(app + '.') for app in installed_apps): warn( f'Unexpected error {e.__class__.__name__} occurred when attempting ' f'to import {cls.target_class} for extension {cls.__name__} ({e}).' ) cls.target_class = None @classmethod def _matches(cls, target: Any) -> bool: if isinstance(cls.target_class, str): cls._load_class() if cls.target_class is None: return False # app not installed elif cls.match_subclasses: # Targets may trigger customized check through __subclasscheck__. Attempt to be more robust try: return issubclass(get_class(target), cls.target_class) # type: ignore except TypeError: return False else: return get_class(target) == cls.target_class @classmethod def get_match(cls, target) -> Optional[T]: for extension in sorted(cls._registry, key=lambda e: e.priority, reverse=True): if extension._matches(target): return extension(target) return None def deep_import_string(string: str) -> Any: """ augmented import from string, e.g. MODULE.CLASS/OBJECT.ATTRIBUTE """ try: return import_string(string) except ImportError: pass try: *path, attr = string.split('.') obj = import_string('.'.join(path)) return getattr(obj, attr) except (ImportError, AttributeError): pass @cache def load_enum_name_overrides(): overrides = {} for name, choices in spectacular_settings.ENUM_NAME_OVERRIDES.items(): if isinstance(choices, str): choices = deep_import_string(choices) if not choices: warn( f'unable to load choice override for {name} from ENUM_NAME_OVERRIDES. ' f'please check module path string.' ) continue if inspect.isclass(choices) and issubclass(choices, Choices): choices = choices.choices if inspect.isclass(choices) and issubclass(choices, Enum): choices = [(c.value, c.name) for c in choices] normalized_choices = [] for choice in choices: # Allow None values in the simple values list case if isinstance(choice, str) or choice is None: # TODO warning normalized_choices.append((choice, choice)) # simple choice list elif isinstance(choice[1], (list, tuple)): normalized_choices.extend(choice[1]) # categorized nested choices else: normalized_choices.append(choice) # normal 2-tuple form # Get all of choice values that should be used in the hash, blank and None values get excluded # in the post-processing hook for enum overrides, so we do the same here to ensure the hashes match hashable_values = [ (value, label) for value, label in normalized_choices if value not in ['', None] ] overrides[list_hash(hashable_values)] = name if len(spectacular_settings.ENUM_NAME_OVERRIDES) != len(overrides): error( 'ENUM_NAME_OVERRIDES has duplication issues. Encountered multiple names ' 'for the same choice set. Enum naming might be unexpected.' ) return overrides def list_hash(lst: Any) -> str: return hashlib.sha256(json.dumps(list(lst), sort_keys=True, cls=JSONEncoder).encode()).hexdigest()[:16] def anchor_pattern(pattern: str) -> str: if not pattern.startswith('^'): pattern = '^' + pattern if not pattern.endswith('$'): pattern = pattern + '$' return pattern def resolve_django_path_parameter(path_regex, variable, available_formats): """ convert django style path parameters to OpenAPI parameters. """ registered_converters = get_converters() for match in _PATH_PARAMETER_COMPONENT_RE.finditer(path_regex): converter, parameter = match.group('converter'), match.group('parameter') enum_values = None if api_settings.SCHEMA_COERCE_PATH_PK and parameter == 'pk': parameter = 'id' elif spectacular_settings.SCHEMA_COERCE_PATH_PK_SUFFIX and parameter.endswith('_pk'): parameter = f'{parameter[:-3]}_id' if parameter != variable: continue # RE also matches untyped patterns (e.g. "") if not converter: return None # special handling for drf_format_suffix if converter.startswith('drf_format_suffix_'): explicit_formats = converter[len('drf_format_suffix_'):].split('_') enum_values = [ f'.{suffix}' for suffix in explicit_formats if suffix in available_formats ] converter = 'drf_format_suffix' elif converter == 'drf_format_suffix': enum_values = [f'.{suffix}' for suffix in available_formats] if converter in spectacular_settings.PATH_CONVERTER_OVERRIDES: override = spectacular_settings.PATH_CONVERTER_OVERRIDES[converter] if is_basic_type(override): schema = build_basic_type(override) elif isinstance(override, dict): schema = dict(override) else: warn( f'Unable to use path converter override for "{converter}". ' f'Please refer to the documentation on how to use this.' ) return None elif converter in DJANGO_PATH_CONVERTER_MAPPING: schema = build_basic_type(DJANGO_PATH_CONVERTER_MAPPING[converter]) elif converter in registered_converters: # gracious fallback for custom converters that have no override specified. schema = build_basic_type(OpenApiTypes.STR) schema['pattern'] = anchor_pattern(registered_converters[converter].regex) else: error(f'Encountered path converter "{converter}" that is unknown to Django.') return None return build_parameter_type( name=variable, schema=schema, location=OpenApiParameter.PATH, enum=enum_values, ) return None def resolve_regex_path_parameter(path_regex, variable): """ convert regex path parameter to OpenAPI parameter, if pattern is explicitly chosen and not the generic non-empty default '[^/.]+'. """ for parameter, pattern in analyze_named_regex_pattern(path_regex).items(): if api_settings.SCHEMA_COERCE_PATH_PK and parameter == 'pk': parameter = 'id' elif spectacular_settings.SCHEMA_COERCE_PATH_PK_SUFFIX and parameter.endswith('_pk'): parameter = f'{parameter[:-3]}_id' if parameter != variable: continue # do not use default catch-all pattern and defer to model resolution if pattern == '[^/.]+': return None return build_parameter_type( name=variable, schema=build_basic_type(OpenApiTypes.STR), pattern=anchor_pattern(pattern), location=OpenApiParameter.PATH, ) return None def is_versioning_supported(versioning_class) -> bool: return issubclass(versioning_class, ( versioning.URLPathVersioning, versioning.NamespaceVersioning, versioning.AcceptHeaderVersioning )) def operation_matches_version(view, requested_version) -> bool: try: version, _ = view.determine_version(view.request, **view.kwargs) except exceptions.NotAcceptable: return False else: return str(version) == str(requested_version) def modify_for_versioning(patterns, method, path, view, requested_version): assert view.versioning_class and view.request assert requested_version view.request.version = requested_version if issubclass(view.versioning_class, versioning.URLPathVersioning): version_param = view.versioning_class.version_param # substitute version variable to emulate request path = uritemplate.partial(path, var_dict={version_param: requested_version}) if isinstance(path, URITemplate): path = path.uri # emulate router behaviour by injecting substituted variable into view view.kwargs[version_param] = requested_version elif issubclass(view.versioning_class, versioning.NamespaceVersioning): try: view.request.resolver_match = get_resolver( urlconf=detype_patterns(tuple(patterns)), ).resolve(path) except Resolver404: error(f"namespace versioning path resolution failed for {path}. Path will be ignored.") elif issubclass(view.versioning_class, versioning.AcceptHeaderVersioning): # Append the version into request accepted_media_type. # e.g "application/json; version=1.0" # To allow the AcceptHeaderVersioning negotiator going through. if not hasattr(view.request, 'accepted_renderer'): # Probably a mock request, content negotiation was not performed, so, we do it now. negotiated = view.perform_content_negotiation(view.request) view.request.accepted_renderer, view.request.accepted_media_type = negotiated media_type = _MediaType(view.request.accepted_media_type) view.request.accepted_media_type = ( f'{media_type.full_type}; {view.versioning_class.version_param}={requested_version}' ) return path def modify_media_types_for_versioning(view, media_types: List[str]) -> List[str]: if ( not view.versioning_class or not issubclass(view.versioning_class, versioning.AcceptHeaderVersioning) ): return media_types media_type = _MediaType(view.request.accepted_media_type) version = media_type.params.get(view.versioning_class.version_param) # type: ignore version = unicode_http_header(version) if not version or version == view.versioning_class.default_version: return media_types return [ f'{media_type}; {view.versioning_class.version_param}={version}' for media_type in media_types ] def analyze_named_regex_pattern(path: str) -> Dict[str, str]: """ safely extract named groups and their pattern from given regex pattern """ result = {} stack = 0 name_capture, name_buffer = False, '' regex_capture, regex_buffer = False, '' i = 0 while i < len(path): # estimate state at position i skip = False if path[i] == '\\': ff = 2 elif path[i:i + 4] == '(?P<': skip = True name_capture = True ff = 4 elif path[i] in '(' and regex_capture: stack += 1 ff = 1 elif path[i] == '>' and name_capture: assert name_buffer name_capture = False regex_capture = True skip = True ff = 1 elif path[i] in ')' and regex_capture: if not stack: regex_capture = False result[name_buffer] = regex_buffer name_buffer, regex_buffer = '', '' else: stack -= 1 ff = 1 else: ff = 1 # fill buffer based on state if name_capture and not skip: name_buffer += path[i:i + ff] elif regex_capture and not skip: regex_buffer += path[i:i + ff] i += ff assert not stack return result @cache def detype_patterns(patterns): """Cache detyped patterns due to the expensive nature of rebuilding URLResolver.""" return tuple(detype_pattern(pattern) for pattern in patterns) def detype_pattern(pattern): """ return an equivalent pattern that accepts arbitrary values for path parameters. de-typing the path will ease determining a matching route without having properly formatted dummy values for all path parameters. """ if isinstance(pattern, URLResolver): return URLResolver( pattern=detype_pattern(pattern.pattern), urlconf_name=[detype_pattern(p) for p in pattern.url_patterns], default_kwargs=pattern.default_kwargs, app_name=pattern.app_name, namespace=pattern.namespace, ) elif isinstance(pattern, URLPattern): return URLPattern( pattern=detype_pattern(pattern.pattern), callback=pattern.callback, default_args=pattern.default_args, name=pattern.name, ) elif isinstance(pattern, RoutePattern): return RoutePattern( route=re.sub(r'<\w+:(\w+)>', r'<\1>', pattern._route), name=pattern.name, is_endpoint=pattern._is_endpoint ) elif isinstance(pattern, RegexPattern): detyped_regex = pattern._regex for name, regex in analyze_named_regex_pattern(pattern._regex).items(): detyped_regex = detyped_regex.replace( f'(?P<{name}>{regex})', f'(?P<{name}>[^/]+)', ) return RegexPattern( regex=detyped_regex, name=pattern.name, is_endpoint=pattern._is_endpoint ) else: warn(f'unexpected pattern "{pattern}" encountered while simplifying urlpatterns.') return pattern def normalize_result_object(result): """ resolve non-serializable objects like lazy translation strings and OrderedDict """ if isinstance(result, dict) or isinstance(result, OrderedDict): return {k: normalize_result_object(v) for k, v in result.items()} if isinstance(result, list) or isinstance(result, tuple): return [normalize_result_object(v) for v in result] if isinstance(result, Promise): return str(result) for base_type in [bool, int, float, str]: if isinstance(result, base_type): return base_type(result) # coerce basic sub types return result def sanitize_result_object(result): # warn about and resolve operationId collisions with suffixes operations = defaultdict(list) for path, methods in result['paths'].items(): for method, operation in methods.items(): operations[operation['operationId']].append((path, method)) for operation_id, paths in operations.items(): if len(paths) == 1: continue warn(f'operationId "{operation_id}" has collisions {paths}. resolving with numeral suffixes.') for idx, (path, method) in enumerate(sorted(paths)[1:], start=2): suffix = str(idx) if spectacular_settings.CAMELIZE_NAMES else f'_{idx}' result['paths'][path][method]['operationId'] += suffix return result def sanitize_specification_extensions(extensions): # https://spec.openapis.org/oas/v3.0.3#specificationExtensions output = {} for key, value in extensions.items(): if not re.match(r'^x-', key): warn(f'invalid extension {key!r}. vendor extensions must start with "x-"') else: output[key] = value return output def camelize_operation(path, operation): for path_variable in re.findall(r'\{(\w+)\}', path): path = path.replace( f'{{{path_variable}}}', f'{{{inflection.camelize(path_variable, False)}}}' ) for parameter in operation.get('parameters', []): if parameter['in'] == OpenApiParameter.PATH: parameter['name'] = inflection.camelize(parameter['name'], False) operation['operationId'] = inflection.camelize(operation['operationId'], False) return path, operation def build_mock_request(method, path, view, original_request, **kwargs): """ build a mocked request and use original request as reference if available """ request = getattr(APIRequestFactory(), method.lower())(path=path) request = view.initialize_request(request) if original_request: request.user = original_request.user request.auth = original_request.auth # ignore headers related to authorization as it has been handled above. # also ignore ACCEPT as the MIME type refers to SpectacularAPIView and the # version (if available) has already been processed by SpectacularAPIView. for name, value in original_request.META.items(): if not name.startswith('HTTP_'): continue if name in ['HTTP_ACCEPT', 'HTTP_COOKIE', 'HTTP_AUTHORIZATION']: continue request.META[name] = value return request def set_query_parameters(url, **kwargs) -> str: """ deconstruct url, safely attach query parameters in kwargs, and serialize again """ scheme, netloc, path, params, query, fragment = urllib.parse.urlparse(url) query = urllib.parse.parse_qs(query) query.update({k: v for k, v in kwargs.items() if v is not None}) query = urllib.parse.urlencode(query, doseq=True) return urllib.parse.urlunparse((scheme, netloc, path, params, query, fragment)) def get_relative_url(url: str) -> str: scheme, netloc, path, params, query, fragment = urllib.parse.urlparse(url) return urllib.parse.urlunparse(('', '', path, params, query, fragment)) def _get_type_hint_origin(hint): """ graceful fallback for py 3.8 typing functionality """ if sys.version_info >= (3, 8): return typing.get_origin(hint), typing.get_args(hint) else: origin = getattr(hint, '__origin__', None) args = getattr(hint, '__args__', None) origin = { typing.List: list, typing.Dict: dict, typing.Tuple: tuple, typing.Set: set, typing.FrozenSet: frozenset }.get(origin, origin) return origin, args def _resolve_typeddict(hint): """resolve required fields for TypedDicts if on 3.9 or above""" required = None if hasattr(hint, '__required_keys__'): required = [h for h in hint.__required_keys__] return build_object_type( properties={ k: resolve_type_hint(v) for k, v in get_type_hints(hint).items() }, required=required, description=get_doc(hint), ) def resolve_type_hint(hint): """ resolve return value type hints to schema """ origin, args = _get_type_hint_origin(hint) if origin is None and is_basic_type(hint, allow_none=False): return build_basic_type(hint) elif origin is None and inspect.isclass(hint) and issubclass(hint, tuple): # a convoluted way to catch NamedTuple. suggestions welcome. if get_type_hints(hint): properties = {k: resolve_type_hint(v) for k, v in get_type_hints(hint).items()} else: properties = {k: build_basic_type(OpenApiTypes.ANY) for k in hint._fields} return build_object_type(properties=properties, required=properties.keys()) elif origin is list or hint is list or hint is ReturnList: return build_array_type( resolve_type_hint(args[0]) if args else build_basic_type(OpenApiTypes.ANY) ) elif origin is tuple: return build_array_type( schema=build_basic_type(args[0]), max_length=len(args), min_length=len(args), ) elif origin is dict or origin is defaultdict or origin is OrderedDict or hint is ReturnDict: schema = build_basic_type(OpenApiTypes.OBJECT) if args and args[1] is not typing.Any: schema['additionalProperties'] = resolve_type_hint(args[1]) return schema elif origin is set: return build_array_type(resolve_type_hint(args[0])) elif origin is frozenset: return build_array_type(resolve_type_hint(args[0])) elif origin in LITERAL_TYPES: # Literal only works for python >= 3.8 despite typing_extensions, because it # behaves slightly different w.r.t. __origin__ schema = {'enum': list(args)} if all(type(args[0]) is type(choice) for choice in args): schema.update(build_basic_type(type(args[0]))) return schema elif inspect.isclass(hint) and issubclass(hint, Enum): schema = {'enum': [item.value for item in hint]} mixin_base_types = [t for t in hint.__mro__ if is_basic_type(t)] if mixin_base_types: schema.update(build_basic_type(mixin_base_types[0])) return schema elif isinstance(hint, TYPED_DICT_META_TYPES): return _resolve_typeddict(hint) elif origin in UNION_TYPES: type_args = [arg for arg in args if arg is not type(None)] # noqa: E721 if len(type_args) > 1: schema = {'oneOf': [resolve_type_hint(arg) for arg in type_args]} else: schema = resolve_type_hint(type_args[0]) if type(None) in args: schema['nullable'] = True return schema elif origin is collections.abc.Iterable: return build_array_type(resolve_type_hint(args[0])) else: raise UnableToProceedError() def whitelisted(obj: object, classes: Optional[List[Type[object]]], exact=False) -> bool: if classes is None: return True if exact: return obj.__class__ in classes else: return isinstance(obj, tuple(classes)) def build_mocked_view(method: str, path: str, extend_schema_decorator, registry): from rest_framework import parsers, views @extend_schema_decorator class TmpView(views.APIView): parser_classes = [parsers.JSONParser] # emulate what Generator would do to setup schema generation. view_callable = TmpView.as_view() view: views.APIView = view_callable.cls() view.request = spectacular_settings.GET_MOCK_REQUEST( method.upper(), path, view, None ) view.kwargs = {} # prepare AutoSchema with "init" values as if get_operation() was called schema: Any = view.schema schema.registry = registry schema.path = path schema.path_regex = path schema.path_prefix = '' schema.method = method.upper() return view def build_listed_example_value(value: Any, paginator, direction): if not paginator or direction == 'request': return [value] sentinel = object() schema = paginator.get_paginated_response_schema(sentinel) if schema is sentinel: return [value] try: return { field_name: [value] if field_schema is sentinel else field_schema['example'] for field_name, field_schema in schema['properties'].items() } except (AttributeError, KeyError): warn( f"OpenApiExample could not be paginated because {paginator.__class__} either " f"has an unknown schema structure or the individual pagination fields did not " f"provide example values themselves. Using the plain example value as fallback." ) return value def filter_supported_arguments(func, **kwargs): sig = inspect.signature(func) return { arg: val for arg, val in kwargs.items() if arg in sig.parameters } def build_serializer_context(view) -> typing.Dict[str, Any]: try: return view.get_serializer_context() except: # noqa return {'request': view.request} drf-spectacular-0.27.0/drf_spectacular/py.typed000066400000000000000000000000001453572150400215130ustar00rootroot00000000000000drf-spectacular-0.27.0/drf_spectacular/renderers.py000066400000000000000000000053301453572150400223720ustar00rootroot00000000000000from datetime import time, timedelta from decimal import Decimal from uuid import UUID import yaml from django.utils.safestring import SafeString from rest_framework.exceptions import ErrorDetail from rest_framework.renderers import BaseRenderer, JSONRenderer class OpenApiYamlRenderer(BaseRenderer): media_type = 'application/vnd.oai.openapi' format = 'yaml' def render(self, data, media_type=None, renderer_context=None): # disable yaml advanced feature 'alias' for clean, portable, and readable output class Dumper(yaml.SafeDumper): def ignore_aliases(self, data): return True def error_detail_representer(dumper, data): return dumper.represent_dict({'string': str(data), 'code': data.code}) Dumper.add_representer(ErrorDetail, error_detail_representer) def multiline_str_representer(dumper, data): scalar = dumper.represent_str(data) scalar.style = '|' if '\n' in data else None return scalar Dumper.add_representer(str, multiline_str_representer) def decimal_representer(dumper, data): # prevent emitting "!! float" tags on fractionless decimals value = str(data) if '.' in value: return dumper.represent_scalar('tag:yaml.org,2002:float', value) else: return dumper.represent_scalar('tag:yaml.org,2002:int', value) Dumper.add_representer(Decimal, decimal_representer) def timedelta_representer(dumper, data): return dumper.represent_str(str(data.total_seconds())) Dumper.add_representer(timedelta, timedelta_representer) def time_representer(dumper, data): return dumper.represent_str(data.isoformat()) Dumper.add_representer(time, time_representer) def uuid_representer(dumper, data): return dumper.represent_str(str(data)) Dumper.add_representer(UUID, uuid_representer) def safestring_representer(dumper, data): return dumper.represent_str(data) Dumper.add_representer(SafeString, safestring_representer) return yaml.dump( data, default_flow_style=False, sort_keys=False, allow_unicode=True, Dumper=Dumper ).encode('utf-8') class OpenApiYamlRenderer2(OpenApiYamlRenderer): media_type = 'application/yaml' class OpenApiJsonRenderer(JSONRenderer): media_type = 'application/vnd.oai.openapi+json' def get_indent(self, accepted_media_type, renderer_context): return super().get_indent(accepted_media_type, renderer_context) or 4 class OpenApiJsonRenderer2(OpenApiJsonRenderer): media_type = 'application/json' drf-spectacular-0.27.0/drf_spectacular/serializers.py000066400000000000000000000103361453572150400227370ustar00rootroot00000000000000from drf_spectacular.drainage import error, warn from drf_spectacular.extensions import OpenApiSerializerExtension from drf_spectacular.plumbing import force_instance, is_list_serializer, is_serializer class PolymorphicProxySerializerExtension(OpenApiSerializerExtension): target_class = 'drf_spectacular.utils.PolymorphicProxySerializer' priority = -1 def get_name(self): return self.target.component_name def map_serializer(self, auto_schema, direction): """ custom handling for @extend_schema's injection of PolymorphicProxySerializer """ if isinstance(self.target.serializers, dict): sub_components = self._get_explicit_sub_components(auto_schema, direction) else: sub_components = self._get_implicit_sub_components(auto_schema, direction) if not self._has_discriminator(): return {'oneOf': [schema for _, schema in sub_components]} else: return { 'oneOf': [schema for _, schema in sub_components], 'discriminator': { 'propertyName': self.target.resource_type_field_name, 'mapping': {resource_type: schema['$ref'] for resource_type, schema in sub_components} } } def _get_implicit_sub_components(self, auto_schema, direction): sub_components = [] for sub_serializer in self.target.serializers: sub_serializer = self._prep_serializer(sub_serializer) (resolved_name, resolved_schema) = self._process_serializer(auto_schema, sub_serializer, direction) if not resolved_schema: continue if not self._has_discriminator(): sub_components.append((None, resolved_schema)) else: try: discriminator_field = sub_serializer.fields[self.target.resource_type_field_name] resource_type = discriminator_field.to_representation(None) except: # noqa: E722 warn( f'sub-serializer {resolved_name} of {self.target.component_name} ' f'must contain the discriminator field "{self.target.resource_type_field_name}". ' f'defaulting to sub-serializer name, but schema will likely not match the API.' ) resource_type = resolved_name sub_components.append((resource_type, resolved_schema)) return sub_components def _get_explicit_sub_components(self, auto_schema, direction): sub_components = [] for resource_type, sub_serializer in self.target.serializers.items(): sub_serializer = self._prep_serializer(sub_serializer) (_, resolved_schema) = self._process_serializer(auto_schema, sub_serializer, direction) if resolved_schema: sub_components.append((resource_type, resolved_schema)) return sub_components def _has_discriminator(self): return self.target.resource_type_field_name is not None def _prep_serializer(self, serializer): serializer = force_instance(serializer) serializer.partial = self.target.partial return serializer def _process_serializer(self, auto_schema, serializer, direction): if is_list_serializer(serializer): if self._has_discriminator() or self.target._many is not False: warn( "To control sub-serializer's \"many\" attribute, following usage pattern is necessary: " "PolymorphicProxySerializer(serializers=[...], resource_type_field_name=None, " "many=False). Ignoring serializer as it is not processable in this configuration." ) return None, None schema = auto_schema._unwrap_list_serializer(serializer, direction) return None, schema elif is_serializer(serializer): resolved = auto_schema.resolve_serializer(serializer, direction) return (resolved.name, resolved.ref) if resolved else (None, None) else: error("PolymorphicProxySerializer's serializer argument contained unknown objects.") return None, None drf-spectacular-0.27.0/drf_spectacular/settings.py000066400000000000000000000324131453572150400222430ustar00rootroot00000000000000from contextlib import contextmanager from typing import Any, Dict from django.conf import settings from rest_framework.settings import APISettings, perform_import SPECTACULAR_DEFAULTS: Dict[str, Any] = { # A regex specifying the common denominator for all operation paths. If # SCHEMA_PATH_PREFIX is set to None, drf-spectacular will attempt to estimate # a common prefix. Use '' to disable. # Mainly used for tag extraction, where paths like '/api/v1/albums' with # a SCHEMA_PATH_PREFIX regex '/api/v[0-9]' would yield the tag 'albums'. 'SCHEMA_PATH_PREFIX': None, # Remove matching SCHEMA_PATH_PREFIX from operation path. Usually used in # conjunction with appended prefixes in SERVERS. 'SCHEMA_PATH_PREFIX_TRIM': False, # Insert a manual path prefix to the operation path, e.g. '/service/backend'. # Use this for example to align paths when the API is mounted as a sub-resource # behind a proxy and Django is not aware of that. Alternatively, prefixes can # also specified via SERVERS, but this makes the operation path more explicit. 'SCHEMA_PATH_PREFIX_INSERT': '', # Coercion of {pk} to {id} is controlled by SCHEMA_COERCE_PATH_PK. Additionally, # some libraries (e.g. drf-nested-routers) use "_pk" suffixed path variables. # This setting globally coerces path variables like "{user_pk}" to "{user_id}". 'SCHEMA_COERCE_PATH_PK_SUFFIX': False, # Schema generation parameters to influence how components are constructed. # Some schema features might not translate well to your target. # Demultiplexing/modifying components might help alleviate those issues. 'DEFAULT_GENERATOR_CLASS': 'drf_spectacular.generators.SchemaGenerator', # Create separate components for PATCH endpoints (without required list) 'COMPONENT_SPLIT_PATCH': True, # Split components into request and response parts where appropriate # This setting is highly recommended to achieve the most accurate API # description, however it comes at the cost of having more components. 'COMPONENT_SPLIT_REQUEST': False, # Aid client generator targets that have trouble with read-only properties. 'COMPONENT_NO_READ_ONLY_REQUIRED': False, # Adds "minLength: 1" to fields that do not allow blank strings. Deactivated # by default because serializers do not strictly enforce this on responses and # so "minLength: 1" may not always accurately describe API behavior. # Gets implicitly enabled by COMPONENT_SPLIT_REQUEST, because this can be # accurately modeled when request and response components are separated. 'ENFORCE_NON_BLANK_FIELDS': False, # This version string will end up the in schema header. The default OpenAPI # version is 3.0.3, which is heavily tested. We now also support 3.1.0, # which contains the same features and a few mandatory, but minor changes. 'OAS_VERSION': '3.0.3', # Configuration for serving a schema subset with SpectacularAPIView 'SERVE_URLCONF': None, # complete public schema or a subset based on the requesting user 'SERVE_PUBLIC': True, # include schema endpoint into schema 'SERVE_INCLUDE_SCHEMA': True, # list of authentication/permission classes for spectacular's views. 'SERVE_PERMISSIONS': ['rest_framework.permissions.AllowAny'], # None will default to DRF's AUTHENTICATION_CLASSES 'SERVE_AUTHENTICATION': None, # Dictionary of general configuration to pass to the SwaggerUI({ ... }) # https://swagger.io/docs/open-source-tools/swagger-ui/usage/configuration/ # The settings are serialized with json.dumps(). If you need customized JS, use a # string instead. The string must then contain valid JS and is passed unchanged. 'SWAGGER_UI_SETTINGS': { 'deepLinking': True, }, # Initialize SwaggerUI with additional OAuth2 configuration. # https://swagger.io/docs/open-source-tools/swagger-ui/usage/oauth2/ 'SWAGGER_UI_OAUTH2_CONFIG': {}, # Dictionary of general configuration to pass to the Redoc.init({ ... }) # https://github.com/Redocly/redoc#redoc-options-object # The settings are serialized with json.dumps(). If you need customized JS, use a # string instead. The string must then contain valid JS and is passed unchanged. 'REDOC_UI_SETTINGS': {}, # CDNs for swagger and redoc. You can change the version or even host your # own depending on your requirements. For self-hosting, have a look at # the sidecar option in the README. 'SWAGGER_UI_DIST': 'https://cdn.jsdelivr.net/npm/swagger-ui-dist@latest', 'SWAGGER_UI_FAVICON_HREF': 'https://cdn.jsdelivr.net/npm/swagger-ui-dist@latest/favicon-32x32.png', 'REDOC_DIST': 'https://cdn.jsdelivr.net/npm/redoc@latest', # Append OpenAPI objects to path and components in addition to the generated objects 'APPEND_PATHS': {}, 'APPEND_COMPONENTS': {}, # STRONGLY DISCOURAGED (with the exception for the djangorestframework-api-key library) # please don't use this anymore as it has tricky implications that # are hard to get right. For authentication, OpenApiAuthenticationExtension are # strongly preferred because they are more robust and easy to write. # However if used, the list of methods is appended to every endpoint in the schema! 'SECURITY': [], # Postprocessing functions that run at the end of schema generation. # must satisfy interface result = hook(generator, request, public, result) 'POSTPROCESSING_HOOKS': [ 'drf_spectacular.hooks.postprocess_schema_enums' ], # Preprocessing functions that run before schema generation. # must satisfy interface result = hook(endpoints=result) where result # is a list of Tuples (path, path_regex, method, callback). # Example: 'drf_spectacular.hooks.preprocess_exclude_path_format' 'PREPROCESSING_HOOKS': [], # Determines how operations should be sorted. If you intend to do sorting with a # PREPROCESSING_HOOKS, be sure to disable this setting. If configured, the sorting # is applied after the PREPROCESSING_HOOKS. Accepts either # True (drf-spectacular's alpha-sorter), False, or a callable for sort's key arg. 'SORT_OPERATIONS': True, # enum name overrides. dict with keys "YourEnum" and their choice values "field.choices" # e.g. {'SomeEnum': ['A', 'B'], 'OtherEnum': 'import.path.to.choices'} 'ENUM_NAME_OVERRIDES': {}, # Adds "blank" and "null" enum choices where appropriate. disable on client generation issues 'ENUM_ADD_EXPLICIT_BLANK_NULL_CHOICE': True, # Add/Append a list of (``choice value`` - choice name) to the enum description string. 'ENUM_GENERATE_CHOICE_DESCRIPTION': True, # function that returns a list of all classes that should be excluded from doc string extraction 'GET_LIB_DOC_EXCLUDES': 'drf_spectacular.plumbing.get_lib_doc_excludes', # Function that returns a mocked request for view processing. For CLI usage # original_request will be None. # interface: request = build_mock_request(method, path, view, original_request, **kwargs) 'GET_MOCK_REQUEST': 'drf_spectacular.plumbing.build_mock_request', # Camelize names like "operationId" and path parameter names # Camelization of the operation schema itself requires the addition of # 'drf_spectacular.contrib.djangorestframework_camel_case.camelize_serializer_fields' # to POSTPROCESSING_HOOKS. Please note that the hook depends on # ``djangorestframework_camel_case``, while CAMELIZE_NAMES itself does not. 'CAMELIZE_NAMES': False, # Determines if and how free-form 'additionalProperties' should be emitted in the schema. Some # code generator targets are sensitive to this. None disables generic 'additionalProperties'. # allowed values are 'dict', 'bool', None 'GENERIC_ADDITIONAL_PROPERTIES': 'dict', # Path converter schema overrides (e.g. ). Can be used to either modify default # behavior or provide a schema for custom converters registered with register_converter(...). # Takes converter labels as keys and either basic python types, OpenApiType, or raw schemas # as values. Example: {'aint': OpenApiTypes.INT, 'bint': str, 'cint': {'type': ...}} 'PATH_CONVERTER_OVERRIDES': {}, # Determines whether operation parameters should be sorted alphanumerically or just in # the order they arrived. Accepts either True, False, or a callable for sort's key arg. 'SORT_OPERATION_PARAMETERS': True, # @extend_schema allows to specify status codes besides 200. This functionality is usually used # to describe error responses, which rarely make use of list mechanics. Therefore, we suppress # listing (pagination and filtering) on non-2XX status codes by default. Toggle this to enable # list responses with ListSerializers/many=True irrespective of the status code. 'ENABLE_LIST_MECHANICS_ON_NON_2XX': False, # This setting allows you to deviate from the default manager by accessing a different model # property. We use "objects" by default for compatibility reasons. Using "_default_manager" # will likely fix most issues, though you are free to choose any name. "DEFAULT_QUERY_MANAGER": 'objects', # Controls which authentication methods are exposed in the schema. If not None, will hide # authentication classes that are not contained in the whitelist. Use full import paths # like ['rest_framework.authentication.TokenAuthentication', ...]. # Empty list ([]) will hide all authentication methods. The default None will show all. 'AUTHENTICATION_WHITELIST': None, # Controls which parsers are exposed in the schema. Works analog to AUTHENTICATION_WHITELIST. # List of allowed parsers or None to allow all. 'PARSER_WHITELIST': None, # Controls which renderers are exposed in the schema. Works analog to AUTHENTICATION_WHITELIST. # rest_framework.renderers.BrowsableAPIRenderer is ignored by default if whitelist is None 'RENDERER_WHITELIST': None, # Option for turning off error and warn messages 'DISABLE_ERRORS_AND_WARNINGS': False, # Runs exemplary schema generation and emits warnings as part of "./manage.py check --deploy" 'ENABLE_DJANGO_DEPLOY_CHECK': True, # General schema metadata. Refer to spec for valid inputs # https://spec.openapis.org/oas/v3.0.3#openapi-object 'TITLE': '', 'DESCRIPTION': '', 'TOS': None, # Optional: MAY contain "name", "url", "email" 'CONTACT': {}, # Optional: MUST contain "name", MAY contain URL 'LICENSE': {}, # Statically set schema version. May also be an empty string. When used together with # view versioning, will become '0.0.0 (v2)' for 'v2' versioned requests. # Set VERSION to None if only the request version should be rendered. 'VERSION': '0.0.0', # Optional list of servers. # Each entry MUST contain "url", MAY contain "description", "variables" # e.g. [{'url': 'https://example.com/v1', 'description': 'Text'}, ...] 'SERVERS': [], # Tags defined in the global scope 'TAGS': [], # Optional: MUST contain 'url', may contain "description" 'EXTERNAL_DOCS': {}, # Arbitrary specification extensions attached to the schema's info object. # https://swagger.io/specification/#specification-extensions 'EXTENSIONS_INFO': {}, # Arbitrary specification extensions attached to the schema's root object. # https://swagger.io/specification/#specification-extensions 'EXTENSIONS_ROOT': {}, # Oauth2 related settings. used for example by django-oauth2-toolkit. # https://spec.openapis.org/oas/v3.0.3#oauthFlowsObject 'OAUTH2_FLOWS': [], 'OAUTH2_AUTHORIZATION_URL': None, 'OAUTH2_TOKEN_URL': None, 'OAUTH2_REFRESH_URL': None, 'OAUTH2_SCOPES': None, } IMPORT_STRINGS = [ 'DEFAULT_GENERATOR_CLASS', 'SERVE_AUTHENTICATION', 'SERVE_PERMISSIONS', 'POSTPROCESSING_HOOKS', 'PREPROCESSING_HOOKS', 'GET_LIB_DOC_EXCLUDES', 'GET_MOCK_REQUEST', 'SORT_OPERATIONS', 'SORT_OPERATION_PARAMETERS', 'AUTHENTICATION_WHITELIST', 'RENDERER_WHITELIST', 'PARSER_WHITELIST', ] class SpectacularSettings(APISettings): _original_settings: Dict[str, Any] = {} def apply_patches(self, patches): for attr, val in patches.items(): if attr.startswith('SERVE_') or attr == 'DEFAULT_GENERATOR_CLASS': raise AttributeError( f'{attr} not allowed in custom_settings. use dedicated parameter instead.' ) if attr in self.import_strings: val = perform_import(val, attr) # load and store original value, then override __dict__ entry self._original_settings[attr] = getattr(self, attr) setattr(self, attr, val) def clear_patches(self): for attr, orig_val in self._original_settings.items(): setattr(self, attr, orig_val) self._original_settings = {} spectacular_settings = SpectacularSettings( user_settings=getattr(settings, 'SPECTACULAR_SETTINGS', {}), # type: ignore defaults=SPECTACULAR_DEFAULTS, # type: ignore import_strings=IMPORT_STRINGS, ) @contextmanager def patched_settings(patches): """ temporarily patch the global spectacular settings (or do nothing) """ if not patches: yield else: try: spectacular_settings.apply_patches(patches) yield finally: spectacular_settings.clear_patches() drf-spectacular-0.27.0/drf_spectacular/templates/000077500000000000000000000000001453572150400220245ustar00rootroot00000000000000drf-spectacular-0.27.0/drf_spectacular/templates/drf_spectacular/000077500000000000000000000000001453572150400251655ustar00rootroot00000000000000drf-spectacular-0.27.0/drf_spectacular/templates/drf_spectacular/redoc.html000066400000000000000000000021361453572150400271510ustar00rootroot00000000000000 {% block head %} {{ title|default:"Redoc" }} {% endblock head %} {% block body %} {% if settings %}
{% else %} {% endif %} {% endblock body %} drf-spectacular-0.27.0/drf_spectacular/templates/drf_spectacular/swagger_ui.html000066400000000000000000000016341453572150400302130ustar00rootroot00000000000000 {% block head %} {{ title|default:"Swagger" }} {% if favicon_href %}{% endif %} {% endblock head %} {% block body %}
{% if script_url %} {% else %} {% endif %} {% endblock %} drf-spectacular-0.27.0/drf_spectacular/templates/drf_spectacular/swagger_ui.js000066400000000000000000000070351453572150400276640ustar00rootroot00000000000000"use strict"; const swaggerSettings = {{ settings|safe }}; const schemaAuthNames = {{ schema_auth_names|safe }}; let schemaAuthFailed = false; const plugins = []; const reloadSchemaOnAuthChange = () => { return { statePlugins: { auth: { wrapActions: { authorize: (ori) => (...args) => { schemaAuthFailed = false; setTimeout(() => ui.specActions.download()); return ori(...args); }, logout: (ori) => (...args) => { schemaAuthFailed = false; setTimeout(() => ui.specActions.download()); return ori(...args); }, }, }, }, }; }; if (schemaAuthNames.length > 0) { plugins.push(reloadSchemaOnAuthChange); } const uiInitialized = () => { try { ui; return true; } catch { return false; } }; const isSchemaUrl = (url) => { if (!uiInitialized()) { return false; } return url === new URL(ui.getConfigs().url, document.baseURI).href; }; const responseInterceptor = (response, ...args) => { if (!response.ok && isSchemaUrl(response.url)) { console.warn("schema request received '" + response.status + "'. disabling credentials for schema till logout."); if (!schemaAuthFailed) { // only retry once to prevent endless loop. schemaAuthFailed = true; setTimeout(() => ui.specActions.download()); } } return response; }; const injectAuthCredentials = (request) => { let authorized; if (uiInitialized()) { const state = ui.getState().get("auth").get("authorized"); if (state !== undefined && Object.keys(state.toJS()).length !== 0) { authorized = state.toJS(); } } else if (![undefined, "{}"].includes(localStorage.authorized)) { authorized = JSON.parse(localStorage.authorized); } if (authorized === undefined) { return; } for (const authName of schemaAuthNames) { const authDef = authorized[authName]; if (authDef === undefined || authDef.schema === undefined) { continue; } if (authDef.schema.type === "http" && authDef.schema.scheme === "bearer") { request.headers["Authorization"] = "Bearer " + authDef.value; return; } else if (authDef.schema.type === "http" && authDef.schema.scheme === "basic") { request.headers["Authorization"] = "Basic " + btoa(authDef.value.username + ":" + authDef.value.password); return; } else if (authDef.schema.type === "apiKey" && authDef.schema.in === "header") { request.headers[authDef.schema.name] = authDef.value; return; } else if (authDef.schema.type === "oauth2" && authDef.token.token_type === "Bearer") { request.headers["Authorization"] = `Bearer ${authDef.token.access_token}`; return; } } }; const requestInterceptor = (request, ...args) => { if (request.loadSpec && schemaAuthNames.length > 0 && !schemaAuthFailed) { try { injectAuthCredentials(request); } catch (e) { console.error("schema auth injection failed with error: ", e); } } // selectively omit adding headers to mitigate CORS issues. if (!["GET", undefined].includes(request.method) && request.credentials === "same-origin") { request.headers["{{ csrf_header_name }}"] = "{{ csrf_token }}"; } return request; }; const ui = SwaggerUIBundle({ url: "{{ schema_url|escapejs }}", dom_id: "#swagger-ui", presets: [SwaggerUIBundle.presets.apis], plugins, layout: "BaseLayout", requestInterceptor, responseInterceptor, ...swaggerSettings, }); {% if oauth2_config %}ui.initOAuth({{ oauth2_config|safe }});{% endif %} drf-spectacular-0.27.0/drf_spectacular/types.py000066400000000000000000000170041453572150400215460ustar00rootroot00000000000000import enum import typing from datetime import date, datetime, time, timedelta from decimal import Decimal from ipaddress import IPv4Address, IPv6Address from uuid import UUID _KnownPythonTypes = typing.Type[typing.Union[ str, float, bool, bytes, int, dict, UUID, Decimal, datetime, date, time, timedelta, IPv4Address, IPv6Address, ]] class OpenApiTypes(enum.Enum): """ Basic types known to the OpenAPI specification or at least common format extension of it. - Use ``BYTE`` for base64-encoded data wrapped in a string - Use ``BINARY`` for raw binary data - Use ``OBJECT`` for arbitrary free-form object (usually a :py:class:`dict`) """ #: Converted to ``{"type": "number"}``. NUMBER = enum.auto() #: Converted to ``{"type": "number", "format": "float"}``. #: Equivalent to :py:class:`float`. FLOAT = enum.auto() #: Converted to ``{"type": "number", "format": "double"}``. DOUBLE = enum.auto() #: Converted to ``{"type": "boolean"}``. #: Equivalent to :py:class:`bool`. BOOL = enum.auto() #: Converted to ``{"type": "string"}``. #: Equivalent to :py:class:`str`. STR = enum.auto() #: Converted to ``{"type": "string", "format": "byte"}``. #: Use this for base64-encoded data wrapped in a string. BYTE = enum.auto() #: Converted to ``{"type": "string", "format": "binary"}``. #: Equivalent to :py:class:`bytes`. #: Use this for raw binary data. BINARY = enum.auto() #: Converted to ``{"type": "string", "format": "password"}``. PASSWORD = enum.auto() #: Converted to ``{"type": "integer"}``. #: Equivalent to :py:class:`int`. INT = enum.auto() #: Converted to ``{"type": "integer", "format": "int32"}``. INT32 = enum.auto() #: Converted to ``{"type": "integer", "format": "int64"}``. INT64 = enum.auto() #: Converted to ``{"type": "string", "format": "uuid"}``. #: Equivalent to :py:class:`~uuid.UUID`. UUID = enum.auto() #: Converted to ``{"type": "string", "format": "uri"}``. URI = enum.auto() #: Converted to ``{"type": "string", "format": "uri-reference"}``. URI_REF = enum.auto() #: Converted to ``{"type": "string", "format": "uri-template"}``. URI_TPL = enum.auto() #: Converted to ``{"type": "string", "format": "iri"}``. IRI = enum.auto() #: Converted to ``{"type": "string", "format": "iri-reference"}``. IRI_REF = enum.auto() #: Converted to ``{"type": "string", "format": "ipv4"}``. #: Equivalent to :py:class:`~ipaddress.IPv4Address`. IP4 = enum.auto() #: Converted to ``{"type": "string", "format": "ipv6"}``. #: Equivalent to :py:class:`~ipaddress.IPv6Address`. IP6 = enum.auto() #: Converted to ``{"type": "string", "format": "hostname"}``. HOSTNAME = enum.auto() #: Converted to ``{"type": "string", "format": "idn-hostname"}``. IDN_HOSTNAME = enum.auto() #: Converted to ``{"type": "number", "format": "double"}``. #: The same as :py:attr:`~drf_spectacular.types.OpenApiTypes.DOUBLE`. #: Equivalent to :py:class:`~decimal.Decimal`. DECIMAL = enum.auto() #: Converted to ``{"type": "string", "format": "date-time"}``. #: Equivalent to :py:class:`~datetime.datetime`. DATETIME = enum.auto() #: Converted to ``{"type": "string", "format": "date"}``. #: Equivalent to :py:class:`~datetime.date`. DATE = enum.auto() #: Converted to ``{"type": "string", "format": "time"}``. #: Equivalent to :py:class:`~datetime.time`. TIME = enum.auto() #: Converted to ``{"type": "string", "format": "duration"}``. #: Equivalent to :py:class:`~datetime.timedelta`. #: Expressed according to ISO 8601. DURATION = enum.auto() #: Converted to ``{"type": "string", "format": "email"}``. EMAIL = enum.auto() #: Converted to ``{"type": "string", "format": "idn-email"}``. IDN_EMAIL = enum.auto() #: Converted to ``{"type": "string", "format": "json-pointer"}``. JSON_PTR = enum.auto() #: Converted to ``{"type": "string", "format": "relative-json-pointer"}``. JSON_PTR_REL = enum.auto() #: Converted to ``{"type": "string", "format": "regex"}``. REGEX = enum.auto() #: Converted to ``{"type": "object", ...}``. #: Use this for arbitrary free-form objects (usually a :py:class:`dict`). #: The ``additionalProperties`` item is added depending on the ``GENERIC_ADDITIONAL_PROPERTIES`` setting. OBJECT = enum.auto() #: Equivalent to :py:data:`None`. #: This signals that the request or response is empty. NONE = enum.auto() #: Converted to ``{}`` which sets no type and format. #: Equivalent to :py:class:`typing.Any`. ANY = enum.auto() # make a copy with dict() before modifying returned dict OPENAPI_TYPE_MAPPING = { OpenApiTypes.NUMBER: {'type': 'number'}, OpenApiTypes.FLOAT: {'type': 'number', 'format': 'float'}, OpenApiTypes.DOUBLE: {'type': 'number', 'format': 'double'}, OpenApiTypes.BOOL: {'type': 'boolean'}, OpenApiTypes.STR: {'type': 'string'}, OpenApiTypes.BYTE: {'type': 'string', 'format': 'byte'}, OpenApiTypes.BINARY: {'type': 'string', 'format': 'binary'}, OpenApiTypes.PASSWORD: {'type': 'string', 'format': 'password'}, OpenApiTypes.INT: {'type': 'integer'}, OpenApiTypes.INT32: {'type': 'integer', 'format': 'int32'}, OpenApiTypes.INT64: {'type': 'integer', 'format': 'int64'}, OpenApiTypes.UUID: {'type': 'string', 'format': 'uuid'}, OpenApiTypes.URI: {'type': 'string', 'format': 'uri'}, OpenApiTypes.URI_REF: {'type': 'string', 'format': 'uri-reference'}, OpenApiTypes.URI_TPL: {'type': 'string', 'format': 'uri-template'}, OpenApiTypes.IRI: {'type': 'string', 'format': 'iri'}, OpenApiTypes.IRI_REF: {'type': 'string', 'format': 'iri-reference'}, OpenApiTypes.IP4: {'type': 'string', 'format': 'ipv4'}, OpenApiTypes.IP6: {'type': 'string', 'format': 'ipv6'}, OpenApiTypes.HOSTNAME: {'type': 'string', 'format': 'hostname'}, OpenApiTypes.IDN_HOSTNAME: {'type': 'string', 'format': 'idn-hostname'}, OpenApiTypes.DECIMAL: {'type': 'number', 'format': 'double'}, OpenApiTypes.DATETIME: {'type': 'string', 'format': 'date-time'}, OpenApiTypes.DATE: {'type': 'string', 'format': 'date'}, OpenApiTypes.TIME: {'type': 'string', 'format': 'time'}, OpenApiTypes.DURATION: {'type': 'string', 'format': 'duration'}, # ISO 8601 OpenApiTypes.EMAIL: {'type': 'string', 'format': 'email'}, OpenApiTypes.IDN_EMAIL: {'type': 'string', 'format': 'idn-email'}, OpenApiTypes.JSON_PTR: {'type': 'string', 'format': 'json-pointer'}, OpenApiTypes.JSON_PTR_REL: {'type': 'string', 'format': 'relative-json-pointer'}, OpenApiTypes.REGEX: {'type': 'string', 'format': 'regex'}, OpenApiTypes.ANY: {}, OpenApiTypes.NONE: None, # OpenApiTypes.OBJECT is inserted at runtime due to dependency on settings } PYTHON_TYPE_MAPPING = { str: OpenApiTypes.STR, float: OpenApiTypes.DOUBLE, bool: OpenApiTypes.BOOL, bytes: OpenApiTypes.BINARY, int: OpenApiTypes.INT, UUID: OpenApiTypes.UUID, Decimal: OpenApiTypes.DECIMAL, datetime: OpenApiTypes.DATETIME, date: OpenApiTypes.DATE, time: OpenApiTypes.TIME, timedelta: OpenApiTypes.DURATION, IPv4Address: OpenApiTypes.IP4, IPv6Address: OpenApiTypes.IP6, dict: OpenApiTypes.OBJECT, typing.Any: OpenApiTypes.ANY, None: OpenApiTypes.NONE, } DJANGO_PATH_CONVERTER_MAPPING = { 'int': OpenApiTypes.INT, 'path': OpenApiTypes.STR, 'slug': OpenApiTypes.STR, 'str': OpenApiTypes.STR, 'uuid': OpenApiTypes.UUID, 'drf_format_suffix': OpenApiTypes.STR, } drf-spectacular-0.27.0/drf_spectacular/utils.py000066400000000000000000000677171453572150400215620ustar00rootroot00000000000000import inspect import sys from typing import Any, Callable, Dict, Optional, Sequence, Tuple, Type, TypeVar, Union from django.utils.functional import Promise # direct import due to https://github.com/microsoft/pyright/issues/3025 if sys.version_info >= (3, 8): from typing import Final, Literal else: from typing_extensions import Final, Literal from rest_framework.fields import Field, empty from rest_framework.serializers import ListSerializer, Serializer from rest_framework.settings import api_settings from drf_spectacular.drainage import ( error, get_view_method_names, isolate_view_method, set_override, warn, ) from drf_spectacular.types import OpenApiTypes, _KnownPythonTypes _ListSerializerType = Union[ListSerializer, Type[ListSerializer]] _SerializerType = Union[Serializer, Type[Serializer]] _FieldType = Union[Field, Type[Field]] _ParameterLocationType = Literal['query', 'path', 'header', 'cookie'] _StrOrPromise = Union[str, Promise] _SchemaType = Dict[str, Any] Direction = Literal['request', 'response'] class PolymorphicProxySerializer(Serializer): """ This class is to be used with :func:`@extend_schema <.extend_schema>` to signal a request/response might be polymorphic (accepts/returns data possibly from different serializers). Usage usually looks like this: .. code-block:: @extend_schema( request=PolymorphicProxySerializer( component_name='MetaPerson', serializers=[ LegalPersonSerializer, NaturalPersonSerializer, ], resource_type_field_name='person_type', ) ) def create(self, request, *args, **kwargs): return Response(...) **Beware** that this is not a real serializer and it will raise an AssertionError if used in that way. It **cannot** be used in views as ``serializer_class`` or as field in an actual serializer. It is solely meant for annotation purposes. Also make sure that each sub-serializer has a field named after the value of ``resource_type_field_name`` (discriminator field). Generated clients will likely depend on the existence of this field. Setting ``resource_type_field_name`` to ``None`` will remove the discriminator altogether. This may be useful in certain situations, but will most likely break client generation. Another use-case is explicit control over sub-serializer's ``many`` attribute. To explicitly control this aspect, you need disable the discriminator with ``resource_type_field_name=None`` as well as disable automatic list handling with ``many=False``. It is **strongly** recommended to pass the ``Serializers`` as **list**, and by that let *drf-spectacular* retrieve the field and handle the mapping automatically. In special circumstances, the field may not available when *drf-spectacular* processes the serializer. In those cases you can explicitly state the mapping with ``{'legal': LegalPersonSerializer, ...}``, but it is then your responsibility to have a valid mapping. It is also permissible to provide a callable with no parameters for ``serializers``, such as a lambda that will return an appropriate list or dict when evaluated. """ def __init__( self, component_name: str, serializers: Union[ Sequence[_SerializerType], Dict[str, _SerializerType], Callable[[], Sequence[_SerializerType]], Callable[[], Dict[str, _SerializerType]] ], resource_type_field_name: Optional[str], many: Optional[bool] = None, **kwargs ): self.component_name = component_name self.serializers = serializers self.resource_type_field_name = resource_type_field_name if self._many is False: # type: ignore[attr-defined] set_override(self, 'many', False) # retain kwargs in context for potential anonymous re-init with many=True kwargs.setdefault('context', {}).update({ 'component_name': component_name, 'serializers': serializers, 'resource_type_field_name': resource_type_field_name }) super().__init__(**kwargs) def __new__(cls, *args, **kwargs): many = kwargs.pop('many', None) if many is True: context = kwargs.get('context', {}) for arg in ['component_name', 'serializers', 'resource_type_field_name']: if arg in context: kwargs[arg] = context.pop(arg) # re-apply retained args instance = cls.many_init(*args, **kwargs) else: instance = super().__new__(cls, *args, **kwargs) instance._many = many return instance @property def serializers(self): if callable(self._serializers): self._serializers = self._serializers() return self._serializers @serializers.setter def serializers(self, value): self._serializers = value @property def data(self): self._trap() def to_internal_value(self, data): self._trap() def to_representation(self, instance): self._trap() def _trap(self): raise AssertionError( "PolymorphicProxySerializer is an annotation helper and not supposed to " "be used for real requests. See documentation for correct usage." ) class OpenApiSchemaBase: pass class OpenApiExample(OpenApiSchemaBase): """ Helper class to document a API parameter / request body / response body with a concrete example value. It is recommended to provide a singular example value, since pagination and list responses are handled by drf-spectacular. The example will be attached to the operation object where appropriate, i.e. where the given ``media_type``, ``status_code`` and modifiers match. Example that do not match any scenario are ignored. - media_type will default to 'application/json' unless implicitly specified through :class:`.OpenApiResponse` - status_codes will default to [200, 201] unless implicitly specified through :class:`.OpenApiResponse` """ def __init__( self, name: str, value: Any = empty, external_value: str = '', summary: _StrOrPromise = '', description: _StrOrPromise = '', request_only: bool = False, response_only: bool = False, parameter_only: Optional[Tuple[str, _ParameterLocationType]] = None, media_type: Optional[str] = None, status_codes: Optional[Sequence[Union[str, int]]] = None, ): self.name = name self.summary = summary self.description = description self.value = value self.external_value = external_value self.request_only = request_only self.response_only = response_only self.parameter_only = parameter_only self.media_type = media_type self.status_codes = status_codes class OpenApiParameter(OpenApiSchemaBase): """ Helper class to document request query/path/header/cookie parameters. Can also be used to document response headers. Please note that not all arguments apply to all ``location``/``type``/direction variations, e.g. path parameters are ``required=True`` by definition. For valid ``style`` choices please consult the `OpenAPI specification `_. """ QUERY: Final = 'query' PATH: Final = 'path' HEADER: Final = 'header' COOKIE: Final = 'cookie' def __init__( self, name: str, type: Union[_SerializerType, _KnownPythonTypes, OpenApiTypes, _SchemaType] = str, location: _ParameterLocationType = QUERY, required: bool = False, description: _StrOrPromise = '', enum: Optional[Sequence[Any]] = None, pattern: Optional[str] = None, deprecated: bool = False, style: Optional[str] = None, explode: Optional[bool] = None, default: Any = None, allow_blank: bool = True, many: Optional[bool] = None, examples: Optional[Sequence[OpenApiExample]] = None, extensions: Optional[Dict[str, Any]] = None, exclude: bool = False, response: Union[bool, Sequence[Union[int, str]]] = False, ): self.name = name self.type = type self.location = location self.required = required self.description = description self.enum = enum self.pattern = pattern self.deprecated = deprecated self.style = style self.explode = explode self.default = default self.allow_blank = allow_blank self.many = many self.examples = examples or [] self.extensions = extensions self.exclude = exclude self.response = response class OpenApiResponse(OpenApiSchemaBase): """ Helper class to bundle a response object (``Serializer``, ``OpenApiType``, raw schema, etc) together with a response object description and/or examples. Examples can alternatively be provided via :func:`@extend_schema <.extend_schema>`. This class is especially helpful for explicitly describing status codes on a "Response Object" level. """ def __init__( self, response: Any = None, description: _StrOrPromise = '', examples: Optional[Sequence[OpenApiExample]] = None ): self.response = response self.description = description self.examples = examples or [] class OpenApiRequest(OpenApiSchemaBase): """ Helper class to combine a request object (``Serializer``, ``OpenApiType``, raw schema, etc.) together with an encoding object and/or examples. Examples can alternatively be provided via :func:`@extend_schema <.extend_schema>`. This class is especially helpful for customizing value encoding for ``application/x-www-form-urlencoded`` and ``multipart/*``. The encoding parameter takes a dictionary with field names as keys and encoding objects as values. Refer to the `specification `_ on how to build a valid encoding object. """ def __init__( self, request: Any = None, encoding: Optional[Dict[str, Dict[str, Any]]] = None, examples: Optional[Sequence[OpenApiExample]] = None, ): self.request = request self.encoding = encoding self.examples = examples or [] F = TypeVar('F', bound=Callable[..., Any]) class OpenApiCallback(OpenApiSchemaBase): """ Helper class to bundle a callback definition. This specifies a view on the callee's side, effectively stating the expectations on the receiving end. Please note that this particular :func:`@extend_schema <.extend_schema>` instance operates from the perspective of the callback origin, which means that ``request`` specifies the outgoing request. For convenience sake, we assume the callback sends ``application/json`` and return a ``200``. If that is not sufficient, you can use ``request`` and ``responses`` overloads just as you normally would. :param name: Name under which the this callback is listed in the schema. :param path: Path on which the callback operation is performed. To reference request body contents, please refer to OpenAPI specification's `key expressions `_ for valid choices. :param decorator: :func:`@extend_schema <.extend_schema>` decorator that specifies the receiving endpoint. In this special context the allowed parameters are ``requests``, ``responses``, ``summary``, ``description``, ``deprecated``. """ def __init__( self, name: _StrOrPromise, path: str, decorator: Union[Callable[[F], F], Dict[str, Callable[[F], F]], Dict[str, Any]], ): self.name = name self.path = path self.decorator = decorator def extend_schema( operation_id: Optional[str] = None, parameters: Optional[Sequence[Union[OpenApiParameter, _SerializerType]]] = None, request: Any = empty, responses: Any = empty, auth: Optional[Sequence[str]] = None, description: Optional[_StrOrPromise] = None, summary: Optional[_StrOrPromise] = None, deprecated: Optional[bool] = None, tags: Optional[Sequence[str]] = None, filters: Optional[bool] = None, exclude: Optional[bool] = None, operation: Optional[_SchemaType] = None, methods: Optional[Sequence[str]] = None, versions: Optional[Sequence[str]] = None, examples: Optional[Sequence[OpenApiExample]] = None, extensions: Optional[Dict[str, Any]] = None, callbacks: Optional[Sequence[OpenApiCallback]] = None, external_docs: Optional[Union[Dict[str, str], str]] = None, ) -> Callable[[F], F]: """ Decorator mainly for the "view" method kind. Partially or completely overrides what would be otherwise generated by drf-spectacular. :param operation_id: replaces the auto-generated operation_id. make sure there are no naming collisions. :param parameters: list of additional or replacement parameters added to the auto-discovered fields. :param responses: replaces the discovered Serializer. Takes a variety of inputs that can be used individually or combined - ``Serializer`` class - ``Serializer`` instance (e.g. ``Serializer(many=True)`` for listings) - basic types or instances of ``OpenApiTypes`` - :class:`.OpenApiResponse` for bundling any of the other choices together with either a dedicated response description and/or examples. - :class:`.PolymorphicProxySerializer` for signaling that the operation may yield data from different serializers depending on the circumstances. - ``dict`` with status codes as keys and one of the above as values. Additionally in this case, it is also possible to provide a raw schema dict as value. - ``dict`` with tuples (status_code, media_type) as keys and one of the above as values. Additionally in this case, it is also possible to provide a raw schema dict as value. :param request: replaces the discovered ``Serializer``. Takes a variety of inputs - ``Serializer`` class/instance - basic types or instances of ``OpenApiTypes`` - :class:`.PolymorphicProxySerializer` for signaling that the operation accepts a set of different types of objects. - ``dict`` with media_type as keys and one of the above as values. Additionally, in this case, it is also possible to provide a raw schema dict as value. :param auth: replace discovered auth with explicit list of auth methods :param description: replaces discovered doc strings :param summary: an optional short summary of the description :param deprecated: mark operation as deprecated :param tags: override default list of tags :param filters: ignore list detection and forcefully enable/disable filter discovery :param exclude: set True to exclude operation from schema :param operation: manually override what auto-discovery would generate. you must provide a OpenAPI3-compliant dictionary that gets directly translated to YAML. :param methods: scope extend_schema to specific methods. matches all by default. :param versions: scope extend_schema to specific API version. matches all by default. :param examples: attach request/response examples to the operation :param extensions: specification extensions, e.g. ``x-badges``, ``x-code-samples``, etc. :param callbacks: associate callbacks with this endpoint :param external_docs: Link external documentation. Provide a dict with an "url" key and optionally a "description" key. For convenience, if only a string is given it is treated as the URL. :return: """ if methods is not None: methods = [method.upper() for method in methods] def decorator(f): BaseSchema = ( # explicit manually set schema or previous view annotation getattr(f, 'schema', None) # previously set schema with @extend_schema on views methods or getattr(f, 'kwargs', {}).get('schema', None) # previously set schema with @extend_schema on @api_view or getattr(getattr(f, 'cls', None), 'kwargs', {}).get('schema', None) # the default or api_settings.DEFAULT_SCHEMA_CLASS ) if not inspect.isclass(BaseSchema): BaseSchema = BaseSchema.__class__ def is_in_scope(ext_schema): version, _ = ext_schema.view.determine_version( ext_schema.view.request, **ext_schema.view.kwargs ) version_scope = versions is None or version in versions method_scope = methods is None or ext_schema.method in methods return method_scope and version_scope class ExtendedSchema(BaseSchema): def get_operation(self, path, path_regex, path_prefix, method, registry): self.method = method.upper() if operation is not None and is_in_scope(self): return operation return super().get_operation(path, path_regex, path_prefix, method, registry) def is_excluded(self): if exclude is not None and is_in_scope(self): return exclude return super().is_excluded() def get_operation_id(self): if operation_id and is_in_scope(self): return operation_id return super().get_operation_id() def get_override_parameters(self): if parameters and is_in_scope(self): return super().get_override_parameters() + parameters return super().get_override_parameters() def get_auth(self): if auth is not None and is_in_scope(self): return auth return super().get_auth() def get_examples(self): if examples and is_in_scope(self): return super().get_examples() + examples return super().get_examples() def get_request_serializer(self): if request is not empty and is_in_scope(self): return request return super().get_request_serializer() def get_response_serializers(self): if responses is not empty and is_in_scope(self): return responses return super().get_response_serializers() def get_description(self): if description and is_in_scope(self): return description return super().get_description() def get_summary(self): if summary and is_in_scope(self): return str(summary) return super().get_summary() def is_deprecated(self): if deprecated and is_in_scope(self): return deprecated return super().is_deprecated() def get_tags(self): if tags is not None and is_in_scope(self): return tags return super().get_tags() def get_extensions(self): if extensions and is_in_scope(self): return extensions return super().get_extensions() def get_filter_backends(self): if filters is not None and is_in_scope(self): return getattr(self.view, 'filter_backends', []) if filters else [] return super().get_filter_backends() def get_callbacks(self): if callbacks is not None and is_in_scope(self): return callbacks return super().get_callbacks() def get_external_docs(self): if external_docs is not None and is_in_scope(self): return external_docs return super().get_external_docs() if inspect.isclass(f): # either direct decoration of views, or unpacked @api_view from OpenApiViewExtension if operation_id is not None or operation is not None: error( f'using @extend_schema on viewset class {f.__name__} with parameters ' f'operation_id or operation will most likely result in a broken schema.', delayed=f, ) # reorder schema class MRO so that view method annotation takes precedence # over view class annotation. only relevant if there is a method annotation for view_method_name in get_view_method_names(view=f, schema=BaseSchema): if 'schema' not in getattr(getattr(f, view_method_name), 'kwargs', {}): continue view_method = isolate_view_method(f, view_method_name) view_method.kwargs['schema'] = type( 'ExtendedMetaSchema', (view_method.kwargs['schema'], ExtendedSchema), {} ) # persist schema on class to provide annotation to derived view methods. # the second purpose is to serve as base for view multi-annotation f.schema = ExtendedSchema() return f elif callable(f) and hasattr(f, 'cls'): # 'cls' attr signals that as_view() was called, which only applies to @api_view. # keep a "unused" schema reference at root level for multi annotation convenience. setattr(f.cls, 'kwargs', {'schema': ExtendedSchema}) # set schema on method kwargs context to emulate regular view behaviour. for method in f.cls.http_method_names: setattr(getattr(f.cls, method), 'kwargs', {'schema': ExtendedSchema}) return f elif callable(f): # custom actions have kwargs in their context, others don't. create it so our create_view # implementation can overwrite the default schema if not hasattr(f, 'kwargs'): f.kwargs = {} # this simulates what @action is actually doing. somewhere along the line in this process # the schema is picked up from kwargs and used. it's involved my dear friends. # use class instead of instance due to descriptor weakref reverse collisions f.kwargs['schema'] = ExtendedSchema return f else: return f return decorator def extend_schema_field( field: Union[_SerializerType, _FieldType, OpenApiTypes, _SchemaType], component_name: Optional[str] = None ) -> Callable[[F], F]: """ Decorator for the "field" kind. Can be used with ``SerializerMethodField`` (annotate the actual method) or with custom ``serializers.Field`` implementations. If your custom serializer field base class is already the desired type, decoration is not necessary. To override the discovered base class type, you can decorate your custom field class. Always takes precedence over other mechanisms (e.g. type hints, auto-discovery). :param field: accepts a ``Serializer``, :class:`~.types.OpenApiTypes` or raw ``dict`` :param component_name: signals that the field should be broken out as separate component """ def decorator(f): set_override(f, 'field', field) set_override(f, 'field_component_name', component_name) return f return decorator def extend_schema_serializer( many: Optional[bool] = None, exclude_fields: Optional[Sequence[str]] = None, deprecate_fields: Optional[Sequence[str]] = None, examples: Optional[Sequence[OpenApiExample]] = None, extensions: Optional[Dict[str, Any]] = None, component_name: Optional[str] = None, ) -> Callable[[F], F]: """ Decorator for the "serializer" kind. Intended for overriding default serializer behaviour that cannot be influenced through :func:`@extend_schema <.extend_schema>`. :param many: override how serializer is initialized. Mainly used to coerce the list view detection heuristic to acknowledge a non-list serializer. :param exclude_fields: fields to ignore while processing the serializer. only affects the schema. fields will still be exposed through the API. :param deprecate_fields: fields to mark as deprecated while processing the serializer. :param examples: define example data to serializer. :param extensions: specification extensions, e.g. ``x-is-dynamic``, etc. :param component_name: override default class name extraction. """ def decorator(klass): if many is not None: set_override(klass, 'many', many) if exclude_fields: set_override(klass, 'exclude_fields', exclude_fields) if deprecate_fields: set_override(klass, 'deprecate_fields', deprecate_fields) if examples: set_override(klass, 'examples', examples) if extensions: set_override(klass, 'extensions', extensions) if component_name: set_override(klass, 'component_name', component_name) return klass return decorator def extend_schema_view(**kwargs) -> Callable[[F], F]: """ Convenience decorator for the "view" kind. Intended for annotating derived view methods that are are not directly present in the view (usually methods like ``list`` or ``retrieve``). Spares you from overriding methods like ``list``, only to perform a super call in the body so that you have have something to attach :func:`@extend_schema <.extend_schema>` to. This decorator also takes care of safely attaching annotations to derived view methods, preventing leakage into unrelated views. :param kwargs: method names as argument names and :func:`@extend_schema <.extend_schema>` calls as values """ def decorator(view): # special case for @api_view. redirect decoration to enclosed WrappedAPIView if callable(view) and hasattr(view, 'cls'): extend_schema_view(**kwargs)(view.cls) return view available_view_methods = get_view_method_names(view) for method_name, method_decorator in kwargs.items(): if method_name not in available_view_methods: warn( f'@extend_schema_view argument "{method_name}" was not found on view ' f'{view.__name__}. method override for "{method_name}" will be ignored.', delayed=view ) continue # the context of derived methods must not be altered, as it belongs to the # other view. create a new context so the schema can be safely stored in the # wrapped_method. view methods that are not derived can be safely altered. if hasattr(method_decorator, '__iter__'): for sub_method_decorator in method_decorator: sub_method_decorator(isolate_view_method(view, method_name)) else: method_decorator(isolate_view_method(view, method_name)) return view return decorator def inline_serializer(name: str, fields: Dict[str, Field], **kwargs) -> Serializer: """ A helper function to create an inline serializer. Primary use is with :func:`@extend_schema <.extend_schema>`, where one needs an implicit one-off serializer that is not reflected in an actual class. :param name: name of the :param fields: dict with field names as keys and serializer fields as values :param kwargs: optional kwargs for serializer initialization """ serializer_class = type(name, (Serializer,), fields) return serializer_class(**kwargs) drf-spectacular-0.27.0/drf_spectacular/validation/000077500000000000000000000000001453572150400221605ustar00rootroot00000000000000drf-spectacular-0.27.0/drf_spectacular/validation/__init__.py000066400000000000000000000026461453572150400243010ustar00rootroot00000000000000import json import os import jsonschema def validate_schema(api_schema): """ Validate generated API schema against OpenAPI 3.0.X json schema specification. Note: On conflict, the written specification always wins over the json schema. OpenApi3 schema specification taken from: https://github.com/OAI/OpenAPI-Specification/blob/master/schemas/v3.0/schema.json https://github.com/OAI/OpenAPI-Specification/blob/9dff244e5708fbe16e768738f4f17cf3fddf4066/schemas/v3.0/schema.json https://github.com/OAI/OpenAPI-Specification/blob/main/schemas/v3.1/schema.json https://github.com/OAI/OpenAPI-Specification/blob/9dff244e5708fbe16e768738f4f17cf3fddf4066/schemas/v3.1/schema.json """ if api_schema['openapi'].startswith("3.0"): schema_spec_path = os.path.join(os.path.dirname(__file__), 'openapi_3_0_schema.json') elif api_schema['openapi'].startswith("3.1"): schema_spec_path = os.path.join(os.path.dirname(__file__), 'openapi_3_1_schema.json') else: raise RuntimeError('No validation specification available') # pragma: no cover with open(schema_spec_path) as fh: openapi3_schema_spec = json.load(fh) # coerce any remnants of objects to basic types from drf_spectacular.renderers import OpenApiJsonRenderer api_schema = json.loads(OpenApiJsonRenderer().render(api_schema)) jsonschema.validate(instance=api_schema, schema=openapi3_schema_spec) drf-spectacular-0.27.0/drf_spectacular/validation/openapi_3_0_schema.json000066400000000000000000001057501453572150400264770ustar00rootroot00000000000000{ "id": "https://spec.openapis.org/oas/3.0/schema/2021-09-28", "$schema": "http://json-schema.org/draft-04/schema#", "description": "The description of OpenAPI v3.0.x documents, as defined by https://spec.openapis.org/oas/v3.0.3", "type": "object", "required": [ "openapi", "info", "paths" ], "properties": { "openapi": { "type": "string", "pattern": "^3\\.0\\.\\d(-.+)?$" }, "info": { "$ref": "#/definitions/Info" }, "externalDocs": { "$ref": "#/definitions/ExternalDocumentation" }, "servers": { "type": "array", "items": { "$ref": "#/definitions/Server" } }, "security": { "type": "array", "items": { "$ref": "#/definitions/SecurityRequirement" } }, "tags": { "type": "array", "items": { "$ref": "#/definitions/Tag" }, "uniqueItems": true }, "paths": { "$ref": "#/definitions/Paths" }, "components": { "$ref": "#/definitions/Components" } }, "patternProperties": { "^x-": { } }, "additionalProperties": false, "definitions": { "Reference": { "type": "object", "required": [ "$ref" ], "patternProperties": { "^\\$ref$": { "type": "string", "format": "uri-reference" } } }, "Info": { "type": "object", "required": [ "title", "version" ], "properties": { "title": { "type": "string" }, "description": { "type": "string" }, "termsOfService": { "type": "string", "format": "uri-reference" }, "contact": { "$ref": "#/definitions/Contact" }, "license": { "$ref": "#/definitions/License" }, "version": { "type": "string" } }, "patternProperties": { "^x-": { } }, "additionalProperties": false }, "Contact": { "type": "object", "properties": { "name": { "type": "string" }, "url": { "type": "string", "format": "uri-reference" }, "email": { "type": "string", "format": "email" } }, "patternProperties": { "^x-": { } }, "additionalProperties": false }, "License": { "type": "object", "required": [ "name" ], "properties": { "name": { "type": "string" }, "url": { "type": "string", "format": "uri-reference" } }, "patternProperties": { "^x-": { } }, "additionalProperties": false }, "Server": { "type": "object", "required": [ "url" ], "properties": { "url": { "type": "string" }, "description": { "type": "string" }, "variables": { "type": "object", "additionalProperties": { "$ref": "#/definitions/ServerVariable" } } }, "patternProperties": { "^x-": { } }, "additionalProperties": false }, "ServerVariable": { "type": "object", "required": [ "default" ], "properties": { "enum": { "type": "array", "items": { "type": "string" } }, "default": { "type": "string" }, "description": { "type": "string" } }, "patternProperties": { "^x-": { } }, "additionalProperties": false }, "Components": { "type": "object", "properties": { "schemas": { "type": "object", "patternProperties": { "^[a-zA-Z0-9\\.\\-_]+$": { "oneOf": [ { "$ref": "#/definitions/Schema" }, { "$ref": "#/definitions/Reference" } ] } } }, "responses": { "type": "object", "patternProperties": { "^[a-zA-Z0-9\\.\\-_]+$": { "oneOf": [ { "$ref": "#/definitions/Reference" }, { "$ref": "#/definitions/Response" } ] } } }, "parameters": { "type": "object", "patternProperties": { "^[a-zA-Z0-9\\.\\-_]+$": { "oneOf": [ { "$ref": "#/definitions/Reference" }, { "$ref": "#/definitions/Parameter" } ] } } }, "examples": { "type": "object", "patternProperties": { "^[a-zA-Z0-9\\.\\-_]+$": { "oneOf": [ { "$ref": "#/definitions/Reference" }, { "$ref": "#/definitions/Example" } ] } } }, "requestBodies": { "type": "object", "patternProperties": { "^[a-zA-Z0-9\\.\\-_]+$": { "oneOf": [ { "$ref": "#/definitions/Reference" }, { "$ref": "#/definitions/RequestBody" } ] } } }, "headers": { "type": "object", "patternProperties": { "^[a-zA-Z0-9\\.\\-_]+$": { "oneOf": [ { "$ref": "#/definitions/Reference" }, { "$ref": "#/definitions/Header" } ] } } }, "securitySchemes": { "type": "object", "patternProperties": { "^[a-zA-Z0-9\\.\\-_]+$": { "oneOf": [ { "$ref": "#/definitions/Reference" }, { "$ref": "#/definitions/SecurityScheme" } ] } } }, "links": { "type": "object", "patternProperties": { "^[a-zA-Z0-9\\.\\-_]+$": { "oneOf": [ { "$ref": "#/definitions/Reference" }, { "$ref": "#/definitions/Link" } ] } } }, "callbacks": { "type": "object", "patternProperties": { "^[a-zA-Z0-9\\.\\-_]+$": { "oneOf": [ { "$ref": "#/definitions/Reference" }, { "$ref": "#/definitions/Callback" } ] } } } }, "patternProperties": { "^x-": { } }, "additionalProperties": false }, "Schema": { "type": "object", "properties": { "title": { "type": "string" }, "multipleOf": { "type": "number", "minimum": 0, "exclusiveMinimum": true }, "maximum": { "type": "number" }, "exclusiveMaximum": { "type": "boolean", "default": false }, "minimum": { "type": "number" }, "exclusiveMinimum": { "type": "boolean", "default": false }, "maxLength": { "type": "integer", "minimum": 0 }, "minLength": { "type": "integer", "minimum": 0, "default": 0 }, "pattern": { "type": "string", "format": "regex" }, "maxItems": { "type": "integer", "minimum": 0 }, "minItems": { "type": "integer", "minimum": 0, "default": 0 }, "uniqueItems": { "type": "boolean", "default": false }, "maxProperties": { "type": "integer", "minimum": 0 }, "minProperties": { "type": "integer", "minimum": 0, "default": 0 }, "required": { "type": "array", "items": { "type": "string" }, "minItems": 1, "uniqueItems": true }, "enum": { "type": "array", "items": { }, "minItems": 1, "uniqueItems": false }, "type": { "type": "string", "enum": [ "array", "boolean", "integer", "number", "object", "string" ] }, "not": { "oneOf": [ { "$ref": "#/definitions/Schema" }, { "$ref": "#/definitions/Reference" } ] }, "allOf": { "type": "array", "items": { "oneOf": [ { "$ref": "#/definitions/Schema" }, { "$ref": "#/definitions/Reference" } ] } }, "oneOf": { "type": "array", "items": { "oneOf": [ { "$ref": "#/definitions/Schema" }, { "$ref": "#/definitions/Reference" } ] } }, "anyOf": { "type": "array", "items": { "oneOf": [ { "$ref": "#/definitions/Schema" }, { "$ref": "#/definitions/Reference" } ] } }, "items": { "oneOf": [ { "$ref": "#/definitions/Schema" }, { "$ref": "#/definitions/Reference" } ] }, "properties": { "type": "object", "additionalProperties": { "oneOf": [ { "$ref": "#/definitions/Schema" }, { "$ref": "#/definitions/Reference" } ] } }, "additionalProperties": { "oneOf": [ { "$ref": "#/definitions/Schema" }, { "$ref": "#/definitions/Reference" }, { "type": "boolean" } ], "default": true }, "description": { "type": "string" }, "format": { "type": "string" }, "default": { }, "nullable": { "type": "boolean", "default": false }, "discriminator": { "$ref": "#/definitions/Discriminator" }, "readOnly": { "type": "boolean", "default": false }, "writeOnly": { "type": "boolean", "default": false }, "example": { }, "externalDocs": { "$ref": "#/definitions/ExternalDocumentation" }, "deprecated": { "type": "boolean", "default": false }, "xml": { "$ref": "#/definitions/XML" } }, "patternProperties": { "^x-": { } }, "additionalProperties": false }, "Discriminator": { "type": "object", "required": [ "propertyName" ], "properties": { "propertyName": { "type": "string" }, "mapping": { "type": "object", "additionalProperties": { "type": "string" } } } }, "XML": { "type": "object", "properties": { "name": { "type": "string" }, "namespace": { "type": "string", "format": "uri" }, "prefix": { "type": "string" }, "attribute": { "type": "boolean", "default": false }, "wrapped": { "type": "boolean", "default": false } }, "patternProperties": { "^x-": { } }, "additionalProperties": false }, "Response": { "type": "object", "required": [ "description" ], "properties": { "description": { "type": "string" }, "headers": { "type": "object", "additionalProperties": { "oneOf": [ { "$ref": "#/definitions/Header" }, { "$ref": "#/definitions/Reference" } ] } }, "content": { "type": "object", "additionalProperties": { "$ref": "#/definitions/MediaType" } }, "links": { "type": "object", "additionalProperties": { "oneOf": [ { "$ref": "#/definitions/Link" }, { "$ref": "#/definitions/Reference" } ] } } }, "patternProperties": { "^x-": { } }, "additionalProperties": false }, "MediaType": { "type": "object", "properties": { "schema": { "oneOf": [ { "$ref": "#/definitions/Schema" }, { "$ref": "#/definitions/Reference" } ] }, "example": { }, "examples": { "type": "object", "additionalProperties": { "oneOf": [ { "$ref": "#/definitions/Example" }, { "$ref": "#/definitions/Reference" } ] } }, "encoding": { "type": "object", "additionalProperties": { "$ref": "#/definitions/Encoding" } } }, "patternProperties": { "^x-": { } }, "additionalProperties": false, "allOf": [ { "$ref": "#/definitions/ExampleXORExamples" } ] }, "Example": { "type": "object", "properties": { "summary": { "type": "string" }, "description": { "type": "string" }, "value": { }, "externalValue": { "type": "string", "format": "uri-reference" } }, "patternProperties": { "^x-": { } }, "additionalProperties": false }, "Header": { "type": "object", "properties": { "description": { "type": "string" }, "required": { "type": "boolean", "default": false }, "deprecated": { "type": "boolean", "default": false }, "allowEmptyValue": { "type": "boolean", "default": false }, "style": { "type": "string", "enum": [ "simple" ], "default": "simple" }, "explode": { "type": "boolean" }, "allowReserved": { "type": "boolean", "default": false }, "schema": { "oneOf": [ { "$ref": "#/definitions/Schema" }, { "$ref": "#/definitions/Reference" } ] }, "content": { "type": "object", "additionalProperties": { "$ref": "#/definitions/MediaType" }, "minProperties": 1, "maxProperties": 1 }, "example": { }, "examples": { "type": "object", "additionalProperties": { "oneOf": [ { "$ref": "#/definitions/Example" }, { "$ref": "#/definitions/Reference" } ] } } }, "patternProperties": { "^x-": { } }, "additionalProperties": false, "allOf": [ { "$ref": "#/definitions/ExampleXORExamples" }, { "$ref": "#/definitions/SchemaXORContent" } ] }, "Paths": { "type": "object", "patternProperties": { "^\\/": { "$ref": "#/definitions/PathItem" }, "^x-": { } }, "additionalProperties": false }, "PathItem": { "type": "object", "properties": { "$ref": { "type": "string" }, "summary": { "type": "string" }, "description": { "type": "string" }, "servers": { "type": "array", "items": { "$ref": "#/definitions/Server" } }, "parameters": { "type": "array", "items": { "oneOf": [ { "$ref": "#/definitions/Parameter" }, { "$ref": "#/definitions/Reference" } ] }, "uniqueItems": true } }, "patternProperties": { "^(get|put|post|delete|options|head|patch|trace)$": { "$ref": "#/definitions/Operation" }, "^x-": { } }, "additionalProperties": false }, "Operation": { "type": "object", "required": [ "responses" ], "properties": { "tags": { "type": "array", "items": { "type": "string" } }, "summary": { "type": "string" }, "description": { "type": "string" }, "externalDocs": { "$ref": "#/definitions/ExternalDocumentation" }, "operationId": { "type": "string" }, "parameters": { "type": "array", "items": { "oneOf": [ { "$ref": "#/definitions/Parameter" }, { "$ref": "#/definitions/Reference" } ] }, "uniqueItems": true }, "requestBody": { "oneOf": [ { "$ref": "#/definitions/RequestBody" }, { "$ref": "#/definitions/Reference" } ] }, "responses": { "$ref": "#/definitions/Responses" }, "callbacks": { "type": "object", "additionalProperties": { "oneOf": [ { "$ref": "#/definitions/Callback" }, { "$ref": "#/definitions/Reference" } ] } }, "deprecated": { "type": "boolean", "default": false }, "security": { "type": "array", "items": { "$ref": "#/definitions/SecurityRequirement" } }, "servers": { "type": "array", "items": { "$ref": "#/definitions/Server" } } }, "patternProperties": { "^x-": { } }, "additionalProperties": false }, "Responses": { "type": "object", "properties": { "default": { "oneOf": [ { "$ref": "#/definitions/Response" }, { "$ref": "#/definitions/Reference" } ] } }, "patternProperties": { "^[1-5](?:\\d{2}|XX)$": { "oneOf": [ { "$ref": "#/definitions/Response" }, { "$ref": "#/definitions/Reference" } ] }, "^x-": { } }, "minProperties": 1, "additionalProperties": false }, "SecurityRequirement": { "type": "object", "additionalProperties": { "type": "array", "items": { "type": "string" } } }, "Tag": { "type": "object", "required": [ "name" ], "properties": { "name": { "type": "string" }, "description": { "type": "string" }, "externalDocs": { "$ref": "#/definitions/ExternalDocumentation" } }, "patternProperties": { "^x-": { } }, "additionalProperties": false }, "ExternalDocumentation": { "type": "object", "required": [ "url" ], "properties": { "description": { "type": "string" }, "url": { "type": "string", "format": "uri-reference" } }, "patternProperties": { "^x-": { } }, "additionalProperties": false }, "ExampleXORExamples": { "description": "Example and examples are mutually exclusive", "not": { "required": [ "example", "examples" ] } }, "SchemaXORContent": { "description": "Schema and content are mutually exclusive, at least one is required", "not": { "required": [ "schema", "content" ] }, "oneOf": [ { "required": [ "schema" ] }, { "required": [ "content" ], "description": "Some properties are not allowed if content is present", "allOf": [ { "not": { "required": [ "style" ] } }, { "not": { "required": [ "explode" ] } }, { "not": { "required": [ "allowReserved" ] } }, { "not": { "required": [ "example" ] } }, { "not": { "required": [ "examples" ] } } ] } ] }, "Parameter": { "type": "object", "properties": { "name": { "type": "string" }, "in": { "type": "string" }, "description": { "type": "string" }, "required": { "type": "boolean", "default": false }, "deprecated": { "type": "boolean", "default": false }, "allowEmptyValue": { "type": "boolean", "default": false }, "style": { "type": "string" }, "explode": { "type": "boolean" }, "allowReserved": { "type": "boolean", "default": false }, "schema": { "oneOf": [ { "$ref": "#/definitions/Schema" }, { "$ref": "#/definitions/Reference" } ] }, "content": { "type": "object", "additionalProperties": { "$ref": "#/definitions/MediaType" }, "minProperties": 1, "maxProperties": 1 }, "example": { }, "examples": { "type": "object", "additionalProperties": { "oneOf": [ { "$ref": "#/definitions/Example" }, { "$ref": "#/definitions/Reference" } ] } } }, "patternProperties": { "^x-": { } }, "additionalProperties": false, "required": [ "name", "in" ], "allOf": [ { "$ref": "#/definitions/ExampleXORExamples" }, { "$ref": "#/definitions/SchemaXORContent" }, { "$ref": "#/definitions/ParameterLocation" } ] }, "ParameterLocation": { "description": "Parameter location", "oneOf": [ { "description": "Parameter in path", "required": [ "required" ], "properties": { "in": { "enum": [ "path" ] }, "style": { "enum": [ "matrix", "label", "simple" ], "default": "simple" }, "required": { "enum": [ true ] } } }, { "description": "Parameter in query", "properties": { "in": { "enum": [ "query" ] }, "style": { "enum": [ "form", "spaceDelimited", "pipeDelimited", "deepObject" ], "default": "form" } } }, { "description": "Parameter in header", "properties": { "in": { "enum": [ "header" ] }, "style": { "enum": [ "simple" ], "default": "simple" } } }, { "description": "Parameter in cookie", "properties": { "in": { "enum": [ "cookie" ] }, "style": { "enum": [ "form" ], "default": "form" } } } ] }, "RequestBody": { "type": "object", "required": [ "content" ], "properties": { "description": { "type": "string" }, "content": { "type": "object", "additionalProperties": { "$ref": "#/definitions/MediaType" } }, "required": { "type": "boolean", "default": false } }, "patternProperties": { "^x-": { } }, "additionalProperties": false }, "SecurityScheme": { "oneOf": [ { "$ref": "#/definitions/APIKeySecurityScheme" }, { "$ref": "#/definitions/HTTPSecurityScheme" }, { "$ref": "#/definitions/OAuth2SecurityScheme" }, { "$ref": "#/definitions/OpenIdConnectSecurityScheme" } ] }, "APIKeySecurityScheme": { "type": "object", "required": [ "type", "name", "in" ], "properties": { "type": { "type": "string", "enum": [ "apiKey" ] }, "name": { "type": "string" }, "in": { "type": "string", "enum": [ "header", "query", "cookie" ] }, "description": { "type": "string" } }, "patternProperties": { "^x-": { } }, "additionalProperties": false }, "HTTPSecurityScheme": { "type": "object", "required": [ "scheme", "type" ], "properties": { "scheme": { "type": "string" }, "bearerFormat": { "type": "string" }, "description": { "type": "string" }, "type": { "type": "string", "enum": [ "http" ] } }, "patternProperties": { "^x-": { } }, "additionalProperties": false, "oneOf": [ { "description": "Bearer", "properties": { "scheme": { "type": "string", "pattern": "^[Bb][Ee][Aa][Rr][Ee][Rr]$" } } }, { "description": "Non Bearer", "not": { "required": [ "bearerFormat" ] }, "properties": { "scheme": { "not": { "type": "string", "pattern": "^[Bb][Ee][Aa][Rr][Ee][Rr]$" } } } } ] }, "OAuth2SecurityScheme": { "type": "object", "required": [ "type", "flows" ], "properties": { "type": { "type": "string", "enum": [ "oauth2" ] }, "flows": { "$ref": "#/definitions/OAuthFlows" }, "description": { "type": "string" } }, "patternProperties": { "^x-": { } }, "additionalProperties": false }, "OpenIdConnectSecurityScheme": { "type": "object", "required": [ "type", "openIdConnectUrl" ], "properties": { "type": { "type": "string", "enum": [ "openIdConnect" ] }, "openIdConnectUrl": { "type": "string", "format": "uri-reference" }, "description": { "type": "string" } }, "patternProperties": { "^x-": { } }, "additionalProperties": false }, "OAuthFlows": { "type": "object", "properties": { "implicit": { "$ref": "#/definitions/ImplicitOAuthFlow" }, "password": { "$ref": "#/definitions/PasswordOAuthFlow" }, "clientCredentials": { "$ref": "#/definitions/ClientCredentialsFlow" }, "authorizationCode": { "$ref": "#/definitions/AuthorizationCodeOAuthFlow" } }, "patternProperties": { "^x-": { } }, "additionalProperties": false }, "ImplicitOAuthFlow": { "type": "object", "required": [ "authorizationUrl", "scopes" ], "properties": { "authorizationUrl": { "type": "string", "format": "uri-reference" }, "refreshUrl": { "type": "string", "format": "uri-reference" }, "scopes": { "type": "object", "additionalProperties": { "type": "string" } } }, "patternProperties": { "^x-": { } }, "additionalProperties": false }, "PasswordOAuthFlow": { "type": "object", "required": [ "tokenUrl", "scopes" ], "properties": { "tokenUrl": { "type": "string", "format": "uri-reference" }, "refreshUrl": { "type": "string", "format": "uri-reference" }, "scopes": { "type": "object", "additionalProperties": { "type": "string" } } }, "patternProperties": { "^x-": { } }, "additionalProperties": false }, "ClientCredentialsFlow": { "type": "object", "required": [ "tokenUrl", "scopes" ], "properties": { "tokenUrl": { "type": "string", "format": "uri-reference" }, "refreshUrl": { "type": "string", "format": "uri-reference" }, "scopes": { "type": "object", "additionalProperties": { "type": "string" } } }, "patternProperties": { "^x-": { } }, "additionalProperties": false }, "AuthorizationCodeOAuthFlow": { "type": "object", "required": [ "authorizationUrl", "tokenUrl", "scopes" ], "properties": { "authorizationUrl": { "type": "string", "format": "uri-reference" }, "tokenUrl": { "type": "string", "format": "uri-reference" }, "refreshUrl": { "type": "string", "format": "uri-reference" }, "scopes": { "type": "object", "additionalProperties": { "type": "string" } } }, "patternProperties": { "^x-": { } }, "additionalProperties": false }, "Link": { "type": "object", "properties": { "operationId": { "type": "string" }, "operationRef": { "type": "string", "format": "uri-reference" }, "parameters": { "type": "object", "additionalProperties": { } }, "requestBody": { }, "description": { "type": "string" }, "server": { "$ref": "#/definitions/Server" } }, "patternProperties": { "^x-": { } }, "additionalProperties": false, "not": { "description": "Operation Id and Operation Ref are mutually exclusive", "required": [ "operationId", "operationRef" ] } }, "Callback": { "type": "object", "additionalProperties": { "$ref": "#/definitions/PathItem" }, "patternProperties": { "^x-": { } } }, "Encoding": { "type": "object", "properties": { "contentType": { "type": "string" }, "headers": { "type": "object", "additionalProperties": { "oneOf": [ { "$ref": "#/definitions/Header" }, { "$ref": "#/definitions/Reference" } ] } }, "style": { "type": "string", "enum": [ "form", "spaceDelimited", "pipeDelimited", "deepObject" ] }, "explode": { "type": "boolean" }, "allowReserved": { "type": "boolean", "default": false } }, "patternProperties": { "^x-": { } }, "additionalProperties": false } } }drf-spectacular-0.27.0/drf_spectacular/validation/openapi_3_1_schema.json000066400000000000000000001027061453572150400264760ustar00rootroot00000000000000{ "$id": "https://spec.openapis.org/oas/3.1/schema/2022-10-07", "$schema": "https://json-schema.org/draft/2020-12/schema", "description": "The description of OpenAPI v3.1.x documents without schema validation, as defined by https://spec.openapis.org/oas/v3.1.0", "type": "object", "properties": { "openapi": { "type": "string", "pattern": "^3\\.1\\.\\d+(-.+)?$" }, "info": { "$ref": "#/$defs/info" }, "jsonSchemaDialect": { "type": "string", "format": "uri", "default": "https://spec.openapis.org/oas/3.1/dialect/base" }, "servers": { "type": "array", "items": { "$ref": "#/$defs/server" }, "default": [ { "url": "/" } ] }, "paths": { "$ref": "#/$defs/paths" }, "webhooks": { "type": "object", "additionalProperties": { "$ref": "#/$defs/path-item-or-reference" } }, "components": { "$ref": "#/$defs/components" }, "security": { "type": "array", "items": { "$ref": "#/$defs/security-requirement" } }, "tags": { "type": "array", "items": { "$ref": "#/$defs/tag" } }, "externalDocs": { "$ref": "#/$defs/external-documentation" } }, "required": [ "openapi", "info" ], "anyOf": [ { "required": [ "paths" ] }, { "required": [ "components" ] }, { "required": [ "webhooks" ] } ], "$ref": "#/$defs/specification-extensions", "unevaluatedProperties": false, "$defs": { "info": { "$comment": "https://spec.openapis.org/oas/v3.1.0#info-object", "type": "object", "properties": { "title": { "type": "string" }, "summary": { "type": "string" }, "description": { "type": "string" }, "termsOfService": { "type": "string", "format": "uri" }, "contact": { "$ref": "#/$defs/contact" }, "license": { "$ref": "#/$defs/license" }, "version": { "type": "string" } }, "required": [ "title", "version" ], "$ref": "#/$defs/specification-extensions", "unevaluatedProperties": false }, "contact": { "$comment": "https://spec.openapis.org/oas/v3.1.0#contact-object", "type": "object", "properties": { "name": { "type": "string" }, "url": { "type": "string", "format": "uri" }, "email": { "type": "string", "format": "email" } }, "$ref": "#/$defs/specification-extensions", "unevaluatedProperties": false }, "license": { "$comment": "https://spec.openapis.org/oas/v3.1.0#license-object", "type": "object", "properties": { "name": { "type": "string" }, "identifier": { "type": "string" }, "url": { "type": "string", "format": "uri" } }, "required": [ "name" ], "dependentSchemas": { "identifier": { "not": { "required": [ "url" ] } } }, "$ref": "#/$defs/specification-extensions", "unevaluatedProperties": false }, "server": { "$comment": "https://spec.openapis.org/oas/v3.1.0#server-object", "type": "object", "properties": { "url": { "type": "string", "format": "uri-reference" }, "description": { "type": "string" }, "variables": { "type": "object", "additionalProperties": { "$ref": "#/$defs/server-variable" } } }, "required": [ "url" ], "$ref": "#/$defs/specification-extensions", "unevaluatedProperties": false }, "server-variable": { "$comment": "https://spec.openapis.org/oas/v3.1.0#server-variable-object", "type": "object", "properties": { "enum": { "type": "array", "items": { "type": "string" }, "minItems": 1 }, "default": { "type": "string" }, "description": { "type": "string" } }, "required": [ "default" ], "$ref": "#/$defs/specification-extensions", "unevaluatedProperties": false }, "components": { "$comment": "https://spec.openapis.org/oas/v3.1.0#components-object", "type": "object", "properties": { "schemas": { "type": "object", "additionalProperties": { "$dynamicRef": "#meta" } }, "responses": { "type": "object", "additionalProperties": { "$ref": "#/$defs/response-or-reference" } }, "parameters": { "type": "object", "additionalProperties": { "$ref": "#/$defs/parameter-or-reference" } }, "examples": { "type": "object", "additionalProperties": { "$ref": "#/$defs/example-or-reference" } }, "requestBodies": { "type": "object", "additionalProperties": { "$ref": "#/$defs/request-body-or-reference" } }, "headers": { "type": "object", "additionalProperties": { "$ref": "#/$defs/header-or-reference" } }, "securitySchemes": { "type": "object", "additionalProperties": { "$ref": "#/$defs/security-scheme-or-reference" } }, "links": { "type": "object", "additionalProperties": { "$ref": "#/$defs/link-or-reference" } }, "callbacks": { "type": "object", "additionalProperties": { "$ref": "#/$defs/callbacks-or-reference" } }, "pathItems": { "type": "object", "additionalProperties": { "$ref": "#/$defs/path-item-or-reference" } } }, "patternProperties": { "^(schemas|responses|parameters|examples|requestBodies|headers|securitySchemes|links|callbacks|pathItems)$": { "$comment": "Enumerating all of the property names in the regex above is necessary for unevaluatedProperties to work as expected", "propertyNames": { "pattern": "^[a-zA-Z0-9._-]+$" } } }, "$ref": "#/$defs/specification-extensions", "unevaluatedProperties": false }, "paths": { "$comment": "https://spec.openapis.org/oas/v3.1.0#paths-object", "type": "object", "patternProperties": { "^/": { "$ref": "#/$defs/path-item" } }, "$ref": "#/$defs/specification-extensions", "unevaluatedProperties": false }, "path-item": { "$comment": "https://spec.openapis.org/oas/v3.1.0#path-item-object", "type": "object", "properties": { "summary": { "type": "string" }, "description": { "type": "string" }, "servers": { "type": "array", "items": { "$ref": "#/$defs/server" } }, "parameters": { "type": "array", "items": { "$ref": "#/$defs/parameter-or-reference" } }, "get": { "$ref": "#/$defs/operation" }, "put": { "$ref": "#/$defs/operation" }, "post": { "$ref": "#/$defs/operation" }, "delete": { "$ref": "#/$defs/operation" }, "options": { "$ref": "#/$defs/operation" }, "head": { "$ref": "#/$defs/operation" }, "patch": { "$ref": "#/$defs/operation" }, "trace": { "$ref": "#/$defs/operation" } }, "$ref": "#/$defs/specification-extensions", "unevaluatedProperties": false }, "path-item-or-reference": { "if": { "type": "object", "required": [ "$ref" ] }, "then": { "$ref": "#/$defs/reference" }, "else": { "$ref": "#/$defs/path-item" } }, "operation": { "$comment": "https://spec.openapis.org/oas/v3.1.0#operation-object", "type": "object", "properties": { "tags": { "type": "array", "items": { "type": "string" } }, "summary": { "type": "string" }, "description": { "type": "string" }, "externalDocs": { "$ref": "#/$defs/external-documentation" }, "operationId": { "type": "string" }, "parameters": { "type": "array", "items": { "$ref": "#/$defs/parameter-or-reference" } }, "requestBody": { "$ref": "#/$defs/request-body-or-reference" }, "responses": { "$ref": "#/$defs/responses" }, "callbacks": { "type": "object", "additionalProperties": { "$ref": "#/$defs/callbacks-or-reference" } }, "deprecated": { "default": false, "type": "boolean" }, "security": { "type": "array", "items": { "$ref": "#/$defs/security-requirement" } }, "servers": { "type": "array", "items": { "$ref": "#/$defs/server" } } }, "$ref": "#/$defs/specification-extensions", "unevaluatedProperties": false }, "external-documentation": { "$comment": "https://spec.openapis.org/oas/v3.1.0#external-documentation-object", "type": "object", "properties": { "description": { "type": "string" }, "url": { "type": "string", "format": "uri" } }, "required": [ "url" ], "$ref": "#/$defs/specification-extensions", "unevaluatedProperties": false }, "parameter": { "$comment": "https://spec.openapis.org/oas/v3.1.0#parameter-object", "type": "object", "properties": { "name": { "type": "string" }, "in": { "enum": [ "query", "header", "path", "cookie" ] }, "description": { "type": "string" }, "required": { "default": false, "type": "boolean" }, "deprecated": { "default": false, "type": "boolean" }, "schema": { "$dynamicRef": "#meta" }, "content": { "$ref": "#/$defs/content", "minProperties": 1, "maxProperties": 1 } }, "required": [ "name", "in" ], "oneOf": [ { "required": [ "schema" ] }, { "required": [ "content" ] } ], "if": { "properties": { "in": { "const": "query" } }, "required": [ "in" ] }, "then": { "properties": { "allowEmptyValue": { "default": false, "type": "boolean" } } }, "dependentSchemas": { "schema": { "properties": { "style": { "type": "string" }, "explode": { "type": "boolean" } }, "allOf": [ { "$ref": "#/$defs/examples" }, { "$ref": "#/$defs/parameter/dependentSchemas/schema/$defs/styles-for-path" }, { "$ref": "#/$defs/parameter/dependentSchemas/schema/$defs/styles-for-header" }, { "$ref": "#/$defs/parameter/dependentSchemas/schema/$defs/styles-for-query" }, { "$ref": "#/$defs/parameter/dependentSchemas/schema/$defs/styles-for-cookie" }, { "$ref": "#/$defs/parameter/dependentSchemas/schema/$defs/styles-for-form" } ], "$defs": { "styles-for-path": { "if": { "properties": { "in": { "const": "path" } }, "required": [ "in" ] }, "then": { "properties": { "name": { "pattern": "[^/#?]+$" }, "style": { "default": "simple", "enum": [ "matrix", "label", "simple" ] }, "required": { "const": true } }, "required": [ "required" ] } }, "styles-for-header": { "if": { "properties": { "in": { "const": "header" } }, "required": [ "in" ] }, "then": { "properties": { "style": { "default": "simple", "const": "simple" } } } }, "styles-for-query": { "if": { "properties": { "in": { "const": "query" } }, "required": [ "in" ] }, "then": { "properties": { "style": { "default": "form", "enum": [ "form", "spaceDelimited", "pipeDelimited", "deepObject" ] }, "allowReserved": { "default": false, "type": "boolean" } } } }, "styles-for-cookie": { "if": { "properties": { "in": { "const": "cookie" } }, "required": [ "in" ] }, "then": { "properties": { "style": { "default": "form", "const": "form" } } } }, "styles-for-form": { "if": { "properties": { "style": { "const": "form" } }, "required": [ "style" ] }, "then": { "properties": { "explode": { "default": true } } }, "else": { "properties": { "explode": { "default": false } } } } } } }, "$ref": "#/$defs/specification-extensions", "unevaluatedProperties": false }, "parameter-or-reference": { "if": { "type": "object", "required": [ "$ref" ] }, "then": { "$ref": "#/$defs/reference" }, "else": { "$ref": "#/$defs/parameter" } }, "request-body": { "$comment": "https://spec.openapis.org/oas/v3.1.0#request-body-object", "type": "object", "properties": { "description": { "type": "string" }, "content": { "$ref": "#/$defs/content" }, "required": { "default": false, "type": "boolean" } }, "required": [ "content" ], "$ref": "#/$defs/specification-extensions", "unevaluatedProperties": false }, "request-body-or-reference": { "if": { "type": "object", "required": [ "$ref" ] }, "then": { "$ref": "#/$defs/reference" }, "else": { "$ref": "#/$defs/request-body" } }, "content": { "$comment": "https://spec.openapis.org/oas/v3.1.0#fixed-fields-10", "type": "object", "additionalProperties": { "$ref": "#/$defs/media-type" }, "propertyNames": { "format": "media-range" } }, "media-type": { "$comment": "https://spec.openapis.org/oas/v3.1.0#media-type-object", "type": "object", "properties": { "schema": { "$dynamicRef": "#meta" }, "encoding": { "type": "object", "additionalProperties": { "$ref": "#/$defs/encoding" } } }, "allOf": [ { "$ref": "#/$defs/specification-extensions" }, { "$ref": "#/$defs/examples" } ], "unevaluatedProperties": false }, "encoding": { "$comment": "https://spec.openapis.org/oas/v3.1.0#encoding-object", "type": "object", "properties": { "contentType": { "type": "string", "format": "media-range" }, "headers": { "type": "object", "additionalProperties": { "$ref": "#/$defs/header-or-reference" } }, "style": { "default": "form", "enum": [ "form", "spaceDelimited", "pipeDelimited", "deepObject" ] }, "explode": { "type": "boolean" }, "allowReserved": { "default": false, "type": "boolean" } }, "allOf": [ { "$ref": "#/$defs/specification-extensions" }, { "$ref": "#/$defs/encoding/$defs/explode-default" } ], "unevaluatedProperties": false, "$defs": { "explode-default": { "if": { "properties": { "style": { "const": "form" } }, "required": [ "style" ] }, "then": { "properties": { "explode": { "default": true } } }, "else": { "properties": { "explode": { "default": false } } } } } }, "responses": { "$comment": "https://spec.openapis.org/oas/v3.1.0#responses-object", "type": "object", "properties": { "default": { "$ref": "#/$defs/response-or-reference" } }, "patternProperties": { "^[1-5](?:[0-9]{2}|XX)$": { "$ref": "#/$defs/response-or-reference" } }, "minProperties": 1, "$ref": "#/$defs/specification-extensions", "unevaluatedProperties": false, "if": { "$comment": "either default, or at least one response code property must exist", "patternProperties": { "^[1-5](?:[0-9]{2}|XX)$": false } }, "then" : { "required": [ "default" ] } }, "response": { "$comment": "https://spec.openapis.org/oas/v3.1.0#response-object", "type": "object", "properties": { "description": { "type": "string" }, "headers": { "type": "object", "additionalProperties": { "$ref": "#/$defs/header-or-reference" } }, "content": { "$ref": "#/$defs/content" }, "links": { "type": "object", "additionalProperties": { "$ref": "#/$defs/link-or-reference" } } }, "required": [ "description" ], "$ref": "#/$defs/specification-extensions", "unevaluatedProperties": false }, "response-or-reference": { "if": { "type": "object", "required": [ "$ref" ] }, "then": { "$ref": "#/$defs/reference" }, "else": { "$ref": "#/$defs/response" } }, "callbacks": { "$comment": "https://spec.openapis.org/oas/v3.1.0#callback-object", "type": "object", "$ref": "#/$defs/specification-extensions", "additionalProperties": { "$ref": "#/$defs/path-item-or-reference" } }, "callbacks-or-reference": { "if": { "type": "object", "required": [ "$ref" ] }, "then": { "$ref": "#/$defs/reference" }, "else": { "$ref": "#/$defs/callbacks" } }, "example": { "$comment": "https://spec.openapis.org/oas/v3.1.0#example-object", "type": "object", "properties": { "summary": { "type": "string" }, "description": { "type": "string" }, "value": true, "externalValue": { "type": "string", "format": "uri" } }, "not": { "required": [ "value", "externalValue" ] }, "$ref": "#/$defs/specification-extensions", "unevaluatedProperties": false }, "example-or-reference": { "if": { "type": "object", "required": [ "$ref" ] }, "then": { "$ref": "#/$defs/reference" }, "else": { "$ref": "#/$defs/example" } }, "link": { "$comment": "https://spec.openapis.org/oas/v3.1.0#link-object", "type": "object", "properties": { "operationRef": { "type": "string", "format": "uri-reference" }, "operationId": { "type": "string" }, "parameters": { "$ref": "#/$defs/map-of-strings" }, "requestBody": true, "description": { "type": "string" }, "body": { "$ref": "#/$defs/server" } }, "oneOf": [ { "required": [ "operationRef" ] }, { "required": [ "operationId" ] } ], "$ref": "#/$defs/specification-extensions", "unevaluatedProperties": false }, "link-or-reference": { "if": { "type": "object", "required": [ "$ref" ] }, "then": { "$ref": "#/$defs/reference" }, "else": { "$ref": "#/$defs/link" } }, "header": { "$comment": "https://spec.openapis.org/oas/v3.1.0#header-object", "type": "object", "properties": { "description": { "type": "string" }, "required": { "default": false, "type": "boolean" }, "deprecated": { "default": false, "type": "boolean" }, "schema": { "$dynamicRef": "#meta" }, "content": { "$ref": "#/$defs/content", "minProperties": 1, "maxProperties": 1 } }, "oneOf": [ { "required": [ "schema" ] }, { "required": [ "content" ] } ], "dependentSchemas": { "schema": { "properties": { "style": { "default": "simple", "const": "simple" }, "explode": { "default": false, "type": "boolean" } }, "$ref": "#/$defs/examples" } }, "$ref": "#/$defs/specification-extensions", "unevaluatedProperties": false }, "header-or-reference": { "if": { "type": "object", "required": [ "$ref" ] }, "then": { "$ref": "#/$defs/reference" }, "else": { "$ref": "#/$defs/header" } }, "tag": { "$comment": "https://spec.openapis.org/oas/v3.1.0#tag-object", "type": "object", "properties": { "name": { "type": "string" }, "description": { "type": "string" }, "externalDocs": { "$ref": "#/$defs/external-documentation" } }, "required": [ "name" ], "$ref": "#/$defs/specification-extensions", "unevaluatedProperties": false }, "reference": { "$comment": "https://spec.openapis.org/oas/v3.1.0#reference-object", "type": "object", "properties": { "$ref": { "type": "string", "format": "uri-reference" }, "summary": { "type": "string" }, "description": { "type": "string" } }, "unevaluatedProperties": false }, "schema": { "$comment": "https://spec.openapis.org/oas/v3.1.0#schema-object", "$dynamicAnchor": "meta", "type": [ "object", "boolean" ] }, "security-scheme": { "$comment": "https://spec.openapis.org/oas/v3.1.0#security-scheme-object", "type": "object", "properties": { "type": { "enum": [ "apiKey", "http", "mutualTLS", "oauth2", "openIdConnect" ] }, "description": { "type": "string" } }, "required": [ "type" ], "allOf": [ { "$ref": "#/$defs/specification-extensions" }, { "$ref": "#/$defs/security-scheme/$defs/type-apikey" }, { "$ref": "#/$defs/security-scheme/$defs/type-http" }, { "$ref": "#/$defs/security-scheme/$defs/type-http-bearer" }, { "$ref": "#/$defs/security-scheme/$defs/type-oauth2" }, { "$ref": "#/$defs/security-scheme/$defs/type-oidc" } ], "unevaluatedProperties": false, "$defs": { "type-apikey": { "if": { "properties": { "type": { "const": "apiKey" } }, "required": [ "type" ] }, "then": { "properties": { "name": { "type": "string" }, "in": { "enum": [ "query", "header", "cookie" ] } }, "required": [ "name", "in" ] } }, "type-http": { "if": { "properties": { "type": { "const": "http" } }, "required": [ "type" ] }, "then": { "properties": { "scheme": { "type": "string" } }, "required": [ "scheme" ] } }, "type-http-bearer": { "if": { "properties": { "type": { "const": "http" }, "scheme": { "type": "string", "pattern": "^[Bb][Ee][Aa][Rr][Ee][Rr]$" } }, "required": [ "type", "scheme" ] }, "then": { "properties": { "bearerFormat": { "type": "string" } } } }, "type-oauth2": { "if": { "properties": { "type": { "const": "oauth2" } }, "required": [ "type" ] }, "then": { "properties": { "flows": { "$ref": "#/$defs/oauth-flows" } }, "required": [ "flows" ] } }, "type-oidc": { "if": { "properties": { "type": { "const": "openIdConnect" } }, "required": [ "type" ] }, "then": { "properties": { "openIdConnectUrl": { "type": "string", "format": "uri" } }, "required": [ "openIdConnectUrl" ] } } } }, "security-scheme-or-reference": { "if": { "type": "object", "required": [ "$ref" ] }, "then": { "$ref": "#/$defs/reference" }, "else": { "$ref": "#/$defs/security-scheme" } }, "oauth-flows": { "type": "object", "properties": { "implicit": { "$ref": "#/$defs/oauth-flows/$defs/implicit" }, "password": { "$ref": "#/$defs/oauth-flows/$defs/password" }, "clientCredentials": { "$ref": "#/$defs/oauth-flows/$defs/client-credentials" }, "authorizationCode": { "$ref": "#/$defs/oauth-flows/$defs/authorization-code" } }, "$ref": "#/$defs/specification-extensions", "unevaluatedProperties": false, "$defs": { "implicit": { "type": "object", "properties": { "authorizationUrl": { "type": "string", "format": "uri" }, "refreshUrl": { "type": "string", "format": "uri" }, "scopes": { "$ref": "#/$defs/map-of-strings" } }, "required": [ "authorizationUrl", "scopes" ], "$ref": "#/$defs/specification-extensions", "unevaluatedProperties": false }, "password": { "type": "object", "properties": { "tokenUrl": { "type": "string", "format": "uri" }, "refreshUrl": { "type": "string", "format": "uri" }, "scopes": { "$ref": "#/$defs/map-of-strings" } }, "required": [ "tokenUrl", "scopes" ], "$ref": "#/$defs/specification-extensions", "unevaluatedProperties": false }, "client-credentials": { "type": "object", "properties": { "tokenUrl": { "type": "string", "format": "uri" }, "refreshUrl": { "type": "string", "format": "uri" }, "scopes": { "$ref": "#/$defs/map-of-strings" } }, "required": [ "tokenUrl", "scopes" ], "$ref": "#/$defs/specification-extensions", "unevaluatedProperties": false }, "authorization-code": { "type": "object", "properties": { "authorizationUrl": { "type": "string", "format": "uri" }, "tokenUrl": { "type": "string", "format": "uri" }, "refreshUrl": { "type": "string", "format": "uri" }, "scopes": { "$ref": "#/$defs/map-of-strings" } }, "required": [ "authorizationUrl", "tokenUrl", "scopes" ], "$ref": "#/$defs/specification-extensions", "unevaluatedProperties": false } } }, "security-requirement": { "$comment": "https://spec.openapis.org/oas/v3.1.0#security-requirement-object", "type": "object", "additionalProperties": { "type": "array", "items": { "type": "string" } } }, "specification-extensions": { "$comment": "https://spec.openapis.org/oas/v3.1.0#specification-extensions", "patternProperties": { "^x-": true } }, "examples": { "properties": { "example": true, "examples": { "type": "object", "additionalProperties": { "$ref": "#/$defs/example-or-reference" } } } }, "map-of-strings": { "type": "object", "additionalProperties": { "type": "string" } } } }drf-spectacular-0.27.0/drf_spectacular/views.py000066400000000000000000000273721453572150400215500ustar00rootroot00000000000000import json from collections import namedtuple from importlib import import_module from typing import Any, Dict, List, Optional, Type from django.conf import settings from django.templatetags.static import static from django.utils import translation from django.utils.translation import gettext_lazy as _ from django.views.generic import RedirectView from rest_framework.renderers import TemplateHTMLRenderer from rest_framework.response import Response from rest_framework.reverse import reverse from rest_framework.settings import api_settings from rest_framework.views import APIView from drf_spectacular.generators import SchemaGenerator from drf_spectacular.plumbing import get_relative_url, set_query_parameters from drf_spectacular.renderers import ( OpenApiJsonRenderer, OpenApiJsonRenderer2, OpenApiYamlRenderer, OpenApiYamlRenderer2, ) from drf_spectacular.settings import patched_settings, spectacular_settings from drf_spectacular.types import OpenApiTypes from drf_spectacular.utils import OpenApiParameter, extend_schema if spectacular_settings.SERVE_INCLUDE_SCHEMA: SCHEMA_KWARGS: Dict[str, Any] = {'responses': {200: OpenApiTypes.OBJECT}} if settings.USE_I18N: SCHEMA_KWARGS['parameters'] = [ OpenApiParameter( 'lang', str, OpenApiParameter.QUERY, enum=list(dict(settings.LANGUAGES).keys()) ) ] else: SCHEMA_KWARGS = {'exclude': True} if spectacular_settings.SERVE_AUTHENTICATION is not None: AUTHENTICATION_CLASSES = spectacular_settings.SERVE_AUTHENTICATION else: AUTHENTICATION_CLASSES = api_settings.DEFAULT_AUTHENTICATION_CLASSES class SpectacularAPIView(APIView): __doc__ = _(""" OpenApi3 schema for this API. Format can be selected via content negotiation. - YAML: application/vnd.oai.openapi - JSON: application/vnd.oai.openapi+json """) # type: ignore renderer_classes = [ OpenApiYamlRenderer, OpenApiYamlRenderer2, OpenApiJsonRenderer, OpenApiJsonRenderer2 ] permission_classes = spectacular_settings.SERVE_PERMISSIONS authentication_classes = AUTHENTICATION_CLASSES generator_class: Type[SchemaGenerator] = spectacular_settings.DEFAULT_GENERATOR_CLASS serve_public: bool = spectacular_settings.SERVE_PUBLIC urlconf: Optional[str] = spectacular_settings.SERVE_URLCONF api_version: Optional[str] = None custom_settings: Optional[Dict[str, Any]] = None patterns: Optional[List[Any]] = None @extend_schema(**SCHEMA_KWARGS) def get(self, request, *args, **kwargs): # special handling of custom urlconf parameter if isinstance(self.urlconf, list) or isinstance(self.urlconf, tuple): ModuleWrapper = namedtuple('ModuleWrapper', ['urlpatterns']) if all(isinstance(i, str) for i in self.urlconf): # list of import string for urlconf patterns = [] for item in self.urlconf: url = import_module(item) patterns += url.urlpatterns self.urlconf = ModuleWrapper(tuple(patterns)) else: # explicitly resolved urlconf self.urlconf = ModuleWrapper(tuple(self.urlconf)) with patched_settings(self.custom_settings): if settings.USE_I18N and request.GET.get('lang'): with translation.override(request.GET.get('lang')): return self._get_schema_response(request) else: return self._get_schema_response(request) def _get_schema_response(self, request): # version specified as parameter to the view always takes precedence. after # that we try to source version through the schema view's own versioning_class. version = self.api_version or request.version or self._get_version_parameter(request) generator = self.generator_class(urlconf=self.urlconf, api_version=version, patterns=self.patterns) return Response( data=generator.get_schema(request=request, public=self.serve_public), headers={"Content-Disposition": f'inline; filename="{self._get_filename(request, version)}"'} ) def _get_filename(self, request, version): return "{title}{version}.{suffix}".format( title=spectacular_settings.TITLE or 'schema', version=f' ({version})' if version else '', suffix=self.perform_content_negotiation(request, force=True)[0].format ) def _get_version_parameter(self, request): version = request.GET.get('version') if not api_settings.ALLOWED_VERSIONS or version in api_settings.ALLOWED_VERSIONS: return version return None class SpectacularYAMLAPIView(SpectacularAPIView): renderer_classes = [OpenApiYamlRenderer, OpenApiYamlRenderer2] class SpectacularJSONAPIView(SpectacularAPIView): renderer_classes = [OpenApiJsonRenderer, OpenApiJsonRenderer2] def _get_sidecar_url(filepath): return static(f'drf_spectacular_sidecar/{filepath}') class SpectacularSwaggerView(APIView): renderer_classes = [TemplateHTMLRenderer] permission_classes = spectacular_settings.SERVE_PERMISSIONS authentication_classes = AUTHENTICATION_CLASSES url_name: str = 'schema' url: Optional[str] = None template_name: str = 'drf_spectacular/swagger_ui.html' template_name_js: str = 'drf_spectacular/swagger_ui.js' title: str = spectacular_settings.TITLE @extend_schema(exclude=True) def get(self, request, *args, **kwargs): return Response( data={ 'title': self.title, 'swagger_ui_css': self._swagger_ui_resource('swagger-ui.css'), 'swagger_ui_bundle': self._swagger_ui_resource('swagger-ui-bundle.js'), 'swagger_ui_standalone': self._swagger_ui_resource('swagger-ui-standalone-preset.js'), 'favicon_href': self._swagger_ui_favicon(), 'schema_url': self._get_schema_url(request), 'settings': self._dump(spectacular_settings.SWAGGER_UI_SETTINGS), 'oauth2_config': self._dump(spectacular_settings.SWAGGER_UI_OAUTH2_CONFIG), 'template_name_js': self.template_name_js, 'csrf_header_name': self._get_csrf_header_name(), 'schema_auth_names': self._dump(self._get_schema_auth_names()), }, template_name=self.template_name, ) def _dump(self, data): return data if isinstance(data, str) else json.dumps(data, indent=2) def _get_schema_url(self, request): schema_url = self.url or get_relative_url(reverse(self.url_name, request=request)) return set_query_parameters( url=schema_url, lang=request.GET.get('lang'), version=request.GET.get('version') ) def _get_csrf_header_name(self): csrf_header_name = settings.CSRF_HEADER_NAME if csrf_header_name.startswith('HTTP_'): csrf_header_name = csrf_header_name[5:] return csrf_header_name.replace('_', '-') def _get_schema_auth_names(self): from drf_spectacular.extensions import OpenApiAuthenticationExtension if spectacular_settings.SERVE_PUBLIC: return [] auth_extensions = [ OpenApiAuthenticationExtension.get_match(klass) for klass in self.authentication_classes ] return [auth.name for auth in auth_extensions if auth] @staticmethod def _swagger_ui_resource(filename): if spectacular_settings.SWAGGER_UI_DIST == 'SIDECAR': return _get_sidecar_url(f'swagger-ui-dist/{filename}') return f'{spectacular_settings.SWAGGER_UI_DIST}/{filename}' @staticmethod def _swagger_ui_favicon(): if spectacular_settings.SWAGGER_UI_FAVICON_HREF == 'SIDECAR': return _get_sidecar_url('swagger-ui-dist/favicon-32x32.png') return spectacular_settings.SWAGGER_UI_FAVICON_HREF class SpectacularSwaggerSplitView(SpectacularSwaggerView): """ Alternate Swagger UI implementation that separates the html request from the javascript request to cater to web servers with stricter CSP policies. """ url_self: Optional[str] = None @extend_schema(exclude=True) def get(self, request, *args, **kwargs): if request.GET.get('script') is not None: return Response( data={ 'schema_url': self._get_schema_url(request), 'settings': self._dump(spectacular_settings.SWAGGER_UI_SETTINGS), 'oauth2_config': self._dump(spectacular_settings.SWAGGER_UI_OAUTH2_CONFIG), 'csrf_header_name': self._get_csrf_header_name(), 'schema_auth_names': self._dump(self._get_schema_auth_names()), }, template_name=self.template_name_js, content_type='application/javascript', ) else: script_url = self.url_self or request.get_full_path() return Response( data={ 'title': self.title, 'swagger_ui_css': self._swagger_ui_resource('swagger-ui.css'), 'swagger_ui_bundle': self._swagger_ui_resource('swagger-ui-bundle.js'), 'swagger_ui_standalone': self._swagger_ui_resource('swagger-ui-standalone-preset.js'), 'favicon_href': self._swagger_ui_favicon(), 'script_url': set_query_parameters( url=script_url, lang=request.GET.get('lang'), script='' # signal to deliver init script ) }, template_name=self.template_name, ) class SpectacularRedocView(APIView): renderer_classes = [TemplateHTMLRenderer] permission_classes = spectacular_settings.SERVE_PERMISSIONS authentication_classes = AUTHENTICATION_CLASSES url_name: str = 'schema' url: Optional[str] = None template_name: str = 'drf_spectacular/redoc.html' title: Optional[str] = spectacular_settings.TITLE @extend_schema(exclude=True) def get(self, request, *args, **kwargs): return Response( data={ 'title': self.title, 'redoc_standalone': self._redoc_standalone(), 'schema_url': self._get_schema_url(request), 'settings': self._dump(spectacular_settings.REDOC_UI_SETTINGS), }, template_name=self.template_name ) def _dump(self, data): if not data: return None elif isinstance(data, str): return data else: return json.dumps(data, indent=2) @staticmethod def _redoc_standalone(): if spectacular_settings.REDOC_DIST == 'SIDECAR': return _get_sidecar_url('redoc/bundles/redoc.standalone.js') return f'{spectacular_settings.REDOC_DIST}/bundles/redoc.standalone.js' def _get_schema_url(self, request): schema_url = self.url or get_relative_url(reverse(self.url_name, request=request)) return set_query_parameters( url=schema_url, lang=request.GET.get('lang'), version=request.GET.get('version') ) class SpectacularSwaggerOauthRedirectView(RedirectView): """ A view that serves the SwaggerUI oauth2-redirect.html file so that SwaggerUI can authenticate itself using Oauth2 This view should be served as ``./oauth2-redirect.html`` relative to the SwaggerUI itself. If that is not possible, this views absolute url can also be set via the ``SPECTACULAR_SETTINGS.SWAGGER_UI_SETTINGS.oauth2RedirectUrl`` django settings. """ def get_redirect_url(self, *args, **kwargs): return _get_sidecar_url("swagger-ui-dist/oauth2-redirect.html") + "?" + self.request.GET.urlencode() drf-spectacular-0.27.0/helper/000077500000000000000000000000001453572150400161445ustar00rootroot00000000000000drf-spectacular-0.27.0/helper/changelog.sh000077500000000000000000000007401453572150400204330ustar00rootroot00000000000000#!/usr/bin/env bash last_ver=$(git describe --tags --abbrev=0) next_ver=$(grep "__version__" drf_spectacular/__init__.py | sed "s/__version__ = '\\(.*\\)'/\\1/g") echo echo "$next_ver ($(date "+%Y-%m-%d"))" echo "-------------------" echo git log ${last_ver}..origin/master --pretty=format:"- %s [%an]" --no-merges \ | sed 's/\[T. Franzel\]//g' \ | sed 's|\#\([0-9]\+\)|`#\1 `_|g' \ | sed 's/[ \t]*$//' echo drf-spectacular-0.27.0/helper/generate-client.sh000077500000000000000000000012731453572150400215540ustar00rootroot00000000000000#!/usr/bin/env bash if [ $# -lt 2 ] ; then echo "usage: $0 SCHEMA TARGET [NAME]" exit 1 fi SCHEMA=$1 SCHEMA_PATH=${PWD}/${SCHEMA} TARGET=$2 OUTPUT_NAME=${3:-$TARGET} OUTPUT_PATH=${PWD}/generated_clients/${OUTPUT_NAME} mkdir -p $OUTPUT_PATH echo "removing old client at ${OUTPUT_PATH}" sudo rm -rf ${OUTPUT_PATH} docker pull openapitools/openapi-generator-cli echo "generator version: $(docker run --rm openapitools/openapi-generator-cli version)" docker run --rm -v ${SCHEMA_PATH}:/schema.yml -v ${OUTPUT_PATH}:/build openapitools/openapi-generator-cli generate \ -i /schema.yml \ -g ${TARGET} \ -o /build sudo chown -R $USER:$USER $OUTPUT_PATH cp ${SCHEMA_PATH} ${OUTPUT_PATH}/schema.yml drf-spectacular-0.27.0/helper/github-ci-vars.py000066400000000000000000000013471453572150400213470ustar00rootroot00000000000000import datetime import json import os import re import subprocess envs = subprocess.check_output(['tox', '-l']).decode().rstrip().split('\n') matrix = [] for env in envs: version = re.search(r'^py(?P\d)(?P\d+)-', env) # github "commit" checks will fail even though workflow passes overall. # temp remove the optional targets to make github CI work. if 'master' in env: continue matrix.append({ 'toxenv': env, 'python-version': f'{version.group("major")}.{version.group("minor")}', 'experimental': bool('master' in env) }) with open(os.environ['GITHUB_OUTPUT'], "a") as fh: fh.write(f"date={datetime.date.today()}\n") fh.write(f"matrix={json.dumps(matrix)}\n") drf-spectacular-0.27.0/helper/swagger-ui.sh000077500000000000000000000004641453572150400205610ustar00rootroot00000000000000#!/usr/bin/env bash if [ "$#" -ne "1" ] ; then echo usage: $0 SCHEMA exit 1 fi SCHEMA=$1 SCHEMA_PATH=${PWD}/${SCHEMA} docker pull swaggerapi/swagger-ui echo echo UI running at http://localhost:8088 docker run -p 8088:8080 -e SWAGGER_JSON=/schema.yml -v ${SCHEMA_PATH}:/schema.yml swaggerapi/swagger-ui drf-spectacular-0.27.0/requirements.txt000066400000000000000000000002121453572150400201440ustar00rootroot00000000000000-r requirements/base.txt -r requirements/testing.txt -r requirements/optionals.txt -r requirements/packaging.txt -r requirements/docs.txt drf-spectacular-0.27.0/requirements/000077500000000000000000000000001453572150400174105ustar00rootroot00000000000000drf-spectacular-0.27.0/requirements/base.txt000066400000000000000000000002251453572150400210620ustar00rootroot00000000000000Django>=2.2 djangorestframework>=3.10.3 uritemplate>=2.0.0 PyYAML>=5.1 jsonschema>=2.6.0 inflection>=0.3.1 typing-extensions; python_version < "3.10"drf-spectacular-0.27.0/requirements/docs.txt000066400000000000000000000000701453572150400210760ustar00rootroot00000000000000Sphinx>=4.1.0 sphinx_rtd_theme>=0.5.1 typing-extensions drf-spectacular-0.27.0/requirements/linting.txt000066400000000000000000000003231453572150400216130ustar00rootroot00000000000000pytest # required for mypy to succeed flake8 isort==5.12.0 # 5.13 somehow breaks django-stubs plugin mypy==1.7.1 django-stubs==4.2.3 djangorestframework-stubs==3.14.2 Django==4.2.7 djangorestframework==3.14.0drf-spectacular-0.27.0/requirements/optionals.txt000066400000000000000000000010311453572150400221540ustar00rootroot00000000000000django-allauth<0.55.0 # breaking change breaking dj-rest-auth drf-jwt>=0.13.0 dj-rest-auth>=1.0.0 djangorestframework-simplejwt>=4.4.0 setuptools django-polymorphic>=2.1 django-rest-polymorphic>=0.1.8 django-oauth-toolkit>=1.2.0 djangorestframework-camel-case>=1.1.2 django-filter>=2.3.0 psycopg2-binary>=2.7.3.2 drf-nested-routers>=0.93.3 djangorestframework-recursive>=0.1.2 drf-spectacular-sidecar djangorestframework-dataclasses>=1.0.0; python_version >= '3.7' djangorestframework-gis>=1.0.0 pydantic>=2,<3; python_version >= '3.7' drf-spectacular-0.27.0/requirements/packaging.txt000066400000000000000000000000461453572150400220750ustar00rootroot00000000000000twine>=3.1.1 wheel>=0.34.2 setuptools drf-spectacular-0.27.0/requirements/testing.txt000066400000000000000000000000641453572150400216260ustar00rootroot00000000000000pytest>=5.3.5 pytest-django>=3.8.0 pytest-cov>=2.8.1drf-spectacular-0.27.0/runtests.py000077500000000000000000000055611453572150400171400ustar00rootroot00000000000000#! /usr/bin/env python from __future__ import print_function import os import subprocess import sys import pytest PYTEST_ARGS = { 'default': ['tests', '--allow-skip-extra-system-req'], 'fast': ['tests', '-q'], } FLAKE8_ARGS = ['drf_spectacular', 'tests'] MYPY_ARGS = ['--config-file=tox.ini', 'drf_spectacular', 'tests'] ISORT_ARGS = ['--check', '--diff', '.'] sys.path.append(os.path.dirname(__file__)) def exit_on_failure(ret, message=None): if ret: sys.exit(ret) def flake8_main(args): print('Running flake8 code linting') ret = subprocess.call(['flake8'] + args) print('flake8 failed' if ret else 'flake8 passed') return ret def mypy_main(args): print('Running mypy code linting') ret = subprocess.call(['mypy'] + args) print('mypy failed' if ret else 'mypy passed') return ret def isort_main(args): print('Running isort code linting') ret = subprocess.call(['isort'] + args) print('isort failed, run: isort --interactive .' if ret else 'isort passed') return ret def split_class_and_function(string): class_string, function_string = string.split('.', 1) return "%s and %s" % (class_string, function_string) def is_function(string): # `True` if it looks like a test function is included in the string. return string.startswith('test_') or '.test_' in string def is_class(string): # `True` if first character is uppercase - assume it's a class name. return string[0] == string[0].upper() if __name__ == "__main__": try: sys.argv.remove('--nolint') except ValueError: run_lint = True else: run_lint = False try: sys.argv.remove('--lintonly') except ValueError: run_tests = True else: run_tests = False try: sys.argv.remove('--fast') except ValueError: style = 'default' else: style = 'fast' run_lint = False if len(sys.argv) > 1: pytest_args = sys.argv[1:] first_arg = pytest_args[0] if first_arg.startswith('-'): # `runtests.py [flags]` pytest_args = ['tests'] + pytest_args elif is_class(first_arg) and is_function(first_arg): # `runtests.py TestCase.test_function [flags]` expression = split_class_and_function(first_arg) pytest_args = ['tests', '-k', expression] + pytest_args[1:] elif is_class(first_arg) or is_function(first_arg): # `runtests.py TestCase [flags]` # `runtests.py test_function [flags]` pytest_args = ['tests', '-k', pytest_args[0]] + pytest_args[1:] else: pytest_args = PYTEST_ARGS[style] if run_tests: exit_on_failure(pytest.main(pytest_args)) if run_lint: exit_on_failure(flake8_main(FLAKE8_ARGS)) exit_on_failure(isort_main(ISORT_ARGS)) exit_on_failure(mypy_main(MYPY_ARGS)) drf-spectacular-0.27.0/setup.py000066400000000000000000000071011453572150400163760ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- import os import re import shutil import sys from setuptools import find_namespace_packages, setup name = 'drf-spectacular' package = 'drf_spectacular' description = 'Sane and flexible OpenAPI 3 schema generation for Django REST framework' url = 'https://github.com/tfranzel/drf-spectacular' author = 'T. Franzel' author_email = 'tfranzel@gmail.com' license = 'BSD' with open('README.rst') as readme: long_description = readme.read() with open('requirements/base.txt') as fh: requirements = [r for r in fh.read().split('\n') if not r.startswith('#')] def get_version(package): """ Return package version as listed in `__version__` in `init.py`. """ init_py = open(os.path.join(package, '__init__.py')).read() return re.search("^__version__ = ['\"]([^'\"]+)['\"]", init_py, re.MULTILINE).group(1) version = get_version(package) if sys.argv[-1] == 'publish': if os.system("pip freeze | grep twine"): print("twine not installed.\nUse `pip install twine`.\nExiting.") sys.exit(1) os.system("python setup.py sdist bdist_wheel") if os.system("twine check dist/*"): print("twine check failed. Packages might be outdated.") print("Try using `pip install -U twine wheel`.\nExiting.") sys.exit(1) if os.system("twine upload dist/*"): print("failed to upload package") sys.exit(1) if os.environ.get('CI'): os.system("git config user.name github-actions") os.system("git config user.email github-actions@github.com") os.system(f"git tag -a {version} -m 'version {version}'") if os.system("git push --tags"): print("failed pushing release tag") sys.exit(1) shutil.rmtree('dist') shutil.rmtree('build') shutil.rmtree('drf_spectacular.egg-info') sys.exit() setup( name=name, version=version, url=url, license=license, description=description, long_description=long_description, long_description_content_type='text/x-rst', author=author, author_email=author_email, packages=[p for p in find_namespace_packages(exclude=('tests*',)) if p.startswith(package)], include_package_data=True, python_requires=">=3.6", install_requires=requirements, extras_require={ "offline": ["drf-spectacular-sidecar"], "sidecar": ["drf-spectacular-sidecar"], }, classifiers=[ 'Development Status :: 5 - Production/Stable', 'Environment :: Web Environment', 'Framework :: Django', 'Framework :: Django :: 2.2', 'Framework :: Django :: 3.2', 'Framework :: Django :: 4.0', 'Framework :: Django :: 4.1', 'Framework :: Django :: 4.2', 'Intended Audience :: Developers', 'License :: OSI Approved :: BSD License', 'Operating System :: OS Independent', 'Natural Language :: English', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.11', 'Programming Language :: Python :: 3.12', 'Topic :: Internet :: WWW/HTTP', 'Topic :: Documentation', 'Topic :: Software Development :: Code Generators', ], project_urls={ 'Source': 'https://github.com/tfranzel/drf-spectacular', 'Documentation': 'https://drf-spectacular.readthedocs.io', }, ) drf-spectacular-0.27.0/tests/000077500000000000000000000000001453572150400160275ustar00rootroot00000000000000drf-spectacular-0.27.0/tests/__init__.py000066400000000000000000000101731453572150400201420ustar00rootroot00000000000000import difflib import json import os from drf_spectacular.validation import validate_schema def build_absolute_file_path(relative_path): return os.path.join( os.path.dirname(os.path.dirname(os.path.realpath(__file__))), relative_path ) def assert_schema(schema, reference_filename, transforms=None, reverse_transforms=None): from drf_spectacular.renderers import OpenApiJsonRenderer, OpenApiYamlRenderer schema_yml = OpenApiYamlRenderer().render(schema, renderer_context={}) # render also a json and provoke serialization issues OpenApiJsonRenderer().render(schema, renderer_context={}) reference_filename = build_absolute_file_path(reference_filename) with open(reference_filename.replace('.yml', '_out.yml'), 'wb') as fh: fh.write(schema_yml) if not os.path.exists(reference_filename): raise RuntimeError( f'{reference_filename} was not found for comparison. Carefully inspect ' f'the generated {reference_filename.replace(".yml", "_out.yml")} and ' f'copy it to {reference_filename} to serve as new ground truth.' ) generated = schema_yml.decode() with open(reference_filename) as fh: expected = fh.read() # apply optional transformations to generated result. this mainly serves to unify # discrepancies between Django, DRF and library versions. for t in transforms or []: generated = t(generated) for t in reverse_transforms or []: expected = t(expected) assert_equal(generated, expected) # this is more a less a sanity check as checked-in schemas should be valid anyhow validate_schema(schema) def assert_equal(actual, expected): if not isinstance(actual, str): actual = json.dumps(actual, indent=4) if not isinstance(expected, str): expected = json.dumps(expected, indent=4) diff = difflib.unified_diff( expected.splitlines(True), actual.splitlines(True), ) diff = ''.join(diff) assert actual == expected and not diff, diff def generate_schema(route, viewset=None, view=None, view_function=None, patterns=None): from django.urls import path from rest_framework import routers from rest_framework.viewsets import ViewSetMixin from drf_spectacular.generators import SchemaGenerator if viewset: assert issubclass(viewset, ViewSetMixin) router = routers.SimpleRouter() router.register(route, viewset, basename=route) patterns = router.urls elif view: patterns = [path(route, view.as_view())] elif view_function: patterns = [path(route, view_function)] else: assert route is None and isinstance(patterns, list) generator = SchemaGenerator(patterns=patterns) schema = generator.get_schema(request=None, public=True) validate_schema(schema) # make sure generated schemas are always valid return schema def get_response_schema(operation, status=None, content_type='application/json'): if ( not status and operation['operationId'].endswith('_create') and '201' in operation['responses'] and '200' not in operation['responses'] ): status = '201' elif not status: status = '200' return operation['responses'][status]['content'][content_type]['schema'] def get_request_schema(operation, content_type='application/json'): return operation['requestBody']['content'][content_type]['schema'] def is_gis_installed(): # only load GIS if library is installed. This is required for the GIS test to work from django.core.exceptions import ImproperlyConfigured try: from django.contrib.gis.gdal import gdal_version # noqa: F401 except ImproperlyConfigured: return False else: return True def strip_int64_details(schema): """ remove new min/max/format for django 5 with sqlite db for comparison’s sake """ if schema.get('format') == 'int64' and 'minimum' in schema and 'maximum' in schema: return { k: v for k, v in schema.items() if k not in ('format', 'minimum', 'maximum') } else: return schema drf-spectacular-0.27.0/tests/conftest.py000066400000000000000000000151041453572150400202270ustar00rootroot00000000000000import os import re from importlib import import_module import django import pytest from django import __version__ as DJANGO_VERSION from django.core import management from tests import is_gis_installed def pytest_configure(config): from django.conf import settings config.addinivalue_line( "markers", "contrib(name): mark required contrib package" ) contrib_apps = [ 'dj_rest_auth', 'dj_rest_auth.registration', 'allauth', 'allauth.account', 'oauth2_provider', 'django_filters', # this is not strictly required and when added django-polymorphic # currently breaks the whole Django/DRF upstream testing. # 'polymorphic', # 'rest_framework_jwt', ] # only load GIS if library is installed. This is required for the GIS test to work if is_gis_installed(): contrib_apps.append('rest_framework_gis') base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) settings.configure( DEBUG_PROPAGATE_EXCEPTIONS=True, DATABASES={'default': { 'ENGINE': 'django.db.backends.sqlite3', 'NAME': ':memory:' }}, SITE_ID=1, SECRET_KEY='not very secret in tests', USE_I18N=True, USE_L10N=True, LANGUAGES=[ ('de-de', 'German'), ('en-us', 'English'), ], LOCALE_PATHS=[ base_dir + '/locale/' ], STATIC_URL='/static/', ROOT_URLCONF='tests.urls', TEMPLATES=[ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', 'DIRS': [], 'APP_DIRS': True, 'OPTIONS': { 'context_processors': [ 'django.template.context_processors.debug', ], }, }, ], MIDDLEWARE=( 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.middleware.locale.LocaleMiddleware', ), INSTALLED_APPS=( 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.sites', 'django.contrib.messages', 'django.contrib.staticfiles', 'rest_framework', 'rest_framework.authtoken', *[app for app in contrib_apps if module_available(app)], 'drf_spectacular', 'tests', ), PASSWORD_HASHERS=( 'django.contrib.auth.hashers.SHA1PasswordHasher', 'django.contrib.auth.hashers.PBKDF2PasswordHasher', 'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher', 'django.contrib.auth.hashers.BCryptPasswordHasher', 'django.contrib.auth.hashers.MD5PasswordHasher', 'django.contrib.auth.hashers.CryptPasswordHasher', ), REST_FRAMEWORK={ 'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema', 'PAGE_SIZE': 10, }, DEFAULT_AUTO_FIELD='django.db.models.AutoField', SILENCED_SYSTEM_CHECKS=[ 'rest_framework.W001', 'fields.E210', 'security.W001', 'security.W002', 'security.W003', 'security.W009', 'security.W012' ], ) django.setup() # For whatever reason this works locally without an issue. # on TravisCI content_type table is missing in the sqlite db as # if no migration ran, but then why does it work locally?! management.call_command('migrate') def pytest_addoption(parser): parser.addoption( "--skip-missing-contrib", action="store_true", default=False, help="skip tests depending on missing contrib packages" ) parser.addoption( "--allow-contrib-fail", action="store_true", default=False, help="run contrib tests but allow them to fail" ) parser.addoption( "--allow-skip-extra-system-req", action="store_true", default=False, help="" ) def pytest_collection_modifyitems(config, items): skip_missing_contrib = pytest.mark.skip(reason="skip tests for missing contrib package") allow_contrib_fail = pytest.mark.xfail(reason="contrib test were allowed to fail") allow_sys_requirement_fail = pytest.mark.xfail(reason="may fail due to missing system library") for item in items: for marker in item.own_markers: if marker.name == 'contrib' and config.getoption("--skip-missing-contrib"): if not all([module_available(module_str) for module_str in marker.args]): # pragma: no cover item.add_marker(skip_missing_contrib) if marker.name == 'contrib' and config.getoption("--allow-contrib-fail"): item.add_marker(allow_contrib_fail) if marker.name == 'system_requirement_fulfilled' and config.getoption("--allow-skip-extra-system-req"): if not marker.args[0]: item.add_marker(allow_sys_requirement_fail) @pytest.fixture() def no_warnings(capsys): """ make sure test emits no warnings """ yield capsys captured = capsys.readouterr() assert not captured.out assert not captured.err @pytest.fixture() def warnings(capsys): """ make sure test emits no warnings """ yield capsys captured = capsys.readouterr() assert captured.err @pytest.fixture() def clear_generator_settings(): from drf_spectacular.drainage import GENERATOR_STATS yield GENERATOR_STATS._trace_lineno = False GENERATOR_STATS._red = GENERATOR_STATS._blue = '' GENERATOR_STATS._yellow = GENERATOR_STATS._clear = '' @pytest.fixture() def clear_caches(): from drf_spectacular.plumbing import get_openapi_type_mapping, load_enum_name_overrides load_enum_name_overrides.cache_clear() get_openapi_type_mapping.cache_clear() yield load_enum_name_overrides.cache_clear() get_openapi_type_mapping.cache_clear() def module_available(module_str): try: import_module(module_str) except ImportError: return False else: return True @pytest.fixture() def django_transforms(): def integer_field_sqlite(s): return re.sub( r' *maximum: 9223372036854775807\n *minimum: (-9223372036854775808|0)\n *format: int64\n', '', s, flags=re.M ) if DJANGO_VERSION >= '5': return [integer_field_sqlite] else: return [] drf-spectacular-0.27.0/tests/contrib/000077500000000000000000000000001453572150400174675ustar00rootroot00000000000000drf-spectacular-0.27.0/tests/contrib/__init__.py000066400000000000000000000000001453572150400215660ustar00rootroot00000000000000drf-spectacular-0.27.0/tests/contrib/test_django_filters.py000066400000000000000000000362151453572150400241010ustar00rootroot00000000000000import uuid import pytest from django import __version__ as DJANGO_VERSION from django.db import models from django.db.models import F from django.urls import include, path from rest_framework import generics, routers, serializers, viewsets from rest_framework.test import APIClient from drf_spectacular.types import OpenApiTypes from drf_spectacular.utils import OpenApiExample, extend_schema, extend_schema_field from tests import assert_schema, generate_schema from tests.models import SimpleModel, SimpleSerializer try: from django_filters.rest_framework import ( AllValuesFilter, BaseInFilter, BooleanFilter, CharFilter, ChoiceFilter, DjangoFilterBackend, FilterSet, ModelChoiceFilter, ModelMultipleChoiceFilter, MultipleChoiceFilter, NumberFilter, NumericRangeFilter, OrderingFilter, RangeFilter, UUIDFilter, ) except ImportError: class DjangoFilterBackend: # type: ignore pass class FilterSet: # type: ignore pass class NumberFilter: # type: ignore def init(self, **kwargs): pass CharFilter = NumberFilter ChoiceFilter = NumberFilter OrderingFilter = NumberFilter BaseInFilter = NumberFilter BooleanFilter = NumberFilter UUIDFilter = NumberFilter NumericRangeFilter = NumberFilter RangeFilter = NumberFilter MultipleChoiceFilter = NumberFilter ModelChoiceFilter = NumberFilter ModelMultipleChoiceFilter = NumberFilter AllValuesFilter = NumberFilter class OtherSubProduct(models.Model): uuid = models.UUIDField(default=uuid.uuid4, editable=False, unique=True) class Product(models.Model): category = models.CharField( max_length=10, choices=(('A', 'aaa'), ('B', 'b')), help_text='some category description' ) in_stock = models.BooleanField() price = models.FloatField() other_sub_product = models.ForeignKey(OtherSubProduct, on_delete=models.CASCADE) class SubProduct(models.Model): sub_price = models.FloatField() product = models.ForeignKey(Product, on_delete=models.CASCADE) class ProductSerializer(serializers.ModelSerializer): class Meta: model = Product fields = '__all__' def external_filter_method(queryset, name, value): return queryset # pragma: no cover class CustomBooleanFilter(BooleanFilter): pass class CustomBaseInFilter(BaseInFilter): pass class ProductFilter(FilterSet): # explicit filter declaration max_price = NumberFilter(field_name="price", lookup_expr='lte', label='highest price') max_sub_price = NumberFilter(field_name="subproduct__sub_price", lookup_expr='lte') sub = NumberFilter(field_name="subproduct", lookup_expr='exact') int_id = NumberFilter(method='filter_method_typed') number_id = NumberFilter(method='filter_method_untyped', help_text='some injected help text') number_id_ext = NumberFilter(method=external_filter_method) email = CharFilter(method='filter_method_decorated') # implicit filter declaration subproduct__sub_price = NumberFilter() # reverse relation other_sub_product__uuid = UUIDFilter() # forward relation # special cases ordering = OrderingFilter( fields=('price', 'in_stock'), field_labels={'price': 'Price', 'in_stock': 'in stock'}, ) in_categories = BaseInFilter(field_name='category') is_free = BooleanFilter(field_name='price', lookup_expr='isnull') price_range = RangeFilter(field_name='price') model_multi_cat = ModelMultipleChoiceFilter(field_name='category', queryset=Product.objects.all()) model_single_cat = ModelChoiceFilter(field_name='category', queryset=Product.objects.all()) all_values = AllValuesFilter(field_name='price') custom_filter = CustomBooleanFilter(field_name='price', lookup_expr='isnull') custom_underspec_filter = CustomBaseInFilter(field_name='category') model_multi_cat_relation = ModelMultipleChoiceFilter( field_name='other_sub_product', queryset=OtherSubProduct.objects.all() ) price_range_vat = RangeFilter(field_name='price_vat') price_range_vat_decorated = extend_schema_field(OpenApiTypes.INT)( RangeFilter(field_name='price_vat') ) def get_choices(*args, **kwargs): return (('A', 'aaa'),) cat_callable = ChoiceFilter(field_name="category", choices=get_choices) choice_field_enum_override = extend_schema_field(OpenApiTypes.STR)(ChoiceFilter(choices=(('A', 'aaa'), ))) # will guess type from choices as a last resort untyped_choice_field_method_with_explicit_choices = ChoiceFilter( method="filter_method_untyped", choices=[(1, 'one')], ) untyped_multiple_choice_field_method_with_explicit_choices = MultipleChoiceFilter( method="filter_method_multi_untyped", choices=[(1, 'one')], ) class Meta: model = Product fields = [ 'category', 'in_stock', 'max_price', 'max_sub_price', 'sub', 'subproduct__sub_price', 'other_sub_product__uuid', ] def filter_method_typed(self, queryset, name, value: int): return queryset.filter(id=int(value)) def filter_method_untyped(self, queryset, name, value): return queryset.filter(id=int(value)) # pragma: no cover def filter_method_multi_untyped(self, queryset, name, value): return queryset.filter(id__in=int(value)) # pragma: no cover # email makes no sense here. it's just to test decoration @extend_schema_field(OpenApiTypes.EMAIL) def filter_method_decorated(self, queryset, name, value): return queryset.filter(id=int(value)) # pragma: no cover decorated_serializer_field = CharFilter(method='filter_method_decorated2') @extend_schema_field(serializers.UUIDField) def filter_method_decorated2(self, queryset, name, value): return queryset.filter(id=int(value)) # pragma: no cover @extend_schema( examples=[ OpenApiExample('Magic example 1', value='1337', parameter_only=('max_price', 'query')), OpenApiExample('Magic example 2', value='1234', parameter_only=('max_price', 'query')), ] ) class ProductViewset(viewsets.ReadOnlyModelViewSet): queryset = Product.objects.all() serializer_class = ProductSerializer filter_backends = (DjangoFilterBackend,) filterset_class = ProductFilter def get_queryset(self): return Product.objects.all().annotate( price_vat=F('price') * 1.19 ) @pytest.mark.contrib('django_filter') def test_django_filters(no_warnings): assert_schema( generate_schema('products', ProductViewset), 'tests/contrib/test_django_filters.yml' ) router = routers.SimpleRouter() router.register('products', ProductViewset) urlpatterns = [ path('api/', include(router.urls)), ] @pytest.mark.urls(__name__) @pytest.mark.django_db @pytest.mark.contrib('django_filter') def test_django_filters_requests(no_warnings): other_sub_product = OtherSubProduct.objects.create(uuid=uuid.uuid4()) product = Product.objects.create( category='A', price=4, in_stock=True, other_sub_product=other_sub_product ) SubProduct.objects.create(sub_price=5, product=product) response = APIClient().get('/api/products/?max_price=1') assert response.status_code == 200 assert len(response.json()) == 0 response = APIClient().get('/api/products/?max_price=5') assert response.status_code == 200 assert len(response.json()) == 1 response = APIClient().get('/api/products/?max_sub_price=1') assert response.status_code == 200 assert len(response.json()) == 0 response = APIClient().get('/api/products/?max_sub_price=6') assert response.status_code == 200 assert len(response.json()) == 1 response = APIClient().get('/api/products/?sub=1') assert response.status_code == 200 assert len(response.json()) == 1 response = APIClient().get('/api/products/?sub=2') assert response.status_code == 200 assert len(response.json()) == 0 response = APIClient().get(f'/api/products/?int_id={product.id}') assert response.status_code == 200 assert len(response.json()) == 1 response = APIClient().get(f'/api/products/?int_id={product.id + 1}') assert response.status_code == 200 assert len(response.json()) == 0 response = APIClient().get('/api/products/?ordering=in_stock,-price') assert response.status_code == 200 assert len(response.json()) == 1 response = APIClient().get('/api/products/?price_range_min=7') assert response.status_code == 200 assert len(response.json()) == 0 response = APIClient().get('/api/products/?price_range_max=1') assert response.status_code == 200 assert len(response.json()) == 0 response = APIClient().get('/api/products/?price_range_min=1&price_range_max=5') assert response.status_code == 200 assert len(response.json()) == 1 response = APIClient().get('/api/products/?multi_cat=A&multi_cat=B') assert response.status_code == 200, response.content assert len(response.json()) == 1 response = APIClient().get('/api/products/?cat_callable=A') assert response.status_code == 200, response.content assert len(response.json()) == 1 @pytest.mark.contrib('django_filter') def test_through_model_multi_choice_filter(no_warnings): class RelationModel(models.Model): test = models.CharField(max_length=50) class TestModel(models.Model): reltd = models.ManyToManyField(RelationModel, through="ThroughModel") class ThroughModel(models.Model): tm = models.ForeignKey(TestModel, on_delete=models.PROTECT) rm = models.ForeignKey(RelationModel, on_delete=models.PROTECT) class MyFilter(FilterSet): reltd = ModelMultipleChoiceFilter(field_name="reltd", label="reltd") class Meta: model = TestModel fields = ["reltd"] class TestSerializer(serializers.ModelSerializer): class Meta: model = TestModel fields = '__all__' class TestViewSet(viewsets.ModelViewSet): queryset = TestModel.objects.all() serializer_class = TestSerializer filter_backends = [DjangoFilterBackend] filterset_class = MyFilter generate_schema('x', TestViewSet) @pytest.mark.contrib('django_filter') def test_boolean_filter_subclassing_in_different_import_path(no_warnings): # this import is important as there is a override via subclassing # happening in django_filter.rest_framework.filters class DjangoFilterDummyModel(models.Model): seen = models.DateTimeField(null=True) class XSerializer(serializers.ModelSerializer): class Meta: model = DjangoFilterDummyModel fields = "__all__" class XFilterSet(FilterSet): class Meta: model = DjangoFilterDummyModel fields = [] seen = BooleanFilter(field_name="seen", lookup_expr="isnull") class XViewSet(viewsets.ModelViewSet): queryset = DjangoFilterDummyModel.objects.all() serializer_class = XSerializer filterset_class = XFilterSet filter_backends = [DjangoFilterBackend] schema = generate_schema('/x', XViewSet) assert schema['paths']['/x/']['get']['parameters'] == [ {'in': 'query', 'name': 'seen', 'schema': {'type': 'boolean'}} ] @pytest.mark.contrib('django_filter') def test_filters_on_retrieve_operations(no_warnings): from django_filters import FilterSet from django_filters.rest_framework import DjangoFilterBackend class SimpleFilterSet(FilterSet): pages = BaseInFilter(field_name='id') class Meta: model = SimpleModel fields = '__all__' class XViewset(viewsets.GenericViewSet): queryset = SimpleModel.objects.all() serializer_class = SimpleSerializer filterset_class = SimpleFilterSet filter_backends = [DjangoFilterBackend] @extend_schema( responses={(200, 'application/pdf'): OpenApiTypes.BINARY}, filters=True, ) def retrieve(self, request, *args, **kwargs): pass # pragma: no cover schema = generate_schema('/x', XViewset) assert schema['paths']['/x/{id}/']['get']['parameters'][1] == { 'in': 'query', 'name': 'pages', 'schema': {'type': 'array', 'items': {'type': 'integer'}}, 'description': 'Multiple values may be separated by commas.', 'explode': False, 'style': 'form' } @pytest.mark.contrib('django_filter') def test_filters_raw_schema_decoration_isolation(no_warnings): from django_filters import FilterSet from django_filters.rest_framework import DjangoFilterBackend class SimpleFilterSet(FilterSet): filter_field = NumberFilter(method='filter_method') @extend_schema_field({'description': 'raw schema', 'type': 'integer'}) def filter_method(self, queryset, name, value): pass # pragma: no cover class Meta: model = Product fields = [] class XViewset(viewsets.GenericViewSet): queryset = Product.objects.all() serializer_class = SimpleSerializer filterset_class = SimpleFilterSet filter_backends = [DjangoFilterBackend] def list(self, request, *args, **kwargs): pass # pragma: no cover expected = [ {'in': 'query', 'name': 'filter_field', 'schema': {'type': 'integer'}, 'description': 'raw schema'} ] schema = generate_schema('/x', XViewset) assert schema['paths']['/x/']['get']['parameters'] == expected schema = generate_schema('/x', XViewset) assert schema['paths']['/x/']['get']['parameters'] == expected @pytest.mark.contrib('django_filter') @pytest.mark.skipif(DJANGO_VERSION < '3', reason='Not available before Django 3.0') def test_filterset_enum_description_duplication(no_warnings): class ThingType(models.TextChoices): ONE = "one", "One" TWO = "two", "Two" THREE = "three", "Three" class Thing(models.Model): type = models.CharField(max_length=64, choices=ThingType.choices) class ThingSerializer(serializers.ModelSerializer): class Meta: model = Thing fields = ("type",) class ThingFilterSet(FilterSet): class Meta: model = Thing fields = ("type",) type = ChoiceFilter(choices=ThingType.choices) class XViewSet(viewsets.ModelViewSet): queryset = Thing.objects.all() serializer_class = ThingSerializer filter_backends = [DjangoFilterBackend] filterset_class = ThingFilterSet schema = generate_schema('/x', XViewSet) assert schema['paths']['/x/']['get']['parameters'][0]['description'] == ( '* `one` - One\n* `two` - Two\n* `three` - Three' ) @pytest.mark.contrib('django_filter') def test_filter_on_listapiview(no_warnings): class XListView(generics.ListAPIView): queryset = Product.objects.all() serializer_class = ProductSerializer filter_backends = (DjangoFilterBackend,) filterset_class = ProductFilter def get_queryset(self): return Product.objects.all().annotate( price_vat=F('price') * 1.19 ) schema = generate_schema('/x/', view=XListView) assert len(schema['paths']['/x/']['get']['parameters']) > 1 drf-spectacular-0.27.0/tests/contrib/test_django_filters.yml000066400000000000000000000153051453572150400242470ustar00rootroot00000000000000openapi: 3.0.3 info: title: '' version: 0.0.0 paths: /products/: get: operationId: products_list parameters: - in: query name: all_values schema: type: number format: double - in: query name: cat_callable schema: type: string description: some category description - in: query name: category schema: type: string enum: - A - B description: |- some category description * `A` - aaa * `B` - b - in: query name: choice_field_enum_override schema: type: string - in: query name: custom_filter schema: type: boolean - in: query name: custom_underspec_filter schema: type: array items: type: string enum: - A - B description: Multiple values may be separated by commas. explode: false style: form - in: query name: decorated_serializer_field schema: type: string format: uuid - in: query name: email schema: type: string format: email - in: query name: in_categories schema: type: array items: type: string enum: - A - B description: Multiple values may be separated by commas. explode: false style: form - in: query name: in_stock schema: type: boolean - in: query name: int_id schema: type: integer - in: query name: is_free schema: type: boolean - in: query name: max_price schema: type: number format: float description: highest price examples: MagicExample1: value: '1337' summary: Magic example 1 MagicExample2: value: '1234' summary: Magic example 2 - in: query name: max_sub_price schema: type: number format: float - in: query name: model_multi_cat schema: type: array items: type: string enum: - A - B description: |- some category description * `A` - aaa * `B` - b explode: true style: form - in: query name: model_multi_cat_relation schema: type: array items: type: integer explode: true style: form - in: query name: model_single_cat schema: type: string enum: - A - B description: |- some category description * `A` - aaa * `B` - b - in: query name: number_id schema: type: number description: some injected help text - in: query name: number_id_ext schema: type: number - in: query name: ordering schema: type: array items: type: string enum: - -in_stock - -price - in_stock - price description: |- Ordering * `price` - Price * `-price` - Price (descending) * `in_stock` - in stock * `-in_stock` - in stock (descending) explode: false style: form - in: query name: other_sub_product__uuid schema: type: string format: uuid - in: query name: price_range_max schema: type: number format: double - in: query name: price_range_min schema: type: number format: double - in: query name: price_range_vat_decorated_max schema: type: integer - in: query name: price_range_vat_decorated_min schema: type: integer - in: query name: price_range_vat_max schema: type: number format: double - in: query name: price_range_vat_min schema: type: number format: double - in: query name: sub schema: type: integer - in: query name: subproduct__sub_price schema: type: number format: float - in: query name: untyped_choice_field_method_with_explicit_choices schema: type: integer enum: - 1 description: '* `1` - one' - in: query name: untyped_multiple_choice_field_method_with_explicit_choices schema: type: array items: type: integer enum: - 1 description: '* `1` - one' explode: true style: form tags: - products security: - cookieAuth: [] - basicAuth: [] - {} responses: '200': content: application/json: schema: type: array items: $ref: '#/components/schemas/Product' description: '' /products/{id}/: get: operationId: products_retrieve parameters: - in: path name: id schema: type: integer description: A unique integer value identifying this product. required: true tags: - products security: - cookieAuth: [] - basicAuth: [] - {} responses: '200': content: application/json: schema: $ref: '#/components/schemas/Product' description: '' components: schemas: CategoryEnum: enum: - A - B type: string description: |- * `A` - aaa * `B` - b Product: type: object properties: id: type: integer readOnly: true category: allOf: - $ref: '#/components/schemas/CategoryEnum' description: |- some category description * `A` - aaa * `B` - b in_stock: type: boolean price: type: number format: double other_sub_product: type: integer required: - category - id - in_stock - other_sub_product - price securitySchemes: basicAuth: type: http scheme: basic cookieAuth: type: apiKey in: cookie name: sessionid drf-spectacular-0.27.0/tests/contrib/test_djangorestframework_camel_case.py000066400000000000000000000063561453572150400273240ustar00rootroot00000000000000import sys from typing import List from unittest import mock import pytest from rest_framework import mixins, serializers, viewsets from rest_framework.decorators import action # Use 3.9 instead of 3.8 version because of missing __required_keys__ feature, # which allows for a consistent test schema over different versions. if sys.version_info >= (3, 9): from typing import TypedDict else: from typing_extensions import TypedDict from drf_spectacular.contrib.djangorestframework_camel_case import camelize_serializer_fields from drf_spectacular.utils import OpenApiParameter, extend_schema from tests import assert_schema, generate_schema class NestedObject2(TypedDict): field_seven: int field_eight: str class NestedObject(TypedDict): field_three: int field_four: str field_five: NestedObject2 field_six: List[NestedObject2] field_ignored: int class FakeSerializer(serializers.Serializer): field_one = serializers.CharField() field_two = serializers.CharField() field_ignored = serializers.CharField() field_nested = serializers.SerializerMethodField() field_nested_ignored = serializers.SerializerMethodField() def get_field_nested(self) -> NestedObject: # type: ignore pass # pragma: no cover def get_field_nested_ignored(self) -> NestedObject: # type: ignore pass # pragma: no cover @extend_schema( parameters=[ OpenApiParameter( name="field_parameter", description="filter_parameter", required=False, type=str, ), ] ) class FakeViewset(mixins.ListModelMixin, viewsets.GenericViewSet): serializer_class = FakeSerializer @extend_schema(responses=FakeSerializer) @action(detail=False, serializer_class=FakeSerializer) def home(self, request): ... # pragma: no cover @mock.patch( 'drf_spectacular.settings.spectacular_settings.POSTPROCESSING_HOOKS', [camelize_serializer_fields] ) @mock.patch( 'djangorestframework_camel_case.settings.api_settings.JSON_UNDERSCOREIZE', { 'no_underscore_before_number': False, 'ignore_fields': ('field_nested_ignored',), 'ignore_keys': ('field_ignored',), } ) @pytest.mark.contrib('djangorestframework_camel_case') def test_camelize_serializer_fields(no_warnings): assert_schema( generate_schema('a_b_c', FakeViewset), 'tests/contrib/test_djangorestframework_camel_case.yml' ) @mock.patch( 'django.conf.settings.MIDDLEWARE', ['djangorestframework_camel_case.middleware.CamelCaseMiddleWare'], create=True ) @mock.patch( 'drf_spectacular.settings.spectacular_settings.POSTPROCESSING_HOOKS', [camelize_serializer_fields] ) @mock.patch( 'djangorestframework_camel_case.settings.api_settings.JSON_UNDERSCOREIZE', { 'no_underscore_before_number': False, 'ignore_fields': ('field_nested_ignored',), 'ignore_keys': ('field_ignored',), } ) @pytest.mark.contrib('djangorestframework_camel_case') def test_camelize_middleware(no_warnings): assert_schema( generate_schema('a_b_c', FakeViewset), 'tests/contrib/test_djangorestframework_camel_case.yml', reverse_transforms=[lambda x: x.replace("field_parameter", "fieldParameter")] ) drf-spectacular-0.27.0/tests/contrib/test_djangorestframework_camel_case.yml000066400000000000000000000067151453572150400274740ustar00rootroot00000000000000openapi: 3.0.3 info: title: '' version: 0.0.0 paths: /a_b_c/: get: operationId: a_b_c_list parameters: - in: query name: field_parameter schema: type: string description: filter_parameter tags: - a_b_c security: - cookieAuth: [] - basicAuth: [] - {} responses: '200': content: application/json: schema: type: array items: $ref: '#/components/schemas/Fake' description: '' /a_b_c/home/: get: operationId: a_b_c_home_retrieve parameters: - in: query name: field_parameter schema: type: string description: filter_parameter tags: - a_b_c security: - cookieAuth: [] - basicAuth: [] - {} responses: '200': content: application/json: schema: $ref: '#/components/schemas/Fake' description: '' components: schemas: Fake: type: object properties: fieldOne: type: string fieldTwo: type: string field_ignored: type: string fieldNested: type: object properties: fieldThree: type: integer fieldFour: type: string fieldFive: type: object properties: fieldSeven: type: integer fieldEight: type: string required: - fieldEight - fieldSeven fieldSix: type: array items: type: object properties: fieldSeven: type: integer fieldEight: type: string required: - fieldEight - fieldSeven field_ignored: type: integer required: - fieldFive - fieldFour - field_ignored - fieldSix - fieldThree readOnly: true fieldNestedIgnored: type: object properties: field_three: type: integer field_four: type: string field_five: type: object properties: field_seven: type: integer field_eight: type: string required: - field_eight - field_seven field_six: type: array items: type: object properties: field_seven: type: integer field_eight: type: string required: - field_eight - field_seven field_ignored: type: integer required: - field_five - field_four - field_ignored - field_six - field_three readOnly: true required: - field_ignored - fieldNested - fieldNestedIgnored - fieldOne - fieldTwo securitySchemes: basicAuth: type: http scheme: basic cookieAuth: type: apiKey in: cookie name: sessionid drf-spectacular-0.27.0/tests/contrib/test_drf_jwt.py000066400000000000000000000033621453572150400225430ustar00rootroot00000000000000from unittest import mock import pytest from django import __version__ as DJANGO_VERSION from django.urls import path from rest_framework import mixins, routers, serializers, viewsets from tests import assert_schema, generate_schema try: from rest_framework_jwt.authentication import JSONWebTokenAuthentication except ImportError: JSONWebTokenAuthentication = None class XSerializer(serializers.Serializer): uuid = serializers.UUIDField() class XViewset(mixins.ListModelMixin, viewsets.GenericViewSet): serializer_class = XSerializer authentication_classes = [JSONWebTokenAuthentication] required_scopes = ['x:read', 'x:write'] @pytest.mark.skipif(DJANGO_VERSION >= '4', reason='1.19.1 broken for Django 4.0') @pytest.mark.contrib('rest_framework_jwt') def test_drf_jwt(no_warnings): from rest_framework_jwt.views import obtain_jwt_token router = routers.SimpleRouter() router.register('x', XViewset, basename="x") urlpatterns = [ *router.urls, path('api-token-auth/', obtain_jwt_token, name='get_token'), ] schema = generate_schema(None, patterns=urlpatterns) assert_schema(schema, 'tests/contrib/test_drf_jwt.yml') @pytest.mark.skipif(DJANGO_VERSION >= '4', reason='1.19.1 broken for Django 4.0') @pytest.mark.contrib('rest_framework_jwt') @mock.patch('rest_framework_jwt.settings.api_settings.JWT_AUTH_HEADER_PREFIX', 'JWT') def test_drf_jwt_non_bearer_keyword(no_warnings): schema = generate_schema('/x', XViewset) assert schema['components']['securitySchemes'] == { 'jwtAuth': { 'type': 'apiKey', 'in': 'header', 'name': 'Authorization', 'description': 'Token-based authentication with required prefix "JWT"' }, } drf-spectacular-0.27.0/tests/contrib/test_drf_jwt.yml000066400000000000000000000040221453572150400227060ustar00rootroot00000000000000openapi: 3.0.3 info: title: '' version: 0.0.0 paths: /api-token-auth/: post: operationId: api_token_auth_create description: |- API View that receives a POST with a user's username and password. Returns a JSON Web Token that can be used for authenticated requests. tags: - api-token-auth requestBody: content: application/json: schema: $ref: '#/components/schemas/JSONWebToken' application/x-www-form-urlencoded: schema: $ref: '#/components/schemas/JSONWebToken' multipart/form-data: schema: $ref: '#/components/schemas/JSONWebToken' required: true responses: '200': content: application/json: schema: $ref: '#/components/schemas/JSONWebToken' description: '' /x/: get: operationId: x_list tags: - x security: - jwtAuth: [] - {} responses: '200': content: application/json: schema: type: array items: $ref: '#/components/schemas/X' description: '' components: schemas: JSONWebToken: type: object description: |- Serializer class used to validate a username and password. 'username' is identified by the custom UserModel.USERNAME_FIELD. Returns a JSON Web Token that can be used to authenticate later calls. properties: password: type: string writeOnly: true token: type: string readOnly: true username: type: string writeOnly: true required: - password - token - username X: type: object properties: uuid: type: string format: uuid required: - uuid securitySchemes: jwtAuth: type: http scheme: bearer bearerFormat: JWT drf-spectacular-0.27.0/tests/contrib/test_drf_nested_routers.py000066400000000000000000000057071453572150400250110ustar00rootroot00000000000000from unittest import mock import pytest from django.db import models from django.urls import include, re_path from rest_framework import serializers, viewsets from rest_framework.routers import SimpleRouter from tests import assert_schema, generate_schema class Root(models.Model): id = models.UUIDField(primary_key=True) name = models.CharField(max_length=255) class Child(models.Model): name = models.CharField(max_length=255) parent = models.ForeignKey(Root, on_delete=models.CASCADE) class RootSerializer(serializers.HyperlinkedModelSerializer): class Meta: model = Root fields = ('name',) class ChildSerializer(serializers.HyperlinkedModelSerializer): class Meta: model = Child fields = ('name', 'parent',) def _generate_nested_routers_schema(root_viewset, child_viewset): from rest_framework_nested.routers import NestedSimpleRouter router = SimpleRouter() router.register('root', root_viewset, basename='root') root_router = NestedSimpleRouter(router, r'root', lookup='parent') root_router.register(r'child', child_viewset, basename='child') urlpatterns = [ re_path(r'^', include(router.urls)), re_path(r'^', include(root_router.urls)), ] return generate_schema(None, patterns=urlpatterns) @pytest.mark.contrib('drf_nested_routers') @pytest.mark.parametrize('coerce_suffix,transforms', [ (False, []), (True, [lambda x: x.replace('parent_pk', 'parent_id')]), ]) def test_drf_nested_routers_basic_example(no_warnings, coerce_suffix, transforms): class RootViewSet(viewsets.ModelViewSet): serializer_class = RootSerializer queryset = Root.objects.all() class ChildViewSet(viewsets.ModelViewSet): serializer_class = ChildSerializer queryset = Child.objects.all() with mock.patch( 'drf_spectacular.settings.spectacular_settings.SCHEMA_COERCE_PATH_PK_SUFFIX', coerce_suffix ): assert_schema( _generate_nested_routers_schema(RootViewSet, ChildViewSet), 'tests/contrib/test_drf_nested_routers.yml', reverse_transforms=transforms ) @pytest.mark.contrib('rest_framework_nested') def test_drf_nested_routers_basic_example_variation(no_warnings): class RootViewSet(viewsets.ModelViewSet): queryset = Root.objects.all() serializer_class = RootSerializer lookup_value_regex = "[0-9]+" class ChildViewSet(viewsets.ModelViewSet): serializer_class = ChildSerializer def get_queryset(self): return Child.objects.filter(id=self.kwargs.get("parent_pk")) assert_schema( _generate_nested_routers_schema(RootViewSet, ChildViewSet), 'tests/contrib/test_drf_nested_routers.yml', reverse_transforms=[ lambda x: x.replace('format: uuid', 'pattern: ^[0-9]+$'), lambda x: x.replace('\n description: A UUID string identifying this root.', '') ] ) drf-spectacular-0.27.0/tests/contrib/test_drf_nested_routers.yml000066400000000000000000000215321453572150400251540ustar00rootroot00000000000000openapi: 3.0.3 info: title: '' version: 0.0.0 paths: /root/: get: operationId: root_list security: - cookieAuth: [] - basicAuth: [] - {} responses: '200': content: application/json: schema: type: array items: $ref: '#/components/schemas/Root' description: '' post: operationId: root_create requestBody: content: application/json: schema: $ref: '#/components/schemas/Root' application/x-www-form-urlencoded: schema: $ref: '#/components/schemas/Root' multipart/form-data: schema: $ref: '#/components/schemas/Root' required: true security: - cookieAuth: [] - basicAuth: [] - {} responses: '201': content: application/json: schema: $ref: '#/components/schemas/Root' description: '' /root/{parent_pk}/child/: get: operationId: child_list parameters: - in: path name: parent_pk schema: type: string format: uuid required: true tags: - child security: - cookieAuth: [] - basicAuth: [] - {} responses: '200': content: application/json: schema: type: array items: $ref: '#/components/schemas/Child' description: '' post: operationId: child_create parameters: - in: path name: parent_pk schema: type: string format: uuid required: true tags: - child requestBody: content: application/json: schema: $ref: '#/components/schemas/Child' application/x-www-form-urlencoded: schema: $ref: '#/components/schemas/Child' multipart/form-data: schema: $ref: '#/components/schemas/Child' required: true security: - cookieAuth: [] - basicAuth: [] - {} responses: '201': content: application/json: schema: $ref: '#/components/schemas/Child' description: '' /root/{parent_pk}/child/{id}/: get: operationId: child_retrieve parameters: - in: path name: id schema: type: integer description: A unique integer value identifying this child. required: true - in: path name: parent_pk schema: type: string format: uuid required: true tags: - child security: - cookieAuth: [] - basicAuth: [] - {} responses: '200': content: application/json: schema: $ref: '#/components/schemas/Child' description: '' put: operationId: child_update parameters: - in: path name: id schema: type: integer description: A unique integer value identifying this child. required: true - in: path name: parent_pk schema: type: string format: uuid required: true tags: - child requestBody: content: application/json: schema: $ref: '#/components/schemas/Child' application/x-www-form-urlencoded: schema: $ref: '#/components/schemas/Child' multipart/form-data: schema: $ref: '#/components/schemas/Child' required: true security: - cookieAuth: [] - basicAuth: [] - {} responses: '200': content: application/json: schema: $ref: '#/components/schemas/Child' description: '' patch: operationId: child_partial_update parameters: - in: path name: id schema: type: integer description: A unique integer value identifying this child. required: true - in: path name: parent_pk schema: type: string format: uuid required: true tags: - child requestBody: content: application/json: schema: $ref: '#/components/schemas/PatchedChild' application/x-www-form-urlencoded: schema: $ref: '#/components/schemas/PatchedChild' multipart/form-data: schema: $ref: '#/components/schemas/PatchedChild' security: - cookieAuth: [] - basicAuth: [] - {} responses: '200': content: application/json: schema: $ref: '#/components/schemas/Child' description: '' delete: operationId: child_destroy parameters: - in: path name: id schema: type: integer description: A unique integer value identifying this child. required: true - in: path name: parent_pk schema: type: string format: uuid required: true tags: - child security: - cookieAuth: [] - basicAuth: [] - {} responses: '204': description: No response body /root/{id}/: get: operationId: root_retrieve parameters: - in: path name: id schema: type: string format: uuid description: A UUID string identifying this root. required: true security: - cookieAuth: [] - basicAuth: [] - {} responses: '200': content: application/json: schema: $ref: '#/components/schemas/Root' description: '' put: operationId: root_update parameters: - in: path name: id schema: type: string format: uuid description: A UUID string identifying this root. required: true requestBody: content: application/json: schema: $ref: '#/components/schemas/Root' application/x-www-form-urlencoded: schema: $ref: '#/components/schemas/Root' multipart/form-data: schema: $ref: '#/components/schemas/Root' required: true security: - cookieAuth: [] - basicAuth: [] - {} responses: '200': content: application/json: schema: $ref: '#/components/schemas/Root' description: '' patch: operationId: root_partial_update parameters: - in: path name: id schema: type: string format: uuid description: A UUID string identifying this root. required: true requestBody: content: application/json: schema: $ref: '#/components/schemas/PatchedRoot' application/x-www-form-urlencoded: schema: $ref: '#/components/schemas/PatchedRoot' multipart/form-data: schema: $ref: '#/components/schemas/PatchedRoot' security: - cookieAuth: [] - basicAuth: [] - {} responses: '200': content: application/json: schema: $ref: '#/components/schemas/Root' description: '' delete: operationId: root_destroy parameters: - in: path name: id schema: type: string format: uuid description: A UUID string identifying this root. required: true security: - cookieAuth: [] - basicAuth: [] - {} responses: '204': description: No response body components: schemas: Child: type: object properties: name: type: string maxLength: 255 parent: type: string format: uri required: - name - parent PatchedChild: type: object properties: name: type: string maxLength: 255 parent: type: string format: uri PatchedRoot: type: object properties: name: type: string maxLength: 255 Root: type: object properties: name: type: string maxLength: 255 required: - name securitySchemes: basicAuth: type: http scheme: basic cookieAuth: type: apiKey in: cookie name: sessionid drf-spectacular-0.27.0/tests/contrib/test_drf_spectacular_sidecar.py000066400000000000000000000034701453572150400257370ustar00rootroot00000000000000import inspect import os from unittest import mock import pytest from django.urls import path from rest_framework.test import APIClient from drf_spectacular.views import SpectacularAPIView, SpectacularRedocView, SpectacularSwaggerView urlpatterns = [ path('api/schema/', SpectacularAPIView.as_view(), name='schema'), path('api/schema/swagger-ui/', SpectacularSwaggerView.as_view(), name='swagger'), path('api/schema/redoc/', SpectacularRedocView.as_view(), name='redoc'), ] BUNDLE_URL = "static/drf_spectacular_sidecar/swagger-ui-dist/swagger-ui-bundle.js" @mock.patch('drf_spectacular.settings.spectacular_settings.SWAGGER_UI_DIST', 'SIDECAR') @mock.patch('drf_spectacular.settings.spectacular_settings.SWAGGER_UI_FAVICON_HREF', 'SIDECAR') @mock.patch('drf_spectacular.settings.spectacular_settings.REDOC_DIST', 'SIDECAR') @pytest.mark.urls(__name__) @pytest.mark.contrib('drf_spectacular_sidecar') def test_sidecar_shortcut_urls_are_resolved(no_warnings): response = APIClient().get('/api/schema/swagger-ui/') assert b'"/' + BUNDLE_URL.encode() + b'"' in response.content assert b'"/static/drf_spectacular_sidecar/swagger-ui-dist/favicon-32x32.png"' in response.content response = APIClient().get('/api/schema/redoc/') assert b'"/static/drf_spectacular_sidecar/redoc/bundles/redoc.standalone.js"' in response.content @pytest.mark.contrib('drf_spectacular_sidecar') def test_sidecar_package_urls_matching(no_warnings): # poor man's test to make sure the sidecar package contents match with what # collectstatic is going to compile. cannot be tested directly. import drf_spectacular_sidecar # type: ignore[import-not-found] module_root = os.path.dirname(inspect.getfile(drf_spectacular_sidecar)) bundle_path = os.path.join(module_root, BUNDLE_URL) assert os.path.isfile(bundle_path) drf-spectacular-0.27.0/tests/contrib/test_oauth_toolkit.py000066400000000000000000000110521453572150400237640ustar00rootroot00000000000000from unittest import mock import pytest from django.urls import include, path from oauth2_provider.scopes import BaseScopes from rest_framework import mixins, routers, serializers, viewsets from rest_framework.authentication import BasicAuthentication from tests import assert_schema, generate_schema try: from oauth2_provider.contrib.rest_framework import ( IsAuthenticatedOrTokenHasScope, OAuth2Authentication, TokenHasReadWriteScope, TokenHasResourceScope, TokenMatchesOASRequirements, ) except ImportError: IsAuthenticatedOrTokenHasScope = None OAuth2Authentication = None TokenHasReadWriteScope = None TokenHasResourceScope = None TokenMatchesOASRequirements = None class XSerializer(serializers.Serializer): uuid = serializers.UUIDField() class TokenHasReadWriteScopeViewset(mixins.ListModelMixin, viewsets.GenericViewSet): serializer_class = XSerializer authentication_classes = [OAuth2Authentication] permission_classes = [TokenHasReadWriteScope] # required_scopes is not mandatory here class TokenHasResourceScopeViewset(mixins.ListModelMixin, viewsets.GenericViewSet): serializer_class = XSerializer authentication_classes = [OAuth2Authentication] permission_classes = [TokenHasResourceScope] required_scopes = ['extra_scope'] class IsAuthenticatedOrTokenHasScopeViewset(mixins.ListModelMixin, viewsets.GenericViewSet): serializer_class = XSerializer authentication_classes = [OAuth2Authentication, BasicAuthentication] permission_classes = [IsAuthenticatedOrTokenHasScope] required_scopes = ['extra_scope'] class OASRequirementsViewset(mixins.CreateModelMixin, mixins.ListModelMixin, viewsets.GenericViewSet): serializer_class = XSerializer authentication_classes = [OAuth2Authentication] permission_classes = [TokenMatchesOASRequirements] required_alternate_scopes = { 'GET': [['read']], 'POST': [['create1', 'scope2'], ['alt-scope3'], ['alt-scope4', 'alt-scope5']], } class TestScopesBackend(BaseScopes): def get_all_scopes(self): return {'test_backend_scope': 'Test scope for ScopesBackend'} @mock.patch( 'drf_spectacular.settings.spectacular_settings.OAUTH2_FLOWS', ['implicit'] ) @mock.patch( 'drf_spectacular.settings.spectacular_settings.OAUTH2_REFRESH_URL', 'http://127.0.0.1:8000/o/refresh' ) @mock.patch( 'drf_spectacular.settings.spectacular_settings.OAUTH2_AUTHORIZATION_URL', 'http://127.0.0.1:8000/o/authorize' ) @mock.patch( 'oauth2_provider.settings.oauth2_settings.SCOPES', {"read": "Reading scope", "write": "Writing scope", "extra_scope": "Extra Scope"}, ) @mock.patch( 'oauth2_provider.settings.oauth2_settings.DEFAULT_SCOPES', ["read", "write"] ) @pytest.mark.contrib('oauth2_provider') def test_oauth2_toolkit(no_warnings): router = routers.SimpleRouter() router.register('TokenHasReadWriteScope', TokenHasReadWriteScopeViewset, basename="x1") router.register('TokenHasResourceScope', TokenHasResourceScopeViewset, basename="x2") router.register('IsAuthenticatedOrTokenHasScope', IsAuthenticatedOrTokenHasScopeViewset, basename="x3") router.register('OASRequirements', OASRequirementsViewset, basename="x4") urlpatterns = [ *router.urls, path('o/', include('oauth2_provider.urls', namespace='oauth2_provider')), ] schema = generate_schema(None, patterns=urlpatterns) assert_schema(schema, 'tests/contrib/test_oauth_toolkit.yml') @mock.patch( 'drf_spectacular.settings.spectacular_settings.OAUTH2_FLOWS', ['implicit'] ) @mock.patch( 'drf_spectacular.settings.spectacular_settings.OAUTH2_REFRESH_URL', 'http://127.0.0.1:8000/o/refresh' ) @mock.patch( 'drf_spectacular.settings.spectacular_settings.OAUTH2_AUTHORIZATION_URL', 'http://127.0.0.1:8000/o/authorize' ) @mock.patch( 'oauth2_provider.settings.oauth2_settings.SCOPES_BACKEND_CLASS', TestScopesBackend, ) @pytest.mark.contrib('oauth2_provider') def test_oauth2_toolkit_scopes_backend(no_warnings): router = routers.SimpleRouter() router.register('TokenHasReadWriteScope', TokenHasReadWriteScopeViewset, basename='x') urlpatterns = [ *router.urls, path('o/', include('oauth2_provider.urls', namespace='oauth2_provider')), ] schema = generate_schema(None, patterns=urlpatterns) assert 'oauth2' in schema['components']['securitySchemes'] oauth2 = schema['components']['securitySchemes']['oauth2'] assert 'implicit' in oauth2['flows'] flow = oauth2['flows']['implicit'] assert 'test_backend_scope' in flow['scopes'] drf-spectacular-0.27.0/tests/contrib/test_oauth_toolkit.yml000066400000000000000000000057131453572150400241440ustar00rootroot00000000000000openapi: 3.0.3 info: title: '' version: 0.0.0 paths: /IsAuthenticatedOrTokenHasScope/: get: operationId: IsAuthenticatedOrTokenHasScope_list tags: - IsAuthenticatedOrTokenHasScope security: - oauth2: - extra_scope - basicAuth: [] responses: '200': content: application/json: schema: type: array items: $ref: '#/components/schemas/X' description: '' /OASRequirements/: get: operationId: OASRequirements_list tags: - OASRequirements security: - oauth2: - read responses: '200': content: application/json: schema: type: array items: $ref: '#/components/schemas/X' description: '' post: operationId: OASRequirements_create tags: - OASRequirements requestBody: content: application/json: schema: $ref: '#/components/schemas/X' application/x-www-form-urlencoded: schema: $ref: '#/components/schemas/X' multipart/form-data: schema: $ref: '#/components/schemas/X' required: true security: - oauth2: - create1 - scope2 - oauth2: - alt-scope3 - oauth2: - alt-scope4 - alt-scope5 responses: '201': content: application/json: schema: $ref: '#/components/schemas/X' description: '' /TokenHasReadWriteScope/: get: operationId: TokenHasReadWriteScope_list tags: - TokenHasReadWriteScope security: - oauth2: - read responses: '200': content: application/json: schema: type: array items: $ref: '#/components/schemas/X' description: '' /TokenHasResourceScope/: get: operationId: TokenHasResourceScope_list tags: - TokenHasResourceScope security: - oauth2: - extra_scope:read responses: '200': content: application/json: schema: type: array items: $ref: '#/components/schemas/X' description: '' components: schemas: X: type: object properties: uuid: type: string format: uuid required: - uuid securitySchemes: basicAuth: type: http scheme: basic oauth2: type: oauth2 flows: implicit: authorizationUrl: http://127.0.0.1:8000/o/authorize refreshUrl: http://127.0.0.1:8000/o/refresh scopes: read: Reading scope write: Writing scope extra_scope: Extra Scope drf-spectacular-0.27.0/tests/contrib/test_pydantic.py000066400000000000000000000016551453572150400227220ustar00rootroot00000000000000import sys from typing import List import pytest from rest_framework.views import APIView from drf_spectacular.utils import extend_schema from tests import assert_schema, generate_schema try: from pydantic import BaseModel from pydantic.dataclasses import dataclass except ImportError: class BaseModel: # type: ignore pass def dataclass(f): return f @dataclass class C: id: int class B(BaseModel): id: int c: List[C] class A(BaseModel): id: int b: B @pytest.mark.contrib('pydantic') @pytest.mark.skipif(sys.version_info < (3, 7), reason='python 3.7+ is required by package') def test_pydantic_decoration(no_warnings): class XAPIView(APIView): @extend_schema(request=A, responses=B) def post(self, request): pass # pragma: no cover schema = generate_schema('/x', view=XAPIView) assert_schema(schema, 'tests/contrib/test_pydantic.yml') drf-spectacular-0.27.0/tests/contrib/test_pydantic.yml000066400000000000000000000027431453572150400230720ustar00rootroot00000000000000openapi: 3.0.3 info: title: '' version: 0.0.0 paths: /x: post: operationId: x_create tags: - x requestBody: content: application/json: schema: $ref: '#/components/schemas/A' application/x-www-form-urlencoded: schema: $ref: '#/components/schemas/A' multipart/form-data: schema: $ref: '#/components/schemas/A' required: true security: - cookieAuth: [] - basicAuth: [] - {} responses: '200': content: application/json: schema: $ref: '#/components/schemas/B' description: '' components: schemas: A: properties: id: title: Id type: integer b: $ref: '#/components/schemas/B' required: - id - b title: A type: object B: properties: id: title: Id type: integer c: items: $ref: '#/components/schemas/C' title: C type: array required: - id - c title: B type: object C: properties: id: title: Id type: integer required: - id title: C type: object securitySchemes: basicAuth: type: http scheme: basic cookieAuth: type: apiKey in: cookie name: sessionid drf-spectacular-0.27.0/tests/contrib/test_rest_auth.py000066400000000000000000000051201453572150400230740ustar00rootroot00000000000000import re from importlib import reload from unittest import mock import pytest from django.urls import include, path from rest_framework import viewsets from tests import assert_schema, generate_schema from tests.models import SimpleModel, SimpleSerializer transforms = [ # User model first_name differences lambda x: re.sub(r'(first_name:\n *type: string\n *maxLength:) 30', r'\g<1> 150', x, re.M), ] @pytest.mark.contrib('dj_rest_auth', 'allauth') @mock.patch('drf_spectacular.settings.spectacular_settings.SCHEMA_PATH_PREFIX', '') def test_rest_auth(no_warnings): urlpatterns = [ path('rest-auth/', include('dj_rest_auth.urls')), path('rest-auth/registration/', include('dj_rest_auth.registration.urls')), ] schema = generate_schema(None, patterns=urlpatterns) assert_schema( schema, 'tests/contrib/test_rest_auth.yml', transforms=transforms ) @pytest.mark.contrib('dj_rest_auth', 'allauth', 'rest_framework_simplejwt') @mock.patch('drf_spectacular.settings.spectacular_settings.SCHEMA_PATH_PREFIX', '') @mock.patch('dj_rest_auth.app_settings.api_settings.USE_JWT', True) def test_rest_auth_token(no_warnings, settings): # flush module import cache to re-evaluate conditional import import dj_rest_auth.urls reload(dj_rest_auth.urls) urlpatterns = [ # path('rest-auth/', include(urlpatterns)), path('rest-auth/', include('dj_rest_auth.urls')), path('rest-auth/registration/', include('dj_rest_auth.registration.urls')), ] schema = generate_schema(None, patterns=urlpatterns) assert_schema( schema, 'tests/contrib/test_rest_auth_token.yml', transforms=transforms ) @pytest.mark.contrib('dj_rest_auth', 'rest_framework_simplejwt') @mock.patch('django.conf.settings.JWT_AUTH_COOKIE', 'jwt-session', create=True) @mock.patch('dj_rest_auth.app_settings.api_settings.JWT_AUTH_COOKIE', 'jwt-session', create=True) def test_rest_auth_simplejwt_cookie(no_warnings): from dj_rest_auth.jwt_auth import JWTCookieAuthentication class XViewset(viewsets.ModelViewSet): serializer_class = SimpleSerializer queryset = SimpleModel.objects.all() authentication_classes = [JWTCookieAuthentication] schema = generate_schema('/x', XViewset) assert schema['paths']['/x/']['get']['security'] == [ {'jwtHeaderAuth': []}, {'jwtCookieAuth': []}, {} ] assert schema['components']['securitySchemes'] == { 'jwtCookieAuth': {'type': 'apiKey', 'in': 'cookie', 'name': 'jwt-session'}, 'jwtHeaderAuth': {'type': 'http', 'scheme': 'bearer', 'bearerFormat': 'JWT'} } drf-spectacular-0.27.0/tests/contrib/test_rest_auth.yml000066400000000000000000000321231453572150400232500ustar00rootroot00000000000000openapi: 3.0.3 info: title: '' version: 0.0.0 paths: /rest-auth/login/: post: operationId: rest_auth_login_create description: |- Check the credentials and return the REST Token if the credentials are valid and authenticated. Calls Django Auth login method to register User ID in Django session framework Accept the following POST parameters: username, password Return the REST Framework Token Object's key. tags: - rest-auth requestBody: content: application/json: schema: $ref: '#/components/schemas/Login' application/x-www-form-urlencoded: schema: $ref: '#/components/schemas/Login' multipart/form-data: schema: $ref: '#/components/schemas/Login' required: true security: - cookieAuth: [] - basicAuth: [] - {} responses: '200': content: application/json: schema: $ref: '#/components/schemas/Token' description: '' /rest-auth/logout/: post: operationId: rest_auth_logout_create description: |- Calls Django logout method and delete the Token object assigned to the current User object. Accepts/Returns nothing. tags: - rest-auth security: - cookieAuth: [] - basicAuth: [] - {} responses: '200': content: application/json: schema: $ref: '#/components/schemas/RestAuthDetail' description: '' /rest-auth/password/change/: post: operationId: rest_auth_password_change_create description: |- Calls Django Auth SetPasswordForm save method. Accepts the following POST parameters: new_password1, new_password2 Returns the success/fail message. tags: - rest-auth requestBody: content: application/json: schema: $ref: '#/components/schemas/PasswordChange' application/x-www-form-urlencoded: schema: $ref: '#/components/schemas/PasswordChange' multipart/form-data: schema: $ref: '#/components/schemas/PasswordChange' required: true security: - cookieAuth: [] - basicAuth: [] responses: '200': content: application/json: schema: $ref: '#/components/schemas/RestAuthDetail' description: '' /rest-auth/password/reset/: post: operationId: rest_auth_password_reset_create description: |- Calls Django Auth PasswordResetForm save method. Accepts the following POST parameters: email Returns the success/fail message. tags: - rest-auth requestBody: content: application/json: schema: $ref: '#/components/schemas/PasswordReset' application/x-www-form-urlencoded: schema: $ref: '#/components/schemas/PasswordReset' multipart/form-data: schema: $ref: '#/components/schemas/PasswordReset' required: true security: - cookieAuth: [] - basicAuth: [] - {} responses: '200': content: application/json: schema: $ref: '#/components/schemas/RestAuthDetail' description: '' /rest-auth/password/reset/confirm/: post: operationId: rest_auth_password_reset_confirm_create description: |- Password reset e-mail link is confirmed, therefore this resets the user's password. Accepts the following POST parameters: token, uid, new_password1, new_password2 Returns the success/fail message. tags: - rest-auth requestBody: content: application/json: schema: $ref: '#/components/schemas/PasswordResetConfirm' application/x-www-form-urlencoded: schema: $ref: '#/components/schemas/PasswordResetConfirm' multipart/form-data: schema: $ref: '#/components/schemas/PasswordResetConfirm' required: true security: - cookieAuth: [] - basicAuth: [] - {} responses: '200': content: application/json: schema: $ref: '#/components/schemas/RestAuthDetail' description: '' /rest-auth/registration/: post: operationId: rest_auth_registration_create tags: - rest-auth requestBody: content: application/json: schema: $ref: '#/components/schemas/Register' application/x-www-form-urlencoded: schema: $ref: '#/components/schemas/Register' multipart/form-data: schema: $ref: '#/components/schemas/Register' required: true security: - cookieAuth: [] - basicAuth: [] - {} responses: '201': content: application/json: schema: $ref: '#/components/schemas/Token' description: '' /rest-auth/registration/resend-email/: post: operationId: rest_auth_registration_resend_email_create tags: - rest-auth requestBody: content: application/json: schema: $ref: '#/components/schemas/ResendEmailVerification' application/x-www-form-urlencoded: schema: $ref: '#/components/schemas/ResendEmailVerification' multipart/form-data: schema: $ref: '#/components/schemas/ResendEmailVerification' security: - cookieAuth: [] - basicAuth: [] - {} responses: '201': content: application/json: schema: $ref: '#/components/schemas/RestAuthDetail' description: '' /rest-auth/registration/verify-email/: post: operationId: rest_auth_registration_verify_email_create tags: - rest-auth requestBody: content: application/json: schema: $ref: '#/components/schemas/VerifyEmail' application/x-www-form-urlencoded: schema: $ref: '#/components/schemas/VerifyEmail' multipart/form-data: schema: $ref: '#/components/schemas/VerifyEmail' required: true security: - cookieAuth: [] - basicAuth: [] - {} responses: '200': content: application/json: schema: $ref: '#/components/schemas/RestAuthDetail' description: '' /rest-auth/user/: get: operationId: rest_auth_user_retrieve description: |- Reads and updates UserModel fields Accepts GET, PUT, PATCH methods. Default accepted fields: username, first_name, last_name Default display fields: pk, username, email, first_name, last_name Read-only fields: pk, email Returns UserModel fields. tags: - rest-auth security: - cookieAuth: [] - basicAuth: [] responses: '200': content: application/json: schema: $ref: '#/components/schemas/UserDetails' description: '' put: operationId: rest_auth_user_update description: |- Reads and updates UserModel fields Accepts GET, PUT, PATCH methods. Default accepted fields: username, first_name, last_name Default display fields: pk, username, email, first_name, last_name Read-only fields: pk, email Returns UserModel fields. tags: - rest-auth requestBody: content: application/json: schema: $ref: '#/components/schemas/UserDetails' application/x-www-form-urlencoded: schema: $ref: '#/components/schemas/UserDetails' multipart/form-data: schema: $ref: '#/components/schemas/UserDetails' required: true security: - cookieAuth: [] - basicAuth: [] responses: '200': content: application/json: schema: $ref: '#/components/schemas/UserDetails' description: '' patch: operationId: rest_auth_user_partial_update description: |- Reads and updates UserModel fields Accepts GET, PUT, PATCH methods. Default accepted fields: username, first_name, last_name Default display fields: pk, username, email, first_name, last_name Read-only fields: pk, email Returns UserModel fields. tags: - rest-auth requestBody: content: application/json: schema: $ref: '#/components/schemas/PatchedUserDetails' application/x-www-form-urlencoded: schema: $ref: '#/components/schemas/PatchedUserDetails' multipart/form-data: schema: $ref: '#/components/schemas/PatchedUserDetails' security: - cookieAuth: [] - basicAuth: [] responses: '200': content: application/json: schema: $ref: '#/components/schemas/UserDetails' description: '' components: schemas: Login: type: object properties: username: type: string email: type: string format: email password: type: string required: - password PasswordChange: type: object properties: new_password1: type: string maxLength: 128 new_password2: type: string maxLength: 128 required: - new_password1 - new_password2 PasswordReset: type: object description: Serializer for requesting a password reset e-mail. properties: email: type: string format: email required: - email PasswordResetConfirm: type: object description: Serializer for confirming a password reset attempt. properties: new_password1: type: string maxLength: 128 new_password2: type: string maxLength: 128 uid: type: string token: type: string required: - new_password1 - new_password2 - token - uid PatchedUserDetails: type: object description: User model w/o password properties: pk: type: integer readOnly: true title: ID username: type: string description: Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only. pattern: ^[\w.@+-]+$ maxLength: 150 email: type: string format: email readOnly: true title: Email address first_name: type: string maxLength: 150 last_name: type: string maxLength: 150 Register: type: object properties: username: type: string maxLength: 150 minLength: 1 email: type: string format: email password1: type: string writeOnly: true password2: type: string writeOnly: true required: - password1 - password2 - username ResendEmailVerification: type: object properties: email: type: string format: email RestAuthDetail: type: object properties: detail: type: string readOnly: true required: - detail Token: type: object description: Serializer for Token model. properties: key: type: string maxLength: 40 required: - key UserDetails: type: object description: User model w/o password properties: pk: type: integer readOnly: true title: ID username: type: string description: Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only. pattern: ^[\w.@+-]+$ maxLength: 150 email: type: string format: email readOnly: true title: Email address first_name: type: string maxLength: 150 last_name: type: string maxLength: 150 required: - email - pk - username VerifyEmail: type: object properties: key: type: string writeOnly: true required: - key securitySchemes: basicAuth: type: http scheme: basic cookieAuth: type: apiKey in: cookie name: sessionid drf-spectacular-0.27.0/tests/contrib/test_rest_auth_token.yml000066400000000000000000000363461453572150400244630ustar00rootroot00000000000000openapi: 3.0.3 info: title: '' version: 0.0.0 paths: /rest-auth/login/: post: operationId: rest_auth_login_create description: |- Check the credentials and return the REST Token if the credentials are valid and authenticated. Calls Django Auth login method to register User ID in Django session framework Accept the following POST parameters: username, password Return the REST Framework Token Object's key. tags: - rest-auth requestBody: content: application/json: schema: $ref: '#/components/schemas/Login' application/x-www-form-urlencoded: schema: $ref: '#/components/schemas/Login' multipart/form-data: schema: $ref: '#/components/schemas/Login' required: true security: - cookieAuth: [] - basicAuth: [] - {} responses: '200': content: application/json: schema: $ref: '#/components/schemas/JWT' description: '' /rest-auth/logout/: post: operationId: rest_auth_logout_create description: |- Calls Django logout method and delete the Token object assigned to the current User object. Accepts/Returns nothing. tags: - rest-auth security: - cookieAuth: [] - basicAuth: [] - {} responses: '200': content: application/json: schema: $ref: '#/components/schemas/RestAuthDetail' description: '' /rest-auth/password/change/: post: operationId: rest_auth_password_change_create description: |- Calls Django Auth SetPasswordForm save method. Accepts the following POST parameters: new_password1, new_password2 Returns the success/fail message. tags: - rest-auth requestBody: content: application/json: schema: $ref: '#/components/schemas/PasswordChange' application/x-www-form-urlencoded: schema: $ref: '#/components/schemas/PasswordChange' multipart/form-data: schema: $ref: '#/components/schemas/PasswordChange' required: true security: - cookieAuth: [] - basicAuth: [] responses: '200': content: application/json: schema: $ref: '#/components/schemas/RestAuthDetail' description: '' /rest-auth/password/reset/: post: operationId: rest_auth_password_reset_create description: |- Calls Django Auth PasswordResetForm save method. Accepts the following POST parameters: email Returns the success/fail message. tags: - rest-auth requestBody: content: application/json: schema: $ref: '#/components/schemas/PasswordReset' application/x-www-form-urlencoded: schema: $ref: '#/components/schemas/PasswordReset' multipart/form-data: schema: $ref: '#/components/schemas/PasswordReset' required: true security: - cookieAuth: [] - basicAuth: [] - {} responses: '200': content: application/json: schema: $ref: '#/components/schemas/RestAuthDetail' description: '' /rest-auth/password/reset/confirm/: post: operationId: rest_auth_password_reset_confirm_create description: |- Password reset e-mail link is confirmed, therefore this resets the user's password. Accepts the following POST parameters: token, uid, new_password1, new_password2 Returns the success/fail message. tags: - rest-auth requestBody: content: application/json: schema: $ref: '#/components/schemas/PasswordResetConfirm' application/x-www-form-urlencoded: schema: $ref: '#/components/schemas/PasswordResetConfirm' multipart/form-data: schema: $ref: '#/components/schemas/PasswordResetConfirm' required: true security: - cookieAuth: [] - basicAuth: [] - {} responses: '200': content: application/json: schema: $ref: '#/components/schemas/RestAuthDetail' description: '' /rest-auth/registration/: post: operationId: rest_auth_registration_create tags: - rest-auth requestBody: content: application/json: schema: $ref: '#/components/schemas/Register' application/x-www-form-urlencoded: schema: $ref: '#/components/schemas/Register' multipart/form-data: schema: $ref: '#/components/schemas/Register' required: true security: - cookieAuth: [] - basicAuth: [] - {} responses: '201': content: application/json: schema: $ref: '#/components/schemas/JWT' description: '' /rest-auth/registration/resend-email/: post: operationId: rest_auth_registration_resend_email_create tags: - rest-auth requestBody: content: application/json: schema: $ref: '#/components/schemas/ResendEmailVerification' application/x-www-form-urlencoded: schema: $ref: '#/components/schemas/ResendEmailVerification' multipart/form-data: schema: $ref: '#/components/schemas/ResendEmailVerification' security: - cookieAuth: [] - basicAuth: [] - {} responses: '201': content: application/json: schema: $ref: '#/components/schemas/RestAuthDetail' description: '' /rest-auth/registration/verify-email/: post: operationId: rest_auth_registration_verify_email_create tags: - rest-auth requestBody: content: application/json: schema: $ref: '#/components/schemas/VerifyEmail' application/x-www-form-urlencoded: schema: $ref: '#/components/schemas/VerifyEmail' multipart/form-data: schema: $ref: '#/components/schemas/VerifyEmail' required: true security: - cookieAuth: [] - basicAuth: [] - {} responses: '200': content: application/json: schema: $ref: '#/components/schemas/RestAuthDetail' description: '' /rest-auth/token/refresh/: post: operationId: rest_auth_token_refresh_create description: |- Takes a refresh type JSON web token and returns an access type JSON web token if the refresh token is valid. tags: - rest-auth requestBody: content: application/json: schema: $ref: '#/components/schemas/TokenRefresh' application/x-www-form-urlencoded: schema: $ref: '#/components/schemas/TokenRefresh' multipart/form-data: schema: $ref: '#/components/schemas/TokenRefresh' required: true responses: '200': content: application/json: schema: $ref: '#/components/schemas/TokenRefresh' description: '' /rest-auth/token/verify/: post: operationId: rest_auth_token_verify_create description: |- Takes a token and indicates if it is valid. This view provides no information about a token's fitness for a particular use. tags: - rest-auth requestBody: content: application/json: schema: $ref: '#/components/schemas/TokenVerify' application/x-www-form-urlencoded: schema: $ref: '#/components/schemas/TokenVerify' multipart/form-data: schema: $ref: '#/components/schemas/TokenVerify' required: true responses: '200': content: application/json: schema: $ref: '#/components/schemas/TokenVerify' description: '' /rest-auth/user/: get: operationId: rest_auth_user_retrieve description: |- Reads and updates UserModel fields Accepts GET, PUT, PATCH methods. Default accepted fields: username, first_name, last_name Default display fields: pk, username, email, first_name, last_name Read-only fields: pk, email Returns UserModel fields. tags: - rest-auth security: - cookieAuth: [] - basicAuth: [] responses: '200': content: application/json: schema: $ref: '#/components/schemas/UserDetails' description: '' put: operationId: rest_auth_user_update description: |- Reads and updates UserModel fields Accepts GET, PUT, PATCH methods. Default accepted fields: username, first_name, last_name Default display fields: pk, username, email, first_name, last_name Read-only fields: pk, email Returns UserModel fields. tags: - rest-auth requestBody: content: application/json: schema: $ref: '#/components/schemas/UserDetails' application/x-www-form-urlencoded: schema: $ref: '#/components/schemas/UserDetails' multipart/form-data: schema: $ref: '#/components/schemas/UserDetails' required: true security: - cookieAuth: [] - basicAuth: [] responses: '200': content: application/json: schema: $ref: '#/components/schemas/UserDetails' description: '' patch: operationId: rest_auth_user_partial_update description: |- Reads and updates UserModel fields Accepts GET, PUT, PATCH methods. Default accepted fields: username, first_name, last_name Default display fields: pk, username, email, first_name, last_name Read-only fields: pk, email Returns UserModel fields. tags: - rest-auth requestBody: content: application/json: schema: $ref: '#/components/schemas/PatchedUserDetails' application/x-www-form-urlencoded: schema: $ref: '#/components/schemas/PatchedUserDetails' multipart/form-data: schema: $ref: '#/components/schemas/PatchedUserDetails' security: - cookieAuth: [] - basicAuth: [] responses: '200': content: application/json: schema: $ref: '#/components/schemas/UserDetails' description: '' components: schemas: JWT: type: object description: Serializer for JWT authentication. properties: access: type: string refresh: type: string user: $ref: '#/components/schemas/UserDetails' required: - access - refresh - user Login: type: object properties: username: type: string email: type: string format: email password: type: string required: - password PasswordChange: type: object properties: new_password1: type: string maxLength: 128 new_password2: type: string maxLength: 128 required: - new_password1 - new_password2 PasswordReset: type: object description: Serializer for requesting a password reset e-mail. properties: email: type: string format: email required: - email PasswordResetConfirm: type: object description: Serializer for confirming a password reset attempt. properties: new_password1: type: string maxLength: 128 new_password2: type: string maxLength: 128 uid: type: string token: type: string required: - new_password1 - new_password2 - token - uid PatchedUserDetails: type: object description: User model w/o password properties: pk: type: integer readOnly: true title: ID username: type: string description: Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only. pattern: ^[\w.@+-]+$ maxLength: 150 email: type: string format: email readOnly: true title: Email address first_name: type: string maxLength: 150 last_name: type: string maxLength: 150 Register: type: object properties: username: type: string maxLength: 150 minLength: 1 email: type: string format: email password1: type: string writeOnly: true password2: type: string writeOnly: true required: - password1 - password2 - username ResendEmailVerification: type: object properties: email: type: string format: email RestAuthDetail: type: object properties: detail: type: string readOnly: true required: - detail TokenRefresh: type: object properties: access: type: string readOnly: true refresh: type: string writeOnly: true required: - access - refresh TokenVerify: type: object properties: token: type: string writeOnly: true required: - token UserDetails: type: object description: User model w/o password properties: pk: type: integer readOnly: true title: ID username: type: string description: Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only. pattern: ^[\w.@+-]+$ maxLength: 150 email: type: string format: email readOnly: true title: Email address first_name: type: string maxLength: 150 last_name: type: string maxLength: 150 required: - email - pk - username VerifyEmail: type: object properties: key: type: string writeOnly: true required: - key securitySchemes: basicAuth: type: http scheme: basic cookieAuth: type: apiKey in: cookie name: sessionid drf-spectacular-0.27.0/tests/contrib/test_rest_framework_dataclasses.py000066400000000000000000000052011453572150400264770ustar00rootroot00000000000000import sys import typing import pytest from django.urls import path from rest_framework.decorators import api_view from drf_spectacular.utils import extend_schema, extend_schema_serializer from tests import assert_schema, generate_schema @pytest.mark.contrib('rest_framework_dataclasses') @pytest.mark.skipif(sys.version_info < (3, 7), reason='dataclass required by package') def test_rest_framework_dataclasses(no_warnings): from dataclasses import dataclass from rest_framework_dataclasses.serializers import DataclassSerializer @dataclass class PersonDetail: name: str length: int @dataclass class Person: name: str length: int detail: PersonDetail @dataclass class Group: name: str leader: Person members: typing.List[Person] class GroupSerializer(DataclassSerializer): class Meta: dataclass = Group class GroupSerializer2(DataclassSerializer): class Meta: dataclass = Group ref_name = "CustomGroupNameFromRefName" @extend_schema_serializer(component_name='CustomGroupNameFromSerializerDecoration') class GroupSerializer3(DataclassSerializer[Group]): class Meta: dataclass = Group @extend_schema_serializer(component_name='CustomGroupNameFromDecoration') @dataclass class Group2: name: str leader: Person members: typing.List[Person] @extend_schema(responses=GroupSerializer) @api_view(['GET']) def named(request): pass # pragma: no cover @extend_schema(responses=DataclassSerializer(dataclass=Person)) @api_view(['GET']) def anonymous(request): pass # pragma: no cover @extend_schema(responses=GroupSerializer2(many=True)) @api_view(['GET']) def custom_name_via_ref(request): pass # pragma: no cover @extend_schema(responses=DataclassSerializer(dataclass=Group2)) @api_view(['GET']) def custom_name_via_decoration(request): pass # pragma: no cover @extend_schema(responses=GroupSerializer3) @api_view(['GET']) def custom_name_via_serializer_decoration(request): pass # pragma: no cover urlpatterns = [ path('named', named), path('anonymous', anonymous), path('custom_name_via_ref', custom_name_via_ref), path('custom_name_via_decoration', custom_name_via_decoration), path('custom_name_via_serializer_decoration', custom_name_via_serializer_decoration) ] assert_schema( generate_schema(None, patterns=urlpatterns), 'tests/contrib/test_rest_framework_dataclasses.yml' ) drf-spectacular-0.27.0/tests/contrib/test_rest_framework_dataclasses.yml000066400000000000000000000076061453572150400266630ustar00rootroot00000000000000openapi: 3.0.3 info: title: '' version: 0.0.0 paths: /anonymous: get: operationId: anonymous_retrieve tags: - anonymous security: - cookieAuth: [] - basicAuth: [] - {} responses: '200': content: application/json: schema: $ref: '#/components/schemas/Person' description: '' /custom_name_via_decoration: get: operationId: custom_name_via_decoration_retrieve tags: - custom_name_via_decoration security: - cookieAuth: [] - basicAuth: [] - {} responses: '200': content: application/json: schema: $ref: '#/components/schemas/CustomGroupNameFromDecoration' description: '' /custom_name_via_ref: get: operationId: custom_name_via_ref_list tags: - custom_name_via_ref security: - cookieAuth: [] - basicAuth: [] - {} responses: '200': content: application/json: schema: type: array items: $ref: '#/components/schemas/CustomGroupNameFromRefName' description: '' /custom_name_via_serializer_decoration: get: operationId: custom_name_via_serializer_decoration_retrieve tags: - custom_name_via_serializer_decoration security: - cookieAuth: [] - basicAuth: [] - {} responses: '200': content: application/json: schema: $ref: '#/components/schemas/CustomGroupNameFromSerializerDecoration' description: '' /named: get: operationId: named_retrieve tags: - named security: - cookieAuth: [] - basicAuth: [] - {} responses: '200': content: application/json: schema: $ref: '#/components/schemas/Group' description: '' components: schemas: CustomGroupNameFromDecoration: type: object properties: name: type: string leader: $ref: '#/components/schemas/Person' members: type: array items: $ref: '#/components/schemas/Person' required: - leader - members - name CustomGroupNameFromRefName: type: object properties: name: type: string leader: $ref: '#/components/schemas/Person' members: type: array items: $ref: '#/components/schemas/Person' required: - leader - members - name CustomGroupNameFromSerializerDecoration: type: object properties: name: type: string leader: $ref: '#/components/schemas/Person' members: type: array items: $ref: '#/components/schemas/Person' required: - leader - members - name Group: type: object properties: name: type: string leader: $ref: '#/components/schemas/Person' members: type: array items: $ref: '#/components/schemas/Person' required: - leader - members - name Person: type: object properties: name: type: string length: type: integer detail: $ref: '#/components/schemas/PersonDetail' required: - detail - length - name PersonDetail: type: object properties: name: type: string length: type: integer required: - length - name securitySchemes: basicAuth: type: http scheme: basic cookieAuth: type: apiKey in: cookie name: sessionid drf-spectacular-0.27.0/tests/contrib/test_rest_framework_gis.py000066400000000000000000000123631453572150400250010ustar00rootroot00000000000000from unittest import mock import pytest from django.db import models from rest_framework import __version__ as DRF_VERSION # type: ignore[attr-defined] from rest_framework import mixins, routers, serializers, viewsets from drf_spectacular.utils import extend_schema_serializer from tests import assert_schema, generate_schema, is_gis_installed @pytest.mark.contrib('rest_framework_gis') @pytest.mark.system_requirement_fulfilled(is_gis_installed()) @pytest.mark.skipif(DRF_VERSION < '3.12', reason='DRF pagination schema broken') @mock.patch('drf_spectacular.settings.spectacular_settings.ENUM_NAME_OVERRIDES', {}) def test_rest_framework_gis(no_warnings, clear_caches, django_transforms): from django.contrib.gis.db.models import ( GeometryCollectionField, GeometryField, LineStringField, MultiLineStringField, MultiPointField, MultiPolygonField, PointField, PolygonField, ) from rest_framework_gis.fields import GeometryField as SerializerGeometryField from rest_framework_gis.pagination import GeoJsonPagination from rest_framework_gis.serializers import GeoFeatureModelSerializer class GeoModel(models.Model): field_random1 = models.CharField(max_length=32) field_random2 = models.IntegerField() field_gis_plain = PointField() field_polygon = PolygonField() field_point = PointField() field_linestring = LineStringField() field_geometry = GeometryField() field_multipolygon = MultiPolygonField() field_multipoint = MultiPointField() field_multilinestring = MultiLineStringField() field_geometrycollection = GeometryCollectionField() class GeoModel2(models.Model): related_model = models.OneToOneField(GeoModel, on_delete=models.DO_NOTHING) field_point = PointField() router = routers.SimpleRouter() # all GIS fields as GeoJSON in singular and list form fields = [ 'Point', 'Polygon', 'Linestring', 'Geometry', 'Multipoint', 'Multipolygon', 'Multilinestring', 'Geometrycollection' ] for name in fields: @extend_schema_serializer(component_name=name) class XSerializer(GeoFeatureModelSerializer): class Meta: model = GeoModel geo_field = f'field_{name.lower()}' auto_bbox = name == 'Polygon' fields = ['id', 'field_random1', 'field_random2', 'field_gis_plain'] class XViewset(mixins.RetrieveModelMixin, mixins.ListModelMixin, viewsets.GenericViewSet): serializer_class = XSerializer queryset = GeoModel.objects.none() router.register(name.lower(), XViewset, basename=name) # plain serializer with GIS fields but without restructured container object class PlainSerializer(serializers.ModelSerializer): field_gis_related = SerializerGeometryField(source="geomodel2.field_point") class Meta: model = GeoModel fields = ['id', 'field_random1', 'field_random2', 'field_gis_plain', 'field_gis_related'] class PlainViewset(mixins.RetrieveModelMixin, mixins.ListModelMixin, viewsets.GenericViewSet): serializer_class = PlainSerializer queryset = GeoModel.objects.none() router.register('plain', PlainViewset, basename='plain') # GIS specific pagination class PlainViewset(mixins.RetrieveModelMixin, mixins.ListModelMixin, viewsets.GenericViewSet): serializer_class = PlainSerializer queryset = GeoModel.objects.none() pagination_class = GeoJsonPagination router.register('paginated', PlainViewset, basename='paginated') assert_schema( generate_schema(None, patterns=router.urls), 'tests/contrib/test_rest_framework_gis.yml', transforms=django_transforms, ) @pytest.mark.contrib('rest_framework_gis', 'django_filter') @pytest.mark.system_requirement_fulfilled(is_gis_installed()) @mock.patch('drf_spectacular.settings.spectacular_settings.ENUM_NAME_OVERRIDES', {}) def test_geo_filter_set(no_warnings): from django.contrib.gis.db.models import PointField from django_filters import filters from django_filters.rest_framework import DjangoFilterBackend from rest_framework_gis.filters import GeometryFilter from rest_framework_gis.filterset import GeoFilterSet class GeoRegionModel(models.Model): slug = models.CharField(max_length=32) geom = PointField() class RegionFilter(GeoFilterSet): slug = filters.CharFilter(field_name='slug', lookup_expr='istartswith') contains_geom = GeometryFilter(field_name='geom', lookup_expr='contains') class Meta: model = GeoRegionModel fields = ['slug', 'contains_geom'] class XSerializer(serializers.Serializer): slug = serializers.CharField() class XViewset(mixins.ListModelMixin, viewsets.GenericViewSet): serializer_class = XSerializer queryset = GeoRegionModel.objects.none() filterset_class = RegionFilter filter_backends = [DjangoFilterBackend] schema = generate_schema('/x', XViewset) assert schema['paths']['/x/']['get']['parameters'] == [ {'in': 'query', 'name': 'contains_geom', 'schema': {'type': 'string'}}, {'in': 'query', 'name': 'slug', 'schema': {'type': 'string'}} ] drf-spectacular-0.27.0/tests/contrib/test_rest_framework_gis.yml000066400000000000000000000722021453572150400251500ustar00rootroot00000000000000openapi: 3.0.3 info: title: '' version: 0.0.0 paths: /geometry/: get: operationId: geometry_list tags: - geometry security: - cookieAuth: [] - basicAuth: [] - {} responses: '200': content: application/json: schema: $ref: '#/components/schemas/GeometryList' description: '' /geometry/{id}/: get: operationId: geometry_retrieve parameters: - in: path name: id schema: type: integer description: A unique integer value identifying this geo model. required: true tags: - geometry security: - cookieAuth: [] - basicAuth: [] - {} responses: '200': content: application/json: schema: $ref: '#/components/schemas/Geometry' description: '' /geometrycollection/: get: operationId: geometrycollection_list tags: - geometrycollection security: - cookieAuth: [] - basicAuth: [] - {} responses: '200': content: application/json: schema: $ref: '#/components/schemas/GeometrycollectionList' description: '' /geometrycollection/{id}/: get: operationId: geometrycollection_retrieve parameters: - in: path name: id schema: type: integer description: A unique integer value identifying this geo model. required: true tags: - geometrycollection security: - cookieAuth: [] - basicAuth: [] - {} responses: '200': content: application/json: schema: $ref: '#/components/schemas/Geometrycollection' description: '' /linestring/: get: operationId: linestring_list tags: - linestring security: - cookieAuth: [] - basicAuth: [] - {} responses: '200': content: application/json: schema: $ref: '#/components/schemas/LinestringList' description: '' /linestring/{id}/: get: operationId: linestring_retrieve parameters: - in: path name: id schema: type: integer description: A unique integer value identifying this geo model. required: true tags: - linestring security: - cookieAuth: [] - basicAuth: [] - {} responses: '200': content: application/json: schema: $ref: '#/components/schemas/Linestring' description: '' /multilinestring/: get: operationId: multilinestring_list tags: - multilinestring security: - cookieAuth: [] - basicAuth: [] - {} responses: '200': content: application/json: schema: $ref: '#/components/schemas/MultilinestringList' description: '' /multilinestring/{id}/: get: operationId: multilinestring_retrieve parameters: - in: path name: id schema: type: integer description: A unique integer value identifying this geo model. required: true tags: - multilinestring security: - cookieAuth: [] - basicAuth: [] - {} responses: '200': content: application/json: schema: $ref: '#/components/schemas/Multilinestring' description: '' /multipoint/: get: operationId: multipoint_list tags: - multipoint security: - cookieAuth: [] - basicAuth: [] - {} responses: '200': content: application/json: schema: $ref: '#/components/schemas/MultipointList' description: '' /multipoint/{id}/: get: operationId: multipoint_retrieve parameters: - in: path name: id schema: type: integer description: A unique integer value identifying this geo model. required: true tags: - multipoint security: - cookieAuth: [] - basicAuth: [] - {} responses: '200': content: application/json: schema: $ref: '#/components/schemas/Multipoint' description: '' /multipolygon/: get: operationId: multipolygon_list tags: - multipolygon security: - cookieAuth: [] - basicAuth: [] - {} responses: '200': content: application/json: schema: $ref: '#/components/schemas/MultipolygonList' description: '' /multipolygon/{id}/: get: operationId: multipolygon_retrieve parameters: - in: path name: id schema: type: integer description: A unique integer value identifying this geo model. required: true tags: - multipolygon security: - cookieAuth: [] - basicAuth: [] - {} responses: '200': content: application/json: schema: $ref: '#/components/schemas/Multipolygon' description: '' /paginated/: get: operationId: paginated_list parameters: - name: page required: false in: query description: A page number within the paginated result set. schema: type: integer - name: page_size required: false in: query description: Number of results to return per page. schema: type: integer tags: - paginated security: - cookieAuth: [] - basicAuth: [] - {} responses: '200': content: application/json: schema: $ref: '#/components/schemas/PaginatedPlainList' description: '' /paginated/{id}/: get: operationId: paginated_retrieve parameters: - in: path name: id schema: type: integer description: A unique integer value identifying this geo model. required: true tags: - paginated security: - cookieAuth: [] - basicAuth: [] - {} responses: '200': content: application/json: schema: $ref: '#/components/schemas/Plain' description: '' /plain/: get: operationId: plain_list tags: - plain security: - cookieAuth: [] - basicAuth: [] - {} responses: '200': content: application/json: schema: type: array items: $ref: '#/components/schemas/Plain' description: '' /plain/{id}/: get: operationId: plain_retrieve parameters: - in: path name: id schema: type: integer description: A unique integer value identifying this geo model. required: true tags: - plain security: - cookieAuth: [] - basicAuth: [] - {} responses: '200': content: application/json: schema: $ref: '#/components/schemas/Plain' description: '' /point/: get: operationId: point_list tags: - point security: - cookieAuth: [] - basicAuth: [] - {} responses: '200': content: application/json: schema: $ref: '#/components/schemas/PointList' description: '' /point/{id}/: get: operationId: point_retrieve parameters: - in: path name: id schema: type: integer description: A unique integer value identifying this geo model. required: true tags: - point security: - cookieAuth: [] - basicAuth: [] - {} responses: '200': content: application/json: schema: $ref: '#/components/schemas/Point' description: '' /polygon/: get: operationId: polygon_list tags: - polygon security: - cookieAuth: [] - basicAuth: [] - {} responses: '200': content: application/json: schema: $ref: '#/components/schemas/PolygonList' description: '' /polygon/{id}/: get: operationId: polygon_retrieve parameters: - in: path name: id schema: type: integer description: A unique integer value identifying this geo model. required: true tags: - polygon security: - cookieAuth: [] - basicAuth: [] - {} responses: '200': content: application/json: schema: $ref: '#/components/schemas/Polygon' description: '' components: schemas: Geometry: type: object properties: type: $ref: '#/components/schemas/GisFeatureEnum' id: type: integer readOnly: true geometry: oneOf: - type: object properties: type: type: string enum: - Point coordinates: type: array items: type: number format: float example: - 12.9721 - 77.5933 minItems: 2 maxItems: 3 - type: object properties: type: type: string enum: - LineString coordinates: type: array items: type: array items: type: number format: float example: - 12.9721 - 77.5933 minItems: 2 maxItems: 3 example: - - 22.4707 - 70.0577 - - 12.9721 - 77.5933 minItems: 2 - type: object properties: type: type: string enum: - Polygon coordinates: type: array items: type: array items: type: array items: type: number format: float example: - 12.9721 - 77.5933 minItems: 2 maxItems: 3 example: - - 22.4707 - 70.0577 - - 12.9721 - 77.5933 minItems: 4 example: - - - 0.0 - 0.0 - - 0.0 - 50.0 - - 50.0 - 50.0 - - 50.0 - 0.0 - - 0.0 - 0.0 properties: type: object properties: field_random1: type: string maxLength: 32 field_random2: type: integer field_gis_plain: type: object properties: type: type: string enum: - Point coordinates: type: array items: type: number format: float example: - 12.9721 - 77.5933 minItems: 2 maxItems: 3 GeometryList: type: object properties: type: $ref: '#/components/schemas/GisFeatureCollectionEnum' features: type: array items: $ref: '#/components/schemas/Geometry' Geometrycollection: type: object properties: type: $ref: '#/components/schemas/GisFeatureEnum' id: type: integer readOnly: true geometry: type: object properties: type: type: string enum: - GeometryCollection coordinates: type: array items: oneOf: - type: object properties: type: type: string enum: - Point coordinates: type: array items: type: number format: float example: - 12.9721 - 77.5933 minItems: 2 maxItems: 3 - type: object properties: type: type: string enum: - LineString coordinates: type: array items: type: array items: type: number format: float example: - 12.9721 - 77.5933 minItems: 2 maxItems: 3 example: - - 22.4707 - 70.0577 - - 12.9721 - 77.5933 minItems: 2 - type: object properties: type: type: string enum: - Polygon coordinates: type: array items: type: array items: type: array items: type: number format: float example: - 12.9721 - 77.5933 minItems: 2 maxItems: 3 example: - - 22.4707 - 70.0577 - - 12.9721 - 77.5933 minItems: 4 example: - - - 0.0 - 0.0 - - 0.0 - 50.0 - - 50.0 - 50.0 - - 50.0 - 0.0 - - 0.0 - 0.0 properties: type: object properties: field_random1: type: string maxLength: 32 field_random2: type: integer field_gis_plain: type: object properties: type: type: string enum: - Point coordinates: type: array items: type: number format: float example: - 12.9721 - 77.5933 minItems: 2 maxItems: 3 GeometrycollectionList: type: object properties: type: $ref: '#/components/schemas/GisFeatureCollectionEnum' features: type: array items: $ref: '#/components/schemas/Geometrycollection' GisFeatureCollectionEnum: type: string enum: - FeatureCollection GisFeatureEnum: type: string enum: - Feature Linestring: type: object properties: type: $ref: '#/components/schemas/GisFeatureEnum' id: type: integer readOnly: true geometry: type: object properties: type: type: string enum: - LineString coordinates: type: array items: type: array items: type: number format: float example: - 12.9721 - 77.5933 minItems: 2 maxItems: 3 example: - - 22.4707 - 70.0577 - - 12.9721 - 77.5933 minItems: 2 properties: type: object properties: field_random1: type: string maxLength: 32 field_random2: type: integer field_gis_plain: type: object properties: type: type: string enum: - Point coordinates: type: array items: type: number format: float example: - 12.9721 - 77.5933 minItems: 2 maxItems: 3 LinestringList: type: object properties: type: $ref: '#/components/schemas/GisFeatureCollectionEnum' features: type: array items: $ref: '#/components/schemas/Linestring' Multilinestring: type: object properties: type: $ref: '#/components/schemas/GisFeatureEnum' id: type: integer readOnly: true geometry: type: object properties: type: type: string enum: - MultiLineString coordinates: type: array items: type: array items: type: array items: type: number format: float example: - 12.9721 - 77.5933 minItems: 2 maxItems: 3 example: - - 22.4707 - 70.0577 - - 12.9721 - 77.5933 minItems: 2 properties: type: object properties: field_random1: type: string maxLength: 32 field_random2: type: integer field_gis_plain: type: object properties: type: type: string enum: - Point coordinates: type: array items: type: number format: float example: - 12.9721 - 77.5933 minItems: 2 maxItems: 3 MultilinestringList: type: object properties: type: $ref: '#/components/schemas/GisFeatureCollectionEnum' features: type: array items: $ref: '#/components/schemas/Multilinestring' Multipoint: type: object properties: type: $ref: '#/components/schemas/GisFeatureEnum' id: type: integer readOnly: true geometry: type: object properties: type: type: string enum: - MultiPoint coordinates: type: array items: type: array items: type: number format: float example: - 12.9721 - 77.5933 minItems: 2 maxItems: 3 properties: type: object properties: field_random1: type: string maxLength: 32 field_random2: type: integer field_gis_plain: type: object properties: type: type: string enum: - Point coordinates: type: array items: type: number format: float example: - 12.9721 - 77.5933 minItems: 2 maxItems: 3 MultipointList: type: object properties: type: $ref: '#/components/schemas/GisFeatureCollectionEnum' features: type: array items: $ref: '#/components/schemas/Multipoint' Multipolygon: type: object properties: type: $ref: '#/components/schemas/GisFeatureEnum' id: type: integer readOnly: true geometry: type: object properties: type: type: string enum: - MultiPolygon coordinates: type: array items: type: array items: type: array items: type: array items: type: number format: float example: - 12.9721 - 77.5933 minItems: 2 maxItems: 3 example: - - 22.4707 - 70.0577 - - 12.9721 - 77.5933 minItems: 4 example: - - - 0.0 - 0.0 - - 0.0 - 50.0 - - 50.0 - 50.0 - - 50.0 - 0.0 - - 0.0 - 0.0 properties: type: object properties: field_random1: type: string maxLength: 32 field_random2: type: integer field_gis_plain: type: object properties: type: type: string enum: - Point coordinates: type: array items: type: number format: float example: - 12.9721 - 77.5933 minItems: 2 maxItems: 3 MultipolygonList: type: object properties: type: $ref: '#/components/schemas/GisFeatureCollectionEnum' features: type: array items: $ref: '#/components/schemas/Multipolygon' PaginatedPlainList: type: object properties: type: $ref: '#/components/schemas/GisFeatureCollectionEnum' count: type: integer example: 123 next: type: string nullable: true format: uri example: http://api.example.org/accounts/?page=4 previous: type: string nullable: true format: uri example: http://api.example.org/accounts/?page=2 features: type: array items: $ref: '#/components/schemas/Plain' Plain: type: object properties: id: type: integer readOnly: true field_random1: type: string maxLength: 32 field_random2: type: integer field_gis_plain: type: object properties: type: type: string enum: - Point coordinates: type: array items: type: number format: float example: - 12.9721 - 77.5933 minItems: 2 maxItems: 3 field_gis_related: type: object properties: type: type: string enum: - Point coordinates: type: array items: type: number format: float example: - 12.9721 - 77.5933 minItems: 2 maxItems: 3 required: - field_gis_plain - field_gis_related - field_random1 - field_random2 - id Point: type: object properties: type: $ref: '#/components/schemas/GisFeatureEnum' id: type: integer readOnly: true geometry: type: object properties: type: type: string enum: - Point coordinates: type: array items: type: number format: float example: - 12.9721 - 77.5933 minItems: 2 maxItems: 3 properties: type: object properties: field_random1: type: string maxLength: 32 field_random2: type: integer field_gis_plain: type: object properties: type: type: string enum: - Point coordinates: type: array items: type: number format: float example: - 12.9721 - 77.5933 minItems: 2 maxItems: 3 PointList: type: object properties: type: $ref: '#/components/schemas/GisFeatureCollectionEnum' features: type: array items: $ref: '#/components/schemas/Point' Polygon: type: object properties: type: $ref: '#/components/schemas/GisFeatureEnum' id: type: integer readOnly: true geometry: type: object properties: type: type: string enum: - Polygon coordinates: type: array items: type: array items: type: array items: type: number format: float example: - 12.9721 - 77.5933 minItems: 2 maxItems: 3 example: - - 22.4707 - 70.0577 - - 12.9721 - 77.5933 minItems: 4 example: - - - 0.0 - 0.0 - - 0.0 - 50.0 - - 50.0 - 50.0 - - 50.0 - 0.0 - - 0.0 - 0.0 bbox: type: array items: type: number minItems: 4 maxItems: 4 example: - 12.9721 - 77.5933 - 12.9721 - 77.5933 properties: type: object properties: field_random1: type: string maxLength: 32 field_random2: type: integer field_gis_plain: type: object properties: type: type: string enum: - Point coordinates: type: array items: type: number format: float example: - 12.9721 - 77.5933 minItems: 2 maxItems: 3 PolygonList: type: object properties: type: $ref: '#/components/schemas/GisFeatureCollectionEnum' features: type: array items: $ref: '#/components/schemas/Polygon' securitySchemes: basicAuth: type: http scheme: basic cookieAuth: type: apiKey in: cookie name: sessionid drf-spectacular-0.27.0/tests/contrib/test_rest_framework_recursive.py000066400000000000000000000040021453572150400262150ustar00rootroot00000000000000import pytest from django.urls import path from rest_framework import serializers from rest_framework.decorators import api_view from drf_spectacular.utils import extend_schema from tests import assert_schema, generate_schema try: from rest_framework_recursive.fields import RecursiveField except ImportError: from rest_framework.fields import Field as RecursiveField class TreeSerializer(serializers.Serializer): name = serializers.CharField() children = serializers.ListField(child=RecursiveField()) class TreeManySerializer(serializers.Serializer): name = serializers.CharField() children = RecursiveField(many=True) class PingSerializer(serializers.Serializer): ping_id = serializers.IntegerField() pong = RecursiveField('PongSerializer', required=False) class PongSerializer(serializers.Serializer): pong_id = serializers.IntegerField() ping = PingSerializer() class LinkSerializer(serializers.Serializer): name = serializers.CharField(max_length=25) next = RecursiveField(allow_null=True) @pytest.mark.contrib('rest_framework_recursive') def test_rest_framework_recursive(no_warnings): @extend_schema(request=TreeSerializer, responses=TreeSerializer) @api_view(['POST']) def tree(request): pass # pragma: no cover @extend_schema(request=TreeManySerializer, responses=TreeManySerializer) @api_view(['POST']) def tree_many(request): pass # pragma: no cover @extend_schema(request=PingSerializer, responses=PingSerializer) @api_view(['POST']) def pong(request): pass # pragma: no cover @extend_schema(request=LinkSerializer, responses=LinkSerializer) @api_view(['POST']) def link(request): pass # pragma: no cover urlpatterns = [ path('tree', tree), path('tree_many', tree_many), path('pong', pong), path('link', link) ] assert_schema( generate_schema(None, patterns=urlpatterns), 'tests/contrib/test_rest_framework_recursive.yml' ) drf-spectacular-0.27.0/tests/contrib/test_rest_framework_recursive.yml000066400000000000000000000100331453572150400263670ustar00rootroot00000000000000openapi: 3.0.3 info: title: '' version: 0.0.0 paths: /link: post: operationId: link_create tags: - link requestBody: content: application/json: schema: $ref: '#/components/schemas/Link' application/x-www-form-urlencoded: schema: $ref: '#/components/schemas/Link' multipart/form-data: schema: $ref: '#/components/schemas/Link' required: true security: - cookieAuth: [] - basicAuth: [] - {} responses: '200': content: application/json: schema: $ref: '#/components/schemas/Link' description: '' /pong: post: operationId: pong_create tags: - pong requestBody: content: application/json: schema: $ref: '#/components/schemas/Ping' application/x-www-form-urlencoded: schema: $ref: '#/components/schemas/Ping' multipart/form-data: schema: $ref: '#/components/schemas/Ping' required: true security: - cookieAuth: [] - basicAuth: [] - {} responses: '200': content: application/json: schema: $ref: '#/components/schemas/Ping' description: '' /tree: post: operationId: tree_create tags: - tree requestBody: content: application/json: schema: $ref: '#/components/schemas/Tree' application/x-www-form-urlencoded: schema: $ref: '#/components/schemas/Tree' multipart/form-data: schema: $ref: '#/components/schemas/Tree' required: true security: - cookieAuth: [] - basicAuth: [] - {} responses: '200': content: application/json: schema: $ref: '#/components/schemas/Tree' description: '' /tree_many: post: operationId: tree_many_create tags: - tree_many requestBody: content: application/json: schema: $ref: '#/components/schemas/TreeMany' application/x-www-form-urlencoded: schema: $ref: '#/components/schemas/TreeMany' multipart/form-data: schema: $ref: '#/components/schemas/TreeMany' required: true security: - cookieAuth: [] - basicAuth: [] - {} responses: '200': content: application/json: schema: $ref: '#/components/schemas/TreeMany' description: '' components: schemas: Link: type: object properties: name: type: string maxLength: 25 next: allOf: - $ref: '#/components/schemas/Link' nullable: true required: - name - next Ping: type: object properties: ping_id: type: integer pong: $ref: '#/components/schemas/Pong' required: - ping_id Pong: type: object properties: pong_id: type: integer ping: $ref: '#/components/schemas/Ping' required: - ping - pong_id Tree: type: object properties: name: type: string children: type: array items: $ref: '#/components/schemas/Tree' required: - children - name TreeMany: type: object properties: name: type: string children: type: array items: $ref: '#/components/schemas/TreeMany' required: - children - name securitySchemes: basicAuth: type: http scheme: basic cookieAuth: type: apiKey in: cookie name: sessionid drf-spectacular-0.27.0/tests/contrib/test_rest_polymorphic.py000066400000000000000000000123761453572150400245130ustar00rootroot00000000000000# type: ignore from unittest import mock import pytest from django.db import models from rest_framework import serializers, viewsets from rest_framework.renderers import JSONRenderer from drf_spectacular.helpers import lazy_serializer from tests import assert_schema, generate_schema try: from polymorphic.models import PolymorphicModel from rest_polymorphic.serializers import PolymorphicSerializer except ImportError: class PolymorphicModel(models.Model): pass class PolymorphicSerializer(serializers.Serializer): pass class Person(PolymorphicModel): address = models.CharField(max_length=30) class LegalPerson(Person): company_name = models.CharField(max_length=30) board = models.ManyToManyField('Person', blank=True, related_name='board') class NaturalPerson(Person): first_name = models.CharField(max_length=30) last_name = models.CharField(max_length=30) supervisor = models.ForeignKey('NaturalPerson', blank=True, null=True, on_delete=models.CASCADE) class NomadicPerson(Person): pass class PersonSerializer(PolymorphicSerializer): model_serializer_mapping = { LegalPerson: lazy_serializer('tests.contrib.test_rest_polymorphic.LegalPersonSerializer'), NaturalPerson: lazy_serializer('tests.contrib.test_rest_polymorphic.NaturalPersonSerializer'), NomadicPerson: lazy_serializer('tests.contrib.test_rest_polymorphic.NomadicPersonSerializer'), } def to_resource_type(self, model_or_instance): # custom name for mapping the polymorphic models return model_or_instance._meta.object_name.lower().replace('person', '') class LegalPersonSerializer(serializers.ModelSerializer): # notice that introduces a recursion loop board = PersonSerializer(many=True, read_only=True) class Meta: model = LegalPerson fields = ('id', 'company_name', 'address', 'board') class NaturalPersonSerializer(serializers.ModelSerializer): # special case: PK related field pointing to a field that has 2 properties # - primary_key=True # - OneToOneField to base model (person_ptr_id) supervisor_id = serializers.PrimaryKeyRelatedField(queryset=NaturalPerson.objects.all(), allow_null=True) class Meta: model = NaturalPerson fields = ('id', 'first_name', 'last_name', 'address', 'supervisor_id') class NomadicPersonSerializer(serializers.ModelSerializer): # special case: all fields are read-only. address = serializers.CharField(max_length=30, read_only=True) class Meta: model = NomadicPerson fields = ('id', 'address') class PersonViewSet(viewsets.ModelViewSet): queryset = Person.objects.all() serializer_class = PersonSerializer @pytest.mark.contrib('polymorphic', 'rest_polymorphic') def test_rest_polymorphic(no_warnings): assert_schema( generate_schema('persons', PersonViewSet), 'tests/contrib/test_rest_polymorphic.yml' ) @pytest.mark.contrib('polymorphic', 'rest_polymorphic') @mock.patch('drf_spectacular.settings.spectacular_settings.COMPONENT_SPLIT_REQUEST', True) def test_rest_polymorphic_split_request_with_ro_serializer(no_warnings): schema = generate_schema('persons', PersonViewSet) components = schema['components']['schemas'] assert 'NomadicPersonRequest' not in components # All fields were read-only. assert 'PatchedNomadicPersonRequest' not in components # All fields were read-only. assert components['Person']['oneOf'] == [ {'$ref': '#/components/schemas/LegalPersonTyped'}, {'$ref': '#/components/schemas/NaturalPersonTyped'}, {'$ref': '#/components/schemas/NomadicPersonTyped'} ] assert components['Person']['discriminator']['mapping'] == { 'legal': '#/components/schemas/LegalPersonTyped', 'natural': '#/components/schemas/NaturalPersonTyped', 'nomadic': '#/components/schemas/NomadicPersonTyped' } assert components['PersonRequest']['oneOf'] == [ {'$ref': '#/components/schemas/LegalPersonTypedRequest'}, {'$ref': '#/components/schemas/NaturalPersonTypedRequest'}, {'$ref': '#/components/schemas/NomadicPersonTypedRequest'}, ] assert components['PersonRequest']['discriminator']['mapping'] == { 'legal': '#/components/schemas/LegalPersonTypedRequest', 'natural': '#/components/schemas/NaturalPersonTypedRequest', 'nomadic': '#/components/schemas/NomadicPersonTypedRequest', } assert components['NomadicPersonTypedRequest'] == { 'properties': {'resourcetype': {'type': 'string'}}, 'required': ['resourcetype'], 'type': 'object', } @pytest.mark.contrib('polymorphic', 'rest_polymorphic') @pytest.mark.django_db def test_model_setup_is_valid(): peter = NaturalPerson(first_name='Peter', last_name='Parker') peter.save() may = NaturalPerson(first_name='May', last_name='Parker') may.save() parker_inc = LegalPerson(company_name='Parker Inc', address='NYC') parker_inc.save() parker_inc.board.add(peter, may) spidey_corp = LegalPerson(company_name='Spidey Corp.', address='NYC') spidey_corp.save() spidey_corp.board.add(peter, parker_inc) JSONRenderer().render( PersonSerializer(spidey_corp).data, accepted_media_type='application/json; indent=4' ).decode() drf-spectacular-0.27.0/tests/contrib/test_rest_polymorphic.yml000066400000000000000000000201601453572150400246520ustar00rootroot00000000000000openapi: 3.0.3 info: title: '' version: 0.0.0 paths: /persons/: get: operationId: persons_list tags: - persons security: - cookieAuth: [] - basicAuth: [] - {} responses: '200': content: application/json: schema: type: array items: $ref: '#/components/schemas/Person' description: '' post: operationId: persons_create tags: - persons requestBody: content: application/json: schema: $ref: '#/components/schemas/Person' application/x-www-form-urlencoded: schema: $ref: '#/components/schemas/Person' multipart/form-data: schema: $ref: '#/components/schemas/Person' security: - cookieAuth: [] - basicAuth: [] - {} responses: '201': content: application/json: schema: $ref: '#/components/schemas/Person' description: '' /persons/{id}/: get: operationId: persons_retrieve parameters: - in: path name: id schema: type: integer description: A unique integer value identifying this person. required: true tags: - persons security: - cookieAuth: [] - basicAuth: [] - {} responses: '200': content: application/json: schema: $ref: '#/components/schemas/Person' description: '' put: operationId: persons_update parameters: - in: path name: id schema: type: integer description: A unique integer value identifying this person. required: true tags: - persons requestBody: content: application/json: schema: $ref: '#/components/schemas/Person' application/x-www-form-urlencoded: schema: $ref: '#/components/schemas/Person' multipart/form-data: schema: $ref: '#/components/schemas/Person' security: - cookieAuth: [] - basicAuth: [] - {} responses: '200': content: application/json: schema: $ref: '#/components/schemas/Person' description: '' patch: operationId: persons_partial_update parameters: - in: path name: id schema: type: integer description: A unique integer value identifying this person. required: true tags: - persons requestBody: content: application/json: schema: $ref: '#/components/schemas/PatchedPerson' application/x-www-form-urlencoded: schema: $ref: '#/components/schemas/PatchedPerson' multipart/form-data: schema: $ref: '#/components/schemas/PatchedPerson' security: - cookieAuth: [] - basicAuth: [] - {} responses: '200': content: application/json: schema: $ref: '#/components/schemas/Person' description: '' delete: operationId: persons_destroy parameters: - in: path name: id schema: type: integer description: A unique integer value identifying this person. required: true tags: - persons security: - cookieAuth: [] - basicAuth: [] - {} responses: '204': description: No response body components: schemas: LegalPerson: type: object properties: id: type: integer readOnly: true company_name: type: string maxLength: 30 address: type: string maxLength: 30 board: type: array items: $ref: '#/components/schemas/Person' readOnly: true required: - address - board - company_name - id LegalPersonTyped: allOf: - type: object properties: resourcetype: type: string required: - resourcetype - $ref: '#/components/schemas/LegalPerson' NaturalPerson: type: object properties: id: type: integer readOnly: true first_name: type: string maxLength: 30 last_name: type: string maxLength: 30 address: type: string maxLength: 30 supervisor_id: type: integer nullable: true required: - address - first_name - id - last_name - supervisor_id NaturalPersonTyped: allOf: - type: object properties: resourcetype: type: string required: - resourcetype - $ref: '#/components/schemas/NaturalPerson' NomadicPerson: type: object properties: id: type: integer readOnly: true address: type: string readOnly: true maxLength: 30 required: - address - id NomadicPersonTyped: allOf: - type: object properties: resourcetype: type: string required: - resourcetype - $ref: '#/components/schemas/NomadicPerson' PatchedLegalPerson: type: object properties: id: type: integer readOnly: true company_name: type: string maxLength: 30 address: type: string maxLength: 30 board: type: array items: $ref: '#/components/schemas/Person' readOnly: true PatchedLegalPersonTyped: allOf: - type: object properties: resourcetype: type: string - $ref: '#/components/schemas/PatchedLegalPerson' PatchedNaturalPerson: type: object properties: id: type: integer readOnly: true first_name: type: string maxLength: 30 last_name: type: string maxLength: 30 address: type: string maxLength: 30 supervisor_id: type: integer nullable: true PatchedNaturalPersonTyped: allOf: - type: object properties: resourcetype: type: string - $ref: '#/components/schemas/PatchedNaturalPerson' PatchedNomadicPerson: type: object properties: id: type: integer readOnly: true address: type: string readOnly: true maxLength: 30 PatchedNomadicPersonTyped: allOf: - type: object properties: resourcetype: type: string - $ref: '#/components/schemas/PatchedNomadicPerson' PatchedPerson: oneOf: - $ref: '#/components/schemas/PatchedLegalPersonTyped' - $ref: '#/components/schemas/PatchedNaturalPersonTyped' - $ref: '#/components/schemas/PatchedNomadicPersonTyped' discriminator: propertyName: resourcetype mapping: legal: '#/components/schemas/PatchedLegalPersonTyped' natural: '#/components/schemas/PatchedNaturalPersonTyped' nomadic: '#/components/schemas/PatchedNomadicPersonTyped' Person: oneOf: - $ref: '#/components/schemas/LegalPersonTyped' - $ref: '#/components/schemas/NaturalPersonTyped' - $ref: '#/components/schemas/NomadicPersonTyped' discriminator: propertyName: resourcetype mapping: legal: '#/components/schemas/LegalPersonTyped' natural: '#/components/schemas/NaturalPersonTyped' nomadic: '#/components/schemas/NomadicPersonTyped' securitySchemes: basicAuth: type: http scheme: basic cookieAuth: type: apiKey in: cookie name: sessionid drf-spectacular-0.27.0/tests/contrib/test_simplejwt.py000066400000000000000000000053611453572150400231230ustar00rootroot00000000000000from unittest import mock import pytest from django.urls import path from rest_framework import mixins, routers, serializers, viewsets from tests import assert_schema, generate_schema try: from rest_framework_simplejwt.authentication import ( JWTAuthentication, JWTTokenUserAuthentication, ) from rest_framework_simplejwt.views import ( TokenObtainPairView, TokenObtainSlidingView, TokenRefreshView, TokenVerifyView, ) except ImportError: JWTAuthentication = None JWTTokenUserAuthentication = None class XSerializer(serializers.Serializer): uuid = serializers.UUIDField() class XViewset(mixins.ListModelMixin, viewsets.GenericViewSet): serializer_class = XSerializer authentication_classes = [JWTAuthentication] required_scopes = ['x:read', 'x:write'] class X2Viewset(mixins.ListModelMixin, viewsets.GenericViewSet): serializer_class = XSerializer authentication_classes = [JWTTokenUserAuthentication] required_scopes = ['x:read', 'x:write'] @pytest.mark.contrib('rest_framework_simplejwt') @pytest.mark.parametrize('view', [XViewset, X2Viewset]) def test_simplejwt(no_warnings, view): router = routers.SimpleRouter() router.register('x', view, basename="x") urlpatterns = [ *router.urls, path('token/', TokenObtainPairView.as_view(), name='token_obtain_pair'), path('token-sliding/', TokenObtainSlidingView.as_view(), name='token_obtain_sliding'), path('token/refresh/', TokenRefreshView.as_view(), name='token_refresh'), path('token/verify/', TokenVerifyView.as_view(), name='token_verify'), ] schema = generate_schema(None, patterns=urlpatterns) assert_schema(schema, 'tests/contrib/test_simplejwt.yml') @pytest.mark.contrib('rest_framework_simplejwt') @mock.patch('rest_framework_simplejwt.settings.api_settings.AUTH_HEADER_TYPES', ('JWT',)) def test_simplejwt_non_bearer_keyword(no_warnings): schema = generate_schema('/x', XViewset) assert schema['components']['securitySchemes'] == { 'jwtAuth': { 'type': 'apiKey', 'in': 'header', 'name': 'Authorization', 'description': 'Token-based authentication with required prefix "JWT"' } } @pytest.mark.contrib('rest_framework_simplejwt') @mock.patch( 'rest_framework_simplejwt.settings.api_settings.AUTH_HEADER_NAME', 'HTTP_X_TOKEN', create=True, ) def test_simplejwt_non_std_header_name(no_warnings): schema = generate_schema('/x', XViewset) assert schema['components']['securitySchemes'] == { 'jwtAuth': { 'type': 'apiKey', 'in': 'header', 'name': 'X-token', 'description': 'Token-based authentication with required prefix "Bearer"' } } drf-spectacular-0.27.0/tests/contrib/test_simplejwt.yml000066400000000000000000000117621453572150400232760ustar00rootroot00000000000000openapi: 3.0.3 info: title: '' version: 0.0.0 paths: /token/: post: operationId: token_create description: |- Takes a set of user credentials and returns an access and refresh JSON web token pair to prove the authentication of those credentials. tags: - token requestBody: content: application/json: schema: $ref: '#/components/schemas/TokenObtainPair' application/x-www-form-urlencoded: schema: $ref: '#/components/schemas/TokenObtainPair' multipart/form-data: schema: $ref: '#/components/schemas/TokenObtainPair' required: true responses: '200': content: application/json: schema: $ref: '#/components/schemas/TokenObtainPair' description: '' /token-sliding/: post: operationId: token_sliding_create description: |- Takes a set of user credentials and returns a sliding JSON web token to prove the authentication of those credentials. tags: - token-sliding requestBody: content: application/json: schema: $ref: '#/components/schemas/TokenObtainSliding' application/x-www-form-urlencoded: schema: $ref: '#/components/schemas/TokenObtainSliding' multipart/form-data: schema: $ref: '#/components/schemas/TokenObtainSliding' required: true responses: '200': content: application/json: schema: $ref: '#/components/schemas/TokenObtainSliding' description: '' /token/refresh/: post: operationId: token_refresh_create description: |- Takes a refresh type JSON web token and returns an access type JSON web token if the refresh token is valid. tags: - token requestBody: content: application/json: schema: $ref: '#/components/schemas/TokenRefresh' application/x-www-form-urlencoded: schema: $ref: '#/components/schemas/TokenRefresh' multipart/form-data: schema: $ref: '#/components/schemas/TokenRefresh' required: true responses: '200': content: application/json: schema: $ref: '#/components/schemas/TokenRefresh' description: '' /token/verify/: post: operationId: token_verify_create description: |- Takes a token and indicates if it is valid. This view provides no information about a token's fitness for a particular use. tags: - token requestBody: content: application/json: schema: $ref: '#/components/schemas/TokenVerify' application/x-www-form-urlencoded: schema: $ref: '#/components/schemas/TokenVerify' multipart/form-data: schema: $ref: '#/components/schemas/TokenVerify' required: true responses: '200': content: application/json: schema: $ref: '#/components/schemas/TokenVerify' description: '' /x/: get: operationId: x_list tags: - x security: - jwtAuth: [] - {} responses: '200': content: application/json: schema: type: array items: $ref: '#/components/schemas/X' description: '' components: schemas: TokenObtainPair: type: object properties: username: type: string writeOnly: true password: type: string writeOnly: true access: type: string readOnly: true refresh: type: string readOnly: true required: - access - password - refresh - username TokenObtainSliding: type: object properties: username: type: string writeOnly: true password: type: string writeOnly: true token: type: string readOnly: true required: - password - token - username TokenRefresh: type: object properties: access: type: string readOnly: true refresh: type: string writeOnly: true required: - access - refresh TokenVerify: type: object properties: token: type: string writeOnly: true required: - token X: type: object properties: uuid: type: string format: uuid required: - uuid securitySchemes: jwtAuth: type: http scheme: bearer bearerFormat: JWT drf-spectacular-0.27.0/tests/locale/000077500000000000000000000000001453572150400172665ustar00rootroot00000000000000drf-spectacular-0.27.0/tests/locale/de/000077500000000000000000000000001453572150400176565ustar00rootroot00000000000000drf-spectacular-0.27.0/tests/locale/de/LC_MESSAGES/000077500000000000000000000000001453572150400214435ustar00rootroot00000000000000drf-spectacular-0.27.0/tests/locale/de/LC_MESSAGES/django.mo000066400000000000000000000014431453572150400232440ustar00rootroot00000000000000T 24E _$'   More lengthy explanation of the view InternationalizationInternationalizationsMain endpoint for creating XNo response bodyUnspecified response bodyProject-Id-Version: Report-Msgid-Bugs-To: PO-Revision-Date: 2020-07-04 14:39+0200 Last-Translator: Language-Team: Language: de MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Plural-Forms: nplurals=2; plural=(n != 1); X-Generator: Poedit 2.3 Eine laengere Erklaerung des ViewsInternätiönalisierungInternätiönalisierungenHauptendpunkt fuer die Erstellung von XKein InhaltUnspezifizierte Antwortdrf-spectacular-0.27.0/tests/locale/de/LC_MESSAGES/django.po000066400000000000000000000020221453572150400232410ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # FIRST AUTHOR , YEAR. # msgid "" msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2020-07-11 13:28+0200\n" "PO-Revision-Date: 2020-07-04 14:39+0200\n" "Last-Translator: \n" "Language-Team: \n" "Language: de\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" "X-Generator: Poedit 2.3\n" msgid "" "\n" " More lengthy explanation of the view\n" " " msgstr "" "\n" " Eine laengere Erklaerung des Views" msgid "Main endpoint for creating X" msgstr "Hauptendpunkt fuer die Erstellung von X" msgid "No response body" msgstr "Kein Inhalt" msgid "Unspecified response body" msgstr "Unspezifizierte Antwort" msgid "Internationalization" msgstr "Internätiönalisierung" msgid "Internationalizations" msgstr "Internätiönalisierungen"drf-spectacular-0.27.0/tests/models.py000066400000000000000000000003561453572150400176700ustar00rootroot00000000000000from django.db import models from rest_framework import serializers class SimpleModel(models.Model): pass class SimpleSerializer(serializers.ModelSerializer): class Meta: model = SimpleModel fields = '__all__' drf-spectacular-0.27.0/tests/settings.py000066400000000000000000000000001453572150400202270ustar00rootroot00000000000000drf-spectacular-0.27.0/tests/test_basic.py000066400000000000000000000052771453572150400205340ustar00rootroot00000000000000import uuid from typing import Optional from unittest import mock from django.db import models from rest_framework import serializers, viewsets from rest_framework.authentication import TokenAuthentication from rest_framework.decorators import action from rest_framework.permissions import IsAuthenticatedOrReadOnly from rest_framework.response import Response from tests import assert_schema, generate_schema class Album(models.Model): id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) title = models.CharField(max_length=100) genre = models.CharField( choices=(('POP', 'Pop'), ('ROCK', 'Rock')), max_length=10 ) year = models.IntegerField() released = models.BooleanField() class Song(models.Model): id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) album = models.ForeignKey(Album, on_delete=models.CASCADE) title = models.CharField(max_length=100) length = models.IntegerField() class SongSerializer(serializers.ModelSerializer): top10 = serializers.SerializerMethodField() class Meta: fields = ['id', 'title', 'length', 'top10'] model = Song def get_top10(self) -> Optional[bool]: return True # pragma: no cover class AlbumSerializer(serializers.ModelSerializer): songs = SongSerializer(many=True, read_only=True) single = SongSerializer(read_only=True) class Meta: fields = '__all__' model = Album class LikeSerializer(serializers.Serializer): def save(self, *args, **kwargs): pass # pragma: no cover class AlbumModelViewset(viewsets.ModelViewSet): authentication_classes = [TokenAuthentication] permission_classes = [IsAuthenticatedOrReadOnly] serializer_class = AlbumSerializer queryset = Album.objects.none() @action(detail=True, methods=['POST'], serializer_class=LikeSerializer) def like(self, request): return Response(self.get_serializer().data) # pragma: no cover def create(self, request, *args, **kwargs): """ Special documentation about creating albums There is even more info here """ return super().create(request, *args, **kwargs) # pragma: no cover def test_basic(no_warnings, django_transforms): assert_schema( generate_schema('albums', AlbumModelViewset), 'tests/test_basic.yml', transforms=django_transforms, ) @mock.patch('drf_spectacular.settings.spectacular_settings.OAS_VERSION', '3.1.0') def test_basic_oas_3_1(no_warnings, django_transforms): assert_schema( generate_schema('albums', AlbumModelViewset), 'tests/test_basic_oas_3_1.yml', transforms=django_transforms, ) drf-spectacular-0.27.0/tests/test_basic.yml000066400000000000000000000140411453572150400206720ustar00rootroot00000000000000openapi: 3.0.3 info: title: '' version: 0.0.0 paths: /albums/: get: operationId: albums_list tags: - albums security: - tokenAuth: [] - {} responses: '200': content: application/json: schema: type: array items: $ref: '#/components/schemas/Album' description: '' post: operationId: albums_create description: |- Special documentation about creating albums There is even more info here tags: - albums requestBody: content: application/json: schema: $ref: '#/components/schemas/Album' application/x-www-form-urlencoded: schema: $ref: '#/components/schemas/Album' multipart/form-data: schema: $ref: '#/components/schemas/Album' required: true security: - tokenAuth: [] responses: '201': content: application/json: schema: $ref: '#/components/schemas/Album' description: '' /albums/{id}/: get: operationId: albums_retrieve parameters: - in: path name: id schema: type: string format: uuid description: A UUID string identifying this album. required: true tags: - albums security: - tokenAuth: [] - {} responses: '200': content: application/json: schema: $ref: '#/components/schemas/Album' description: '' put: operationId: albums_update parameters: - in: path name: id schema: type: string format: uuid description: A UUID string identifying this album. required: true tags: - albums requestBody: content: application/json: schema: $ref: '#/components/schemas/Album' application/x-www-form-urlencoded: schema: $ref: '#/components/schemas/Album' multipart/form-data: schema: $ref: '#/components/schemas/Album' required: true security: - tokenAuth: [] responses: '200': content: application/json: schema: $ref: '#/components/schemas/Album' description: '' patch: operationId: albums_partial_update parameters: - in: path name: id schema: type: string format: uuid description: A UUID string identifying this album. required: true tags: - albums requestBody: content: application/json: schema: $ref: '#/components/schemas/PatchedAlbum' application/x-www-form-urlencoded: schema: $ref: '#/components/schemas/PatchedAlbum' multipart/form-data: schema: $ref: '#/components/schemas/PatchedAlbum' security: - tokenAuth: [] responses: '200': content: application/json: schema: $ref: '#/components/schemas/Album' description: '' delete: operationId: albums_destroy parameters: - in: path name: id schema: type: string format: uuid description: A UUID string identifying this album. required: true tags: - albums security: - tokenAuth: [] responses: '204': description: No response body /albums/{id}/like/: post: operationId: albums_like_create parameters: - in: path name: id schema: type: string format: uuid description: A UUID string identifying this album. required: true tags: - albums security: - tokenAuth: [] responses: '200': description: No response body components: schemas: Album: type: object properties: id: type: string format: uuid readOnly: true songs: type: array items: $ref: '#/components/schemas/Song' readOnly: true single: allOf: - $ref: '#/components/schemas/Song' readOnly: true title: type: string maxLength: 100 genre: $ref: '#/components/schemas/GenreEnum' year: type: integer released: type: boolean required: - genre - id - released - single - songs - title - year GenreEnum: enum: - POP - ROCK type: string description: |- * `POP` - Pop * `ROCK` - Rock PatchedAlbum: type: object properties: id: type: string format: uuid readOnly: true songs: type: array items: $ref: '#/components/schemas/Song' readOnly: true single: allOf: - $ref: '#/components/schemas/Song' readOnly: true title: type: string maxLength: 100 genre: $ref: '#/components/schemas/GenreEnum' year: type: integer released: type: boolean Song: type: object properties: id: type: string format: uuid readOnly: true title: type: string maxLength: 100 length: type: integer top10: type: boolean nullable: true readOnly: true required: - id - length - title - top10 securitySchemes: tokenAuth: type: apiKey in: header name: Authorization description: Token-based authentication with required prefix "Token" drf-spectacular-0.27.0/tests/test_basic_oas_3_1.yml000066400000000000000000000140471453572150400222040ustar00rootroot00000000000000openapi: 3.1.0 info: title: '' version: 0.0.0 paths: /albums/: get: operationId: albums_list tags: - albums security: - tokenAuth: [] - {} responses: '200': content: application/json: schema: type: array items: $ref: '#/components/schemas/Album' description: '' post: operationId: albums_create description: |- Special documentation about creating albums There is even more info here tags: - albums requestBody: content: application/json: schema: $ref: '#/components/schemas/Album' application/x-www-form-urlencoded: schema: $ref: '#/components/schemas/Album' multipart/form-data: schema: $ref: '#/components/schemas/Album' required: true security: - tokenAuth: [] responses: '201': content: application/json: schema: $ref: '#/components/schemas/Album' description: '' /albums/{id}/: get: operationId: albums_retrieve parameters: - in: path name: id schema: type: string format: uuid description: A UUID string identifying this album. required: true tags: - albums security: - tokenAuth: [] - {} responses: '200': content: application/json: schema: $ref: '#/components/schemas/Album' description: '' put: operationId: albums_update parameters: - in: path name: id schema: type: string format: uuid description: A UUID string identifying this album. required: true tags: - albums requestBody: content: application/json: schema: $ref: '#/components/schemas/Album' application/x-www-form-urlencoded: schema: $ref: '#/components/schemas/Album' multipart/form-data: schema: $ref: '#/components/schemas/Album' required: true security: - tokenAuth: [] responses: '200': content: application/json: schema: $ref: '#/components/schemas/Album' description: '' patch: operationId: albums_partial_update parameters: - in: path name: id schema: type: string format: uuid description: A UUID string identifying this album. required: true tags: - albums requestBody: content: application/json: schema: $ref: '#/components/schemas/PatchedAlbum' application/x-www-form-urlencoded: schema: $ref: '#/components/schemas/PatchedAlbum' multipart/form-data: schema: $ref: '#/components/schemas/PatchedAlbum' security: - tokenAuth: [] responses: '200': content: application/json: schema: $ref: '#/components/schemas/Album' description: '' delete: operationId: albums_destroy parameters: - in: path name: id schema: type: string format: uuid description: A UUID string identifying this album. required: true tags: - albums security: - tokenAuth: [] responses: '204': description: No response body /albums/{id}/like/: post: operationId: albums_like_create parameters: - in: path name: id schema: type: string format: uuid description: A UUID string identifying this album. required: true tags: - albums security: - tokenAuth: [] responses: '200': description: No response body components: schemas: Album: type: object properties: id: type: string format: uuid readOnly: true songs: type: array items: $ref: '#/components/schemas/Song' readOnly: true single: allOf: - $ref: '#/components/schemas/Song' readOnly: true title: type: string maxLength: 100 genre: $ref: '#/components/schemas/GenreEnum' year: type: integer released: type: boolean required: - genre - id - released - single - songs - title - year GenreEnum: enum: - POP - ROCK type: string description: |- * `POP` - Pop * `ROCK` - Rock PatchedAlbum: type: object properties: id: type: string format: uuid readOnly: true songs: type: array items: $ref: '#/components/schemas/Song' readOnly: true single: allOf: - $ref: '#/components/schemas/Song' readOnly: true title: type: string maxLength: 100 genre: $ref: '#/components/schemas/GenreEnum' year: type: integer released: type: boolean Song: type: object properties: id: type: string format: uuid readOnly: true title: type: string maxLength: 100 length: type: integer top10: type: - boolean - 'null' readOnly: true required: - id - length - title - top10 securitySchemes: tokenAuth: type: apiKey in: header name: Authorization description: Token-based authentication with required prefix "Token" drf-spectacular-0.27.0/tests/test_callbacks.py000066400000000000000000000065371453572150400213720ustar00rootroot00000000000000from unittest import mock from rest_framework import serializers, viewsets from rest_framework.decorators import action from drf_spectacular.utils import OpenApiCallback, OpenApiResponse, extend_schema, inline_serializer from tests import assert_schema, generate_schema from tests.models import SimpleModel, SimpleSerializer class EventSerializer(serializers.Serializer): id = serializers.CharField(read_only=True) change = serializers.CharField() external_id = serializers.CharField(write_only=True) class XViewset(viewsets.ReadOnlyModelViewSet): queryset = SimpleModel.objects.none() serializer_class = SimpleSerializer @extend_schema( request=inline_serializer( name='SubscribeSerializer', fields={'callbackUrl': serializers.URLField()}, ), responses=None, callbacks=[ OpenApiCallback( name='SubscriptionEvent', path='{$request.body#/callbackUrl}', decorator=extend_schema( summary="some summary", description='pushes events to callbackUrl as "application/x-www-form-urlencoded"', request={ 'application/x-www-form-urlencoded': EventSerializer, }, responses={ 200: OpenApiResponse(description='event was successfully received'), '4XX': OpenApiResponse(description='event will be retried shortly'), }, ), ), ] ) @action(detail=False, methods=['POST']) def subscription(self): pass # pragma: no cover @extend_schema( request=inline_serializer( name='HealthSerializer', fields={'callbackUrl': serializers.URLField()}, ), responses=None, callbacks=[ OpenApiCallback( name='HealthEvent', path='{$request.body#/callbackUrl}', decorator={ 'post': extend_schema( request=EventSerializer, responses=OpenApiResponse(description='status new ack'), ), 'delete': extend_schema( deprecated=True, responses={200: OpenApiResponse(description='status expiration')}, ), 'put': extend_schema( request=EventSerializer, responses=EventSerializer, ), # raw schema 'patch': { 'requestBody': {'content': {'application/yaml': {'schema': {'type': 'integer'}}}}, 'responses': {'200': {'description': 'Raw schema'}} }, }, ), ] ) @action(detail=False, methods=['POST']) def health(self): pass # pragma: no cover def test_callbacks(no_warnings): assert_schema( generate_schema('/x', XViewset), 'tests/test_callbacks.yml' ) @mock.patch('drf_spectacular.settings.spectacular_settings.COMPONENT_SPLIT_REQUEST', True) def test_callbacks_split_request(no_warnings): assert_schema( generate_schema('/x', XViewset), 'tests/test_callbacks_split_request.yml' ) drf-spectacular-0.27.0/tests/test_callbacks.yml000066400000000000000000000115401453572150400215310ustar00rootroot00000000000000openapi: 3.0.3 info: title: '' version: 0.0.0 paths: /x/: get: operationId: x_list tags: - x security: - cookieAuth: [] - basicAuth: [] - {} responses: '200': content: application/json: schema: type: array items: $ref: '#/components/schemas/Simple' description: '' /x/{id}/: get: operationId: x_retrieve parameters: - in: path name: id schema: type: integer description: A unique integer value identifying this simple model. required: true tags: - x security: - cookieAuth: [] - basicAuth: [] - {} responses: '200': content: application/json: schema: $ref: '#/components/schemas/Simple' description: '' /x/health/: post: operationId: x_health_create tags: - x requestBody: content: application/json: schema: $ref: '#/components/schemas/Health' application/x-www-form-urlencoded: schema: $ref: '#/components/schemas/Health' multipart/form-data: schema: $ref: '#/components/schemas/Health' required: true security: - cookieAuth: [] - basicAuth: [] - {} responses: '200': description: No response body callbacks: HealthEvent: '{$request.body#/callbackUrl}': post: requestBody: content: application/json: schema: $ref: '#/components/schemas/Event' required: true responses: '200': description: status new ack delete: deprecated: true responses: '200': description: status expiration put: requestBody: content: application/json: schema: $ref: '#/components/schemas/Event' required: true responses: '200': content: application/json: schema: $ref: '#/components/schemas/Event' description: '' patch: requestBody: content: application/yaml: schema: type: integer responses: '200': description: Raw schema /x/subscription/: post: operationId: x_subscription_create tags: - x requestBody: content: application/json: schema: $ref: '#/components/schemas/Subscribe' application/x-www-form-urlencoded: schema: $ref: '#/components/schemas/Subscribe' multipart/form-data: schema: $ref: '#/components/schemas/Subscribe' required: true security: - cookieAuth: [] - basicAuth: [] - {} responses: '200': description: No response body callbacks: SubscriptionEvent: '{$request.body#/callbackUrl}': post: description: pushes events to callbackUrl as "application/x-www-form-urlencoded" summary: some summary requestBody: content: application/x-www-form-urlencoded: schema: $ref: '#/components/schemas/Event' required: true responses: '200': description: event was successfully received 4XX: description: event will be retried shortly components: schemas: Event: type: object properties: id: type: string readOnly: true change: type: string external_id: type: string writeOnly: true required: - change - external_id - id Health: type: object properties: callbackUrl: type: string format: uri required: - callbackUrl Simple: type: object properties: id: type: integer readOnly: true required: - id Subscribe: type: object properties: callbackUrl: type: string format: uri required: - callbackUrl securitySchemes: basicAuth: type: http scheme: basic cookieAuth: type: apiKey in: cookie name: sessionid drf-spectacular-0.27.0/tests/test_callbacks_split_request.yml000066400000000000000000000121701453572150400245140ustar00rootroot00000000000000openapi: 3.0.3 info: title: '' version: 0.0.0 paths: /x/: get: operationId: x_list tags: - x security: - cookieAuth: [] - basicAuth: [] - {} responses: '200': content: application/json: schema: type: array items: $ref: '#/components/schemas/Simple' description: '' /x/{id}/: get: operationId: x_retrieve parameters: - in: path name: id schema: type: integer description: A unique integer value identifying this simple model. required: true tags: - x security: - cookieAuth: [] - basicAuth: [] - {} responses: '200': content: application/json: schema: $ref: '#/components/schemas/Simple' description: '' /x/health/: post: operationId: x_health_create tags: - x requestBody: content: application/json: schema: $ref: '#/components/schemas/HealthRequest' application/x-www-form-urlencoded: schema: $ref: '#/components/schemas/HealthRequest' multipart/form-data: schema: $ref: '#/components/schemas/HealthRequest' required: true security: - cookieAuth: [] - basicAuth: [] - {} responses: '200': description: No response body callbacks: HealthEvent: '{$request.body#/callbackUrl}': post: requestBody: content: application/json: schema: $ref: '#/components/schemas/Event' required: true responses: '200': description: status new ack delete: deprecated: true responses: '200': description: status expiration put: requestBody: content: application/json: schema: $ref: '#/components/schemas/Event' required: true responses: '200': content: application/json: schema: $ref: '#/components/schemas/EventRequest' description: '' patch: requestBody: content: application/yaml: schema: type: integer responses: '200': description: Raw schema /x/subscription/: post: operationId: x_subscription_create tags: - x requestBody: content: application/json: schema: $ref: '#/components/schemas/SubscribeRequest' application/x-www-form-urlencoded: schema: $ref: '#/components/schemas/SubscribeRequest' multipart/form-data: schema: $ref: '#/components/schemas/SubscribeRequest' required: true security: - cookieAuth: [] - basicAuth: [] - {} responses: '200': description: No response body callbacks: SubscriptionEvent: '{$request.body#/callbackUrl}': post: description: pushes events to callbackUrl as "application/x-www-form-urlencoded" summary: some summary requestBody: content: application/x-www-form-urlencoded: schema: $ref: '#/components/schemas/Event' required: true responses: '200': description: event was successfully received 4XX: description: event will be retried shortly components: schemas: Event: type: object properties: id: type: string readOnly: true change: type: string required: - change - id EventRequest: type: object properties: change: type: string minLength: 1 external_id: type: string writeOnly: true minLength: 1 required: - change - external_id HealthRequest: type: object properties: callbackUrl: type: string format: uri minLength: 1 required: - callbackUrl Simple: type: object properties: id: type: integer readOnly: true required: - id SubscribeRequest: type: object properties: callbackUrl: type: string format: uri minLength: 1 required: - callbackUrl securitySchemes: basicAuth: type: http scheme: basic cookieAuth: type: apiKey in: cookie name: sessionid drf-spectacular-0.27.0/tests/test_command.py000066400000000000000000000052761453572150400210700ustar00rootroot00000000000000import tempfile from unittest import mock import pytest import yaml from django.core import management from django.core.management import CommandError from django.core.management.base import SystemCheckError from django.urls import path from rest_framework.decorators import api_view def test_command_plain(capsys, clear_generator_settings): management.call_command('spectacular', validate=True, fail_on_warn=True) schema_stdout = capsys.readouterr().out schema = yaml.load(schema_stdout, Loader=yaml.SafeLoader) assert 'openapi' in schema assert 'info' in schema assert 'paths' in schema def test_command_parameterized(clear_generator_settings): with tempfile.NamedTemporaryFile() as fh: management.call_command( 'spectacular', '--validate', '--fail-on-warn', '--lang=de', '--generator-class=drf_spectacular.generators.SchemaGenerator', '--file=' + fh.name, ) schema = yaml.load(fh.read(), Loader=yaml.SafeLoader) assert 'openapi' in schema assert 'info' in schema assert 'paths' in schema def test_command_fail(capsys, clear_generator_settings): with pytest.raises(CommandError): management.call_command( 'spectacular', '--fail-on-warn', '--urlconf=tests.test_command', ) stderr = capsys.readouterr().err assert '/tests/test_command.py: Error [func]: unable to guess serializer' in stderr assert 'Schema generation summary:' in stderr def test_command_color(capsys, clear_generator_settings): management.call_command( 'spectacular', '--color', '--urlconf=tests.test_command', ) stderr = capsys.readouterr().err assert '\033[0;31mError [func]:' in stderr CUSTOM = {'DESCRIPTION': 'custom setting'} def test_command_custom_settings(capsys, clear_generator_settings): management.call_command('spectacular', '--custom-settings=tests.test_command.CUSTOM') assert 'description: custom setting' in capsys.readouterr().out def test_command_check(capsys): management.call_command('check', '--deploy') stderr = capsys.readouterr().err assert not stderr @api_view(['GET']) def func(request): pass # pragma: no cover urlpatterns = [path('func', func)] @mock.patch('tests.urls.urlpatterns', [path('api/endpoint/', func)]) def test_command_check_fail(capsys): with pytest.raises(SystemCheckError): management.call_command('check', '--fail-level', 'WARNING', '--deploy') management.call_command('check', '--deploy') stdout = capsys.readouterr().err assert 'System check identified some issues' in stdout assert 'drf_spectacular.W002' in stdout drf-spectacular-0.27.0/tests/test_custom_settings.py000066400000000000000000000050471453572150400227000ustar00rootroot00000000000000import pytest import yaml from django.urls import path from rest_framework import serializers from rest_framework.decorators import api_view from rest_framework.response import Response from rest_framework.test import APIClient from drf_spectacular.utils import extend_schema from drf_spectacular.views import SpectacularAPIView def custom_hook(endpoints, **kwargs): return [ (path.rstrip('/'), path_regex.rstrip('/'), method, callback) for path, path_regex, method, callback in endpoints ] class XSerializer(serializers.Serializer): field = serializers.CharField() @extend_schema(request=XSerializer, responses=XSerializer) @api_view(http_method_names=['POST']) def pi(request): return Response(3.1415) # pragma: no cover urlpatterns = [ path('api/pi/', pi), path('api/schema/', SpectacularAPIView.as_view(), name='schema'), path('api/schema-custom/', SpectacularAPIView.as_view( custom_settings={ 'TITLE': 'Custom settings with this SpectacularAPIView', 'SCHEMA_PATH_PREFIX': '', 'COMPONENT_SPLIT_REQUEST': True, 'PREPROCESSING_HOOKS': ['tests.test_custom_settings.custom_hook'] } ), name='schema-custom'), path('api/schema-invalid/', SpectacularAPIView.as_view( custom_settings={'INVALID': 'INVALID'} ), name='schema-invalid'), path('api/schema-invalid2/', SpectacularAPIView.as_view( custom_settings={'SERVE_PUBLIC': 'INVALID'} ), name='schema-invalid2'), ] @pytest.mark.urls(__name__) def test_custom_settings(no_warnings): response = APIClient().get('/api/schema-custom/') schema = yaml.load(response.content, Loader=yaml.SafeLoader) assert schema['info']['title'] assert '/api/pi' in schema['paths'] # hook executed assert ['api'] == schema['paths']['/api/pi']['post']['tags'] # SCHEMA_PATH_PREFIX assert 'XRequest' in schema['components']['schemas'] # COMPONENT_SPLIT_REQUEST response = APIClient().get('/api/schema/') schema = yaml.load(response.content, Loader=yaml.SafeLoader) assert not schema['info']['title'] assert '/api/pi/' in schema['paths'] # hook not executed assert ['pi'] == schema['paths']['/api/pi/']['post']['tags'] # SCHEMA_PATH_PREFIX assert 'XRequest' not in schema['components']['schemas'] # COMPONENT_SPLIT_REQUEST @pytest.mark.urls(__name__) def test_invalid_custom_settings(): with pytest.raises(AttributeError): APIClient().get('/api/schema-invalid/') with pytest.raises(AttributeError): APIClient().get('/api/schema-invalid2/') drf-spectacular-0.27.0/tests/test_examples.py000066400000000000000000000253101453572150400212570ustar00rootroot00000000000000import pytest from rest_framework import __version__ as DRF_VERSION # type: ignore[attr-defined] from rest_framework import generics, pagination, serializers, status, viewsets from rest_framework.decorators import action from rest_framework.response import Response from drf_spectacular.types import OpenApiTypes from drf_spectacular.utils import ( OpenApiExample, OpenApiParameter, OpenApiResponse, extend_schema, extend_schema_serializer, ) from tests import assert_schema, generate_schema from tests.models import SimpleModel, SimpleSerializer @extend_schema_serializer( examples=[ OpenApiExample( 'Serializer A Example RO', value={"field": 1}, response_only=True, ), OpenApiExample( 'Serializer A Example WO', value={"field": 2}, request_only=True, ), OpenApiExample( 'Serializer A Example RW', summary='Serializer A Example RW custom summary', value={'field': 3} ), OpenApiExample( 'Serializer A Example RW External', external_value='https://example.com/example_a.txt', media_type='application/x-www-form-urlencoded' ) ] ) class ASerializer(serializers.Serializer): field = serializers.IntegerField() class BSerializer(serializers.Serializer): field = serializers.IntegerField() @extend_schema_serializer( examples=[ OpenApiExample( 'Serializer C Example RO', value={"field": 111}, response_only=True, ), OpenApiExample( 'Serializer C Example WO', value={"field": 222}, request_only=True, ), ] ) class CSerializer(serializers.Serializer): field = serializers.IntegerField() @extend_schema( responses=BSerializer, examples=[OpenApiExample("Example ID 1", value=1, parameter_only=('id', 'path'))] ) class ExampleTestWithExtendedViewSet(viewsets.GenericViewSet): serializer_class = ASerializer queryset = SimpleModel.objects.none() @extend_schema( request=ASerializer, responses={ 201: BSerializer, 400: OpenApiTypes.OBJECT, 403: OpenApiTypes.OBJECT, }, examples=[ OpenApiExample( 'Create Example RO', value={'field': 11}, response_only=True, ), OpenApiExample( 'Create Example WO', value={'field': 22}, request_only=True, ), OpenApiExample( 'Create Example RW', value={'field': 33}, ), OpenApiExample( 'Create Error 403 Integer Example', value={'field': 'error (int)'}, response_only=True, status_codes=[status.HTTP_403_FORBIDDEN], ), OpenApiExample( 'Create Error 403 String Example', value={'field': 'error (str)'}, response_only=True, status_codes=['403'] ), ], ) def create(self, request, *args, **kwargs): super().create(request, *args, **kwargs) # pragma: no cover @extend_schema( parameters=[ OpenApiParameter( name="artist", description="Filter by artist", required=False, type=str, examples=[ OpenApiExample( "Artist Query Example 1", value="prince", description="description for artist query example 1" ), OpenApiExample( "Artist Query Example 2", value="miles davis", description="description for artist query example 2" ) ] ), ], responses=CSerializer, ) def list(self, request): return Response() # pragma: no cover @extend_schema( examples=[ OpenApiExample( "Example ID 2", value=2, parameter_only=('id', OpenApiParameter.PATH) ) ] ) def retrieve(self, request): return Response() # pragma: no cover @action(detail=False, methods=['GET']) def raw_action(self, request): return Response() # pragma: no cover @extend_schema(responses=BSerializer) @action(detail=False, methods=['POST']) def override_extend_schema_action(self, request): return Response() # pragma: no cover def test_examples(no_warnings): assert_schema( generate_schema('schema', ExampleTestWithExtendedViewSet), 'tests/test_examples.yml', ) @pytest.mark.skipif(DRF_VERSION < '3.12', reason='DRF pagination schema broken') def test_example_pagination(no_warnings): class PaginatedExamplesViewSet(ExampleTestWithExtendedViewSet): pagination_class = pagination.LimitOffsetPagination schema = generate_schema('e', PaginatedExamplesViewSet) operation = schema['paths']['/e/']['get'] assert operation['responses']['200']['content']['application/json']['examples'] == { 'SerializerCExampleRO': { 'value': { 'count': 123, 'next': 'http://api.example.org/accounts/?offset=400&limit=100', 'previous': 'http://api.example.org/accounts/?offset=200&limit=100', 'results': [{'field': 111}], }, 'summary': 'Serializer C Example RO' } } def test_example_request_response_listed_examples(no_warnings): @extend_schema( request=ASerializer(many=True), responses=ASerializer(many=True), examples=[ OpenApiExample('Ex', {'id': '1234'}) ] ) class XView(generics.CreateAPIView): pass schema = generate_schema('e', view=XView) operation = schema['paths']['/e']['post'] assert operation['requestBody']['content']['application/json'] == { 'schema': {'type': 'array', 'items': {'$ref': '#/components/schemas/A'}}, 'examples': {'Ex': {'value': [{'id': '1234'}]}} } assert operation['responses']['201']['content']['application/json'] == { 'schema': {'type': 'array', 'items': {'$ref': '#/components/schemas/A'}}, 'examples': {'Ex': {'value': [{'id': '1234'}]}} } def test_examples_list_detection_on_non_200_decoration(no_warnings): class ExceptionSerializer(serializers.Serializer): api_status_code = serializers.CharField() extra = serializers.DictField(required=False) @extend_schema( responses={ 200: SimpleSerializer, 400: OpenApiResponse( response=ExceptionSerializer, examples=[ OpenApiExample( "Date parse error", value={"api_status_code": "DATE_PARSE_ERROR", "extra": {"details": "foobar"}}, status_codes=['400'] ) ], ), }, ) class XListView(generics.ListAPIView): model = SimpleModel serializer_class = SimpleSerializer pagination_class = pagination.LimitOffsetPagination schema = generate_schema('/x/', view=XListView) # regular response listed/paginated assert schema['paths']['/x/']['get']['responses']['200']['content']['application/json'] == { 'schema': {'$ref': '#/components/schemas/PaginatedSimpleList'} } # non-200 error response example NOT listed/paginated assert schema['paths']['/x/']['get']['responses']['400']['content']['application/json'] == { 'examples': { 'DateParseError': { 'summary': 'Date parse error', 'value': {'api_status_code': 'DATE_PARSE_ERROR', 'extra': {'details': 'foobar'}} } }, 'schema': {'$ref': '#/components/schemas/Exception'}, } def test_inherited_status_code_from_response_container(no_warnings): @extend_schema( responses={ 400: OpenApiResponse( response=SimpleSerializer, examples=[ # prior to the fix this required the argument status_code=[400] # as the code was not passed down and the filtering sorted it out. OpenApiExample("an example", value={"id": 3}) ], ), }, ) class XListView(generics.ListAPIView): model = SimpleModel serializer_class = SimpleSerializer schema = generate_schema('/x/', view=XListView) assert schema['paths']['/x/']['get']['responses']['400']['content']['application/json'] == { 'schema': {'$ref': '#/components/schemas/Simple'}, 'examples': {'AnExample': {'value': {'id': 3}, 'summary': 'an example'}} } def test_examples_with_falsy_values(no_warnings): @extend_schema( responses=OpenApiResponse( description='something', response=OpenApiTypes.JSON_PTR, examples=[ OpenApiExample('one', value=1), OpenApiExample('empty-list', value=[]), OpenApiExample('false', value=False), OpenApiExample('zero', value=0), OpenApiExample('empty'), ], ), ) class XListView(generics.ListAPIView): model = SimpleModel serializer_class = SimpleSerializer schema = generate_schema('/x/', view=XListView) assert schema['paths']['/x/']['get']['responses']['200']['content']['application/json']['examples'] == { 'One': {'summary': 'one', 'value': 1}, 'Empty-list': {'summary': 'empty-list', 'value': []}, 'False': {'summary': 'false', 'value': False}, 'Zero': {'summary': 'zero', 'value': 0}, 'Empty': {'summary': 'empty'}, } @pytest.mark.skipif(DRF_VERSION < '3.12', reason='DRF pagination schema broken') def test_plain_pagination_example(no_warnings): class PlainPagination(pagination.LimitOffsetPagination): """ return a (unpaginated) basic list, while other might happen in the headers """ def get_paginated_response_schema(self, schema): return schema class PaginatedExamplesViewSet(ExampleTestWithExtendedViewSet): pagination_class = PlainPagination schema = generate_schema('e', PaginatedExamplesViewSet) operation = schema['paths']['/e/']['get'] assert operation['responses']['200']['content']['application/json']['examples'] == { 'SerializerCExampleRO': { 'value': [{'field': 111}], 'summary': 'Serializer C Example RO' } } drf-spectacular-0.27.0/tests/test_examples.yml000066400000000000000000000133431453572150400214330ustar00rootroot00000000000000openapi: 3.0.3 info: title: '' version: 0.0.0 paths: /schema/: get: operationId: schema_list parameters: - in: query name: artist schema: type: string description: Filter by artist examples: ArtistQueryExample1: value: prince summary: Artist Query Example 1 description: description for artist query example 1 ArtistQueryExample2: value: miles davis summary: Artist Query Example 2 description: description for artist query example 2 tags: - schema security: - cookieAuth: [] - basicAuth: [] - {} responses: '200': content: application/json: schema: type: array items: $ref: '#/components/schemas/C' examples: SerializerCExampleRO: value: - field: 111 summary: Serializer C Example RO description: '' post: operationId: schema_create tags: - schema requestBody: content: application/json: schema: $ref: '#/components/schemas/A' examples: CreateExampleWO: value: field: 22 summary: Create Example WO CreateExampleRW: value: field: 33 summary: Create Example RW application/x-www-form-urlencoded: schema: $ref: '#/components/schemas/A' multipart/form-data: schema: $ref: '#/components/schemas/A' required: true security: - cookieAuth: [] - basicAuth: [] - {} responses: '201': content: application/json: schema: $ref: '#/components/schemas/B' examples: CreateExampleRO: value: field: 11 summary: Create Example RO CreateExampleRW: value: field: 33 summary: Create Example RW description: '' '400': content: application/json: schema: type: object additionalProperties: {} description: '' '403': content: application/json: schema: type: object additionalProperties: {} examples: CreateError403IntegerExample: value: field: error (int) summary: Create Error 403 Integer Example CreateError403StringExample: value: field: error (str) summary: Create Error 403 String Example description: '' /schema/{id}/: get: operationId: schema_retrieve parameters: - in: path name: id schema: type: integer description: A unique integer value identifying this simple model. required: true examples: ExampleID1: value: 1 summary: Example ID 1 ExampleID2: value: 2 summary: Example ID 2 tags: - schema security: - cookieAuth: [] - basicAuth: [] - {} responses: '200': content: application/json: schema: $ref: '#/components/schemas/B' description: '' /schema/override_extend_schema_action/: post: operationId: schema_override_extend_schema_action_create tags: - schema requestBody: content: application/json: schema: $ref: '#/components/schemas/A' examples: SerializerAExampleWO: value: field: 2 summary: Serializer A Example WO SerializerAExampleRW: value: field: 3 summary: Serializer A Example RW custom summary application/x-www-form-urlencoded: schema: $ref: '#/components/schemas/A' examples: SerializerAExampleRWExternal: externalValue: https://example.com/example_a.txt summary: Serializer A Example RW External multipart/form-data: schema: $ref: '#/components/schemas/A' required: true security: - cookieAuth: [] - basicAuth: [] - {} responses: '200': content: application/json: schema: $ref: '#/components/schemas/B' description: '' /schema/raw_action/: get: operationId: schema_raw_action_retrieve tags: - schema security: - cookieAuth: [] - basicAuth: [] - {} responses: '200': content: application/json: schema: $ref: '#/components/schemas/B' description: '' components: schemas: A: type: object properties: field: type: integer required: - field B: type: object properties: field: type: integer required: - field C: type: object properties: field: type: integer required: - field securitySchemes: basicAuth: type: http scheme: basic cookieAuth: type: apiKey in: cookie name: sessionid drf-spectacular-0.27.0/tests/test_extend_schema.py000066400000000000000000000344731453572150400222620ustar00rootroot00000000000000from unittest import mock from django.utils.http import urlsafe_base64_encode from rest_framework import mixins, serializers, viewsets from rest_framework.decorators import action, api_view from rest_framework.response import Response from drf_spectacular.openapi import AutoSchema from drf_spectacular.types import OpenApiTypes from drf_spectacular.utils import ( OpenApiParameter, extend_schema, extend_schema_field, extend_schema_serializer, ) from tests import assert_schema, generate_schema, get_response_schema class AlphaSerializer(serializers.Serializer): field_a = serializers.CharField() field_b = serializers.IntegerField() class BetaSerializer(AlphaSerializer): field_c = serializers.JSONField() class DeltaSerializer(serializers.Serializer): field_a = serializers.CharField(required=False) field_b = serializers.IntegerField(required=False) @extend_schema_field(OpenApiTypes.BYTE) class CustomField(serializers.Field): def to_representation(self, value): return urlsafe_base64_encode(b'\xf0\xf1\xf2') # pragma: no cover @extend_schema_field(OpenApiTypes.BYTE) class CustomURLField(serializers.URLField): def to_representation(self, value): return urlsafe_base64_encode(b'\xf0\xf1\xf2') # pragma: no cover @extend_schema_serializer(component_name='GammaEpsilon') class GammaSerializer(serializers.Serializer): encoding = serializers.CharField() image_data = CustomField() custom_url_field = CustomURLField() class InlineSerializer(serializers.Serializer): inline_b = serializers.BooleanField() inline_i = serializers.IntegerField() class QuerySerializer(serializers.Serializer): stars = serializers.IntegerField(min_value=1, max_value=5, help_text='filter by rating stars') contains = serializers.CharField( min_length=3, max_length=10, help_text='filter by containing string', required=False ) order_by = serializers.MultipleChoiceField( choices=['a', 'b', 'c'], default=['a'], # type: ignore ) tag = serializers.CharField(required=False) class ErrorDetailSerializer(serializers.Serializer): field_i = serializers.SerializerMethodField() field_j = serializers.SerializerMethodField() field_k = serializers.SerializerMethodField() field_l = serializers.SerializerMethodField() field_m = serializers.SerializerMethodField() @extend_schema_field(OpenApiTypes.DATETIME) def get_field_i(self, object): return '2020-03-06 20:54:00.104248' # pragma: no cover @extend_schema_field(InlineSerializer(allow_null=True)) def get_field_j(self, object): return InlineSerializer({}).data # pragma: no cover @extend_schema_field(InlineSerializer(many=True)) def get_field_k(self, object): return InlineSerializer([], many=True).data # pragma: no cover @extend_schema_field(serializers.ChoiceField(choices=['a', 'b'])) def get_field_l(self, object): return object.some_choice # pragma: no cover @extend_schema_field({'type': 'array', 'items': {'type': 'integer'}}) def get_field_m(self, object): return [1, 2, 3] # pragma: no cover with mock.patch('rest_framework.settings.api_settings.DEFAULT_SCHEMA_CLASS', AutoSchema): class DoesItAllViewset(viewsets.GenericViewSet): serializer_class = AlphaSerializer @extend_schema( operation_id='customname_create', request=AlphaSerializer, responses={ 200: BetaSerializer(many=True), 201: GammaSerializer, 500: ErrorDetailSerializer, }, parameters=[ OpenApiParameter( 'expiration_date', OpenApiTypes.DATETIME, description='time the object will expire at' ), OpenApiParameter( name='test_mode', type=bool, location=OpenApiParameter.HEADER, enum=[True, False], default=False, description='creation will be in the sandbox', ), OpenApiParameter( name='X-Api-Version', type=str, location=OpenApiParameter.HEADER, response=True, ), OpenApiParameter( name='Location', type=OpenApiTypes.URI, location=OpenApiParameter.HEADER, description='URL of the created resource', response=[201], ), ], description='this weird endpoint needs some explaining', summary='short summary', deprecated=True, tags=['custom_tag'], ) def create(self, request, *args, **kwargs): return Response({}) # pragma: no cover @extend_schema(exclude=True) def list(self, request, *args, **kwargs): return Response([]) # pragma: no cover @extend_schema( parameters=[OpenApiParameter('id', OpenApiTypes.INT, OpenApiParameter.PATH)], request=None, responses={201: None}, ) @action(detail=True, methods=['POST']) def subscribe(self, request): return Response(status=201) # pragma: no cover @extend_schema( request=OpenApiTypes.OBJECT, responses={201: None}, parameters=[OpenApiParameter('ephemeral', OpenApiTypes.UUID, OpenApiParameter.PATH)] ) @action(detail=False, url_path='callback/(?P[^/.]+)', methods=['POST']) def callback(self, request, ephemeral, pk): return Response(status=201) # pragma: no cover @extend_schema(responses={204: None}) @action(detail=False, url_path='only-response-override', methods=['POST']) def only_response_override(self, request): return Response(status=201) # pragma: no cover @extend_schema(parameters=[ QuerySerializer, # exploded OpenApiParameter('nested', QuerySerializer) # nested ]) @action(detail=False, url_path='serializer-query', methods=['GET']) def serializer_query(self, request): return Response([]) # pragma: no cover # this is intended as a measure of last resort when nothing else works @extend_schema(operation={ "operationId": "manual_endpoint", "description": "fallback mechanism where can go all out", "tags": ["manual_tag"], "requestBody": { "content": { "application/json": { "schema": {"$ref": "#/components/schemas/Alpha"} }, } }, "deprecated": True, "responses": { "200": { "content": { "application/json": { "schema": {"$ref": "#/components/schemas/Gamma"} } }, "description": "" }, } }) @action(detail=False, methods=['POST']) def manual(self, request): return Response() # pragma: no cover @extend_schema(request=DeltaSerializer) @action(detail=False, methods=['POST']) def non_required_body(self, request): return Response([]) # pragma: no cover @extend_schema( request={ 'application/json': dict, 'application/pdf': bytes, 'text/html': OpenApiTypes.STR }, responses=None ) @action(detail=False, methods=['POST']) def custom_request_override(self, request): return Response([]) # pragma: no cover @extend_schema(responses={ (200, 'application/pdf'): bytes }) @action(detail=False, methods=['GET']) def document(self, request): return Response(b'deadbeef', status=200) # pragma: no cover def test_extend_schema(no_warnings): assert_schema( generate_schema('doesitall', DoesItAllViewset), 'tests/test_extend_schema.yml' ) def test_layered_extend_schema_on_view_and_method_with_meta(no_warnings): class XSerializer(serializers.Serializer): field = serializers.IntegerField() @extend_schema(tags=['view_tag2']) @extend_schema(tags=['view_tag'], description='view_desc', summary='view_sum') class XViewset(mixins.ListModelMixin, mixins.CreateModelMixin, viewsets.GenericViewSet): serializer_class = XSerializer @extend_schema(tags=['create_tag2']) @extend_schema(tags=['create_tag'], description='create_desc') def create(self, request, *args, **kwargs): super().create(request, *args, **kwargs) # pragma: no cover @extend_schema(tags=['extended_action_tag2']) @extend_schema(tags=['extended_action_tag'], description='extended_action_desc') @action(detail=False, methods=['GET']) def extended_action(self, request): return Response() # pragma: no cover @action(detail=False, methods=['GET']) def raw_action(self, request): return Response() # pragma: no cover schema = generate_schema('x', XViewset) create_op = schema['paths']['/x/']['post'] list_op = schema['paths']['/x/']['get'] raw_action_op = schema['paths']['/x/raw_action/']['get'] extended_action_op = schema['paths']['/x/extended_action/']['get'] assert create_op['tags'][0] == 'create_tag2' assert create_op['description'] == 'create_desc' assert create_op['summary'] == 'view_sum' assert list_op['tags'][0] == 'view_tag2' assert list_op['description'] == 'view_desc' assert list_op['summary'] == 'view_sum' assert raw_action_op['tags'][0] == 'view_tag2' assert raw_action_op['description'] == 'view_desc' assert raw_action_op['summary'] == 'view_sum' assert extended_action_op['tags'][0] == 'extended_action_tag2' assert extended_action_op['description'] == 'extended_action_desc' assert extended_action_op['summary'] == 'view_sum' def test_layered_extend_schema_on_view_and_method_with_serializer(no_warnings): class ASerializer(serializers.Serializer): field = serializers.IntegerField() class BSerializer(serializers.Serializer): field = serializers.IntegerField() class CSerializer(serializers.Serializer): field = serializers.IntegerField() @extend_schema(responses=BSerializer) class XViewset(mixins.ListModelMixin, mixins.CreateModelMixin, viewsets.GenericViewSet): serializer_class = ASerializer @extend_schema(responses=CSerializer) def create(self, request, *args, **kwargs): super().create(request, *args, **kwargs) # pragma: no cover @extend_schema(responses=CSerializer) @action(detail=False, methods=['GET']) def extended_action(self, request): return Response() # pragma: no cover @action(detail=False, methods=['GET']) def raw_action(self, request): return Response() # pragma: no cover schema = generate_schema('x', XViewset) create_op = get_response_schema(schema['paths']['/x/']['post']) list_op = get_response_schema(schema['paths']['/x/']['get']) raw_action_op = get_response_schema(schema['paths']['/x/raw_action/']['get']) extended_action_op = get_response_schema(schema['paths']['/x/extended_action/']['get']) assert create_op['$ref'].endswith('C') assert extended_action_op['$ref'].endswith('C') assert list_op['items']['$ref'].endswith('B') assert raw_action_op['$ref'].endswith('B') def test_extend_schema_field_with_serializer_as_override(no_warnings): class OverrideSerializer(serializers.Serializer): field = serializers.UUIDField() @extend_schema_field(field=OverrideSerializer) class CustomField(serializers.CharField): pass class XSerializer(serializers.Serializer): field = CustomField(read_only=True) @extend_schema(request=XSerializer, responses=XSerializer) @api_view(['POST']) def view_func(request, format=None): pass # pragma: no cover schema = generate_schema('x', view_function=view_func) assert schema['components']['schemas']['Override']['type'] == 'object' property_field = schema['components']['schemas']['X']['properties']['field'] assert 'Override' in property_field['allOf'][0]['$ref'] assert property_field['readOnly'] def test_extend_schema_field_custom_schema_with_without_breakout(no_warnings): field_schema = {'type': 'string', 'pattern': '^[0-9]*$', 'description': 'some explaining'} @extend_schema_field(field=field_schema, component_name='Breakout') class CustomBreakoutField(serializers.CharField): pass @extend_schema_field(field=field_schema) class CustomField(serializers.CharField): pass class XSerializer(serializers.Serializer): field = CustomField(read_only=True) field_breakout = CustomBreakoutField(read_only=True) @extend_schema(request=XSerializer, responses=XSerializer) @api_view(['POST']) def view_func(request, format=None): pass # pragma: no cover schema = generate_schema('x', view_function=view_func) assert schema['components']['schemas']['Breakout']['type'] == 'string' properties = schema['components']['schemas']['X']['properties'] assert properties['field']['description'] == 'some explaining' assert properties['field']['readOnly'] assert 'Breakout' in properties['field_breakout']['allOf'][0]['$ref'] assert properties['field_breakout']['readOnly'] def test_extend_schema_field_with_field_class(no_warnings) -> None: class XSerializer(serializers.Serializer): field = serializers.SerializerMethodField() @extend_schema_field(serializers.IntegerField()) def get_field(self, object): return 1 # pragma: no cover @extend_schema(responses=XSerializer) @api_view(['GET']) def view_func(request, format=None): pass # pragma: no cover schema = generate_schema('x', view_function=view_func) assert schema['components']['schemas']['X'] == { 'type': 'object', 'properties': {'field': {'type': 'integer', 'readOnly': True}}, 'required': ['field'] } drf-spectacular-0.27.0/tests/test_extend_schema.yml000066400000000000000000000236171453572150400224310ustar00rootroot00000000000000openapi: 3.0.3 info: title: '' version: 0.0.0 paths: /doesitall/: post: operationId: customname_create description: this weird endpoint needs some explaining summary: short summary parameters: - in: query name: expiration_date schema: type: string format: date-time description: time the object will expire at - in: header name: test_mode schema: type: boolean enum: - false - true default: false description: creation will be in the sandbox tags: - custom_tag requestBody: content: application/json: schema: $ref: '#/components/schemas/Alpha' application/x-www-form-urlencoded: schema: $ref: '#/components/schemas/Alpha' multipart/form-data: schema: $ref: '#/components/schemas/Alpha' required: true security: - cookieAuth: [] - basicAuth: [] - {} deprecated: true responses: '200': headers: X-Api-Version: schema: type: string content: application/json: schema: type: array items: $ref: '#/components/schemas/Beta' description: '' '201': headers: X-Api-Version: schema: type: string Location: schema: type: string format: uri description: URL of the created resource content: application/json: schema: $ref: '#/components/schemas/GammaEpsilon' description: '' '500': headers: X-Api-Version: schema: type: string content: application/json: schema: $ref: '#/components/schemas/ErrorDetail' description: '' /doesitall/{id}/subscribe/: post: operationId: doesitall_subscribe_create parameters: - in: path name: id schema: type: integer required: true tags: - doesitall security: - cookieAuth: [] - basicAuth: [] - {} responses: '201': description: No response body /doesitall/callback/{ephemeral}/: post: operationId: doesitall_callback_create parameters: - in: path name: ephemeral schema: type: string format: uuid required: true tags: - doesitall requestBody: content: application/json: schema: type: object additionalProperties: {} application/x-www-form-urlencoded: schema: type: object additionalProperties: {} multipart/form-data: schema: type: object additionalProperties: {} security: - cookieAuth: [] - basicAuth: [] - {} responses: '201': description: No response body /doesitall/custom_request_override/: post: operationId: doesitall_custom_request_override_create tags: - doesitall requestBody: content: application/json: schema: type: object additionalProperties: {} application/pdf: schema: type: string format: binary text/html: schema: type: string security: - cookieAuth: [] - basicAuth: [] - {} responses: '200': description: No response body /doesitall/document/: get: operationId: doesitall_document_retrieve tags: - doesitall security: - cookieAuth: [] - basicAuth: [] - {} responses: '200': content: application/pdf: schema: type: string format: binary description: '' /doesitall/manual/: post: operationId: manual_endpoint description: fallback mechanism where can go all out tags: - manual_tag requestBody: content: application/json: schema: $ref: '#/components/schemas/Alpha' deprecated: true responses: '200': content: application/json: schema: $ref: '#/components/schemas/Gamma' description: '' /doesitall/non_required_body/: post: operationId: doesitall_non_required_body_create tags: - doesitall requestBody: content: application/json: schema: $ref: '#/components/schemas/Delta' application/x-www-form-urlencoded: schema: $ref: '#/components/schemas/Delta' multipart/form-data: schema: $ref: '#/components/schemas/Delta' security: - cookieAuth: [] - basicAuth: [] - {} responses: '200': content: application/json: schema: $ref: '#/components/schemas/Alpha' description: '' /doesitall/only-response-override/: post: operationId: doesitall_only_response_override_create tags: - doesitall requestBody: content: application/json: schema: $ref: '#/components/schemas/Alpha' application/x-www-form-urlencoded: schema: $ref: '#/components/schemas/Alpha' multipart/form-data: schema: $ref: '#/components/schemas/Alpha' required: true security: - cookieAuth: [] - basicAuth: [] - {} responses: '204': description: No response body /doesitall/serializer-query/: get: operationId: doesitall_serializer_query_retrieve parameters: - in: query name: contains schema: type: string maxLength: 10 minLength: 3 description: filter by containing string - in: query name: nested schema: $ref: '#/components/schemas/Query' - in: query name: order_by schema: type: array items: enum: - a - b - c type: string description: |- * `a` - a * `b` - b * `c` - c default: - a - in: query name: stars schema: type: integer maximum: 5 minimum: 1 description: filter by rating stars required: true - in: query name: tag schema: type: string minLength: 1 tags: - doesitall security: - cookieAuth: [] - basicAuth: [] - {} responses: '200': content: application/json: schema: $ref: '#/components/schemas/Alpha' description: '' components: schemas: Alpha: type: object properties: field_a: type: string field_b: type: integer required: - field_a - field_b Beta: type: object properties: field_a: type: string field_b: type: integer field_c: {} required: - field_a - field_b - field_c Delta: type: object properties: field_a: type: string field_b: type: integer ErrorDetail: type: object properties: field_i: type: string format: date-time readOnly: true field_j: allOf: - $ref: '#/components/schemas/Inline' nullable: true readOnly: true field_k: type: array items: $ref: '#/components/schemas/Inline' readOnly: true field_l: allOf: - $ref: '#/components/schemas/FieldLEnum' readOnly: true field_m: type: array items: type: integer readOnly: true required: - field_i - field_j - field_k - field_l - field_m FieldLEnum: enum: - a - b type: string description: |- * `a` - a * `b` - b GammaEpsilon: type: object properties: encoding: type: string image_data: type: string format: byte custom_url_field: type: string format: byte required: - custom_url_field - encoding - image_data Inline: type: object properties: inline_b: type: boolean inline_i: type: integer required: - inline_b - inline_i OrderByEnum: enum: - a - b - c type: string description: |- * `a` - a * `b` - b * `c` - c Query: type: object properties: stars: type: integer maximum: 5 minimum: 1 description: filter by rating stars contains: type: string description: filter by containing string maxLength: 10 minLength: 3 order_by: type: array items: $ref: '#/components/schemas/OrderByEnum' default: - a tag: type: string required: - stars securitySchemes: basicAuth: type: http scheme: basic cookieAuth: type: apiKey in: cookie name: sessionid drf-spectacular-0.27.0/tests/test_extend_schema_view.py000066400000000000000000000103101453572150400232740ustar00rootroot00000000000000import pytest from django.db import models from rest_framework import mixins, routers, serializers, viewsets from rest_framework.decorators import action from rest_framework.response import Response from rest_framework.test import APIClient from drf_spectacular.generators import SchemaGenerator from drf_spectacular.types import OpenApiTypes from drf_spectacular.utils import extend_schema, extend_schema_view from tests import assert_schema class ESVModel(models.Model): pass class ESVSerializer(serializers.ModelSerializer): class Meta: model = ESVModel fields = '__all__' class DualMethodActionParamsSerializer(serializers.Serializer): message = serializers.CharField() @extend_schema(tags=['global-tag']) @extend_schema_view( list=extend_schema(description='view list description'), retrieve=extend_schema(description='view retrieve description'), extended_action=extend_schema(description='view extended action description'), raw_action=extend_schema(description='view raw action description'), dual_method_action=[ extend_schema(parameters=[DualMethodActionParamsSerializer], methods=['GET']), extend_schema(request=DualMethodActionParamsSerializer, methods=['POST']), ] ) class XViewSet(mixins.ListModelMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet): queryset = ESVModel.objects.all() serializer_class = ESVSerializer @extend_schema(tags=['custom-retrieve-tag']) def retrieve(self, request, *args, **kwargs): return super().retrieve(request, *args, **kwargs) @extend_schema(responses=OpenApiTypes.DATE) @action(detail=False) def extended_action(self, request): return Response('2020-10-31') @action(detail=False, methods=['GET']) def raw_action(self, request): return Response('2019-03-01') @extend_schema(description='view dual method action description') @action(detail=False, methods=['GET', 'POST']) def dual_method_action(self, request): if request.method == 'POST': data = request.data else: data = request.query_params return Response(data['message']) # view to make sure there is no cross-talk class YViewSet(viewsets.ModelViewSet): serializer_class = ESVSerializer queryset = ESVModel.objects.all() # view to make sure that schema applied to a subclass does not affect its parent. @extend_schema_view( list=extend_schema(exclude=True), retrieve=extend_schema(description='overridden description for child only'), extended_action=extend_schema(responses={200: {'type': 'string', 'pattern': r'^[0-9]{4}(?:-[0-9]{2}){2}$'}}), raw_action=extend_schema(summary="view raw action summary"), ) class ZViewSet(XViewSet): @extend_schema(tags=['child-tag']) @action(detail=False, methods=['GET']) def raw_action(self, request): return Response('2019-03-01') # pragma: no cover router = routers.SimpleRouter() router.register('x', XViewSet) router.register('y', YViewSet) router.register('z', ZViewSet) urlpatterns = router.urls @pytest.mark.urls(__name__) def test_extend_schema_view(no_warnings): assert_schema( SchemaGenerator().get_schema(request=None, public=True), 'tests/test_extend_schema_view.yml' ) @pytest.mark.urls(__name__) @pytest.mark.django_db def test_extend_schema_view_call_transparency(no_warnings): ESVModel.objects.create() response = APIClient().get('/x/') assert response.status_code == 200 assert response.content == b'[{"id":1}]' response = APIClient().get('/x/1/') assert response.status_code == 200 assert response.content == b'{"id":1}' response = APIClient().get('/x/extended_action/') assert response.status_code == 200 assert response.content == b'"2020-10-31"' response = APIClient().get('/x/raw_action/') assert response.status_code == 200 assert response.content == b'"2019-03-01"' response = APIClient().get('/x/dual_method_action/', {'message': 'foo bar'}) assert response.status_code == 200 assert response.content == b'"foo bar"' response = APIClient().post('/x/dual_method_action/', {'message': 'foo bar'}) assert response.status_code == 200 assert response.content == b'"foo bar"' drf-spectacular-0.27.0/tests/test_extend_schema_view.yml000066400000000000000000000237441453572150400234640ustar00rootroot00000000000000openapi: 3.0.3 info: title: '' version: 0.0.0 paths: /x/: get: operationId: x_list description: view list description tags: - global-tag security: - cookieAuth: [] - basicAuth: [] - {} responses: '200': content: application/json: schema: type: array items: $ref: '#/components/schemas/ESV' description: '' /x/{id}/: get: operationId: x_retrieve description: view retrieve description parameters: - in: path name: id schema: type: integer description: A unique integer value identifying this esv model. required: true tags: - custom-retrieve-tag security: - cookieAuth: [] - basicAuth: [] - {} responses: '200': content: application/json: schema: $ref: '#/components/schemas/ESV' description: '' /x/dual_method_action/: get: operationId: x_dual_method_action_retrieve description: view dual method action description parameters: - in: query name: message schema: type: string minLength: 1 required: true tags: - global-tag security: - cookieAuth: [] - basicAuth: [] - {} responses: '200': content: application/json: schema: $ref: '#/components/schemas/ESV' description: '' post: operationId: x_dual_method_action_create description: view dual method action description tags: - global-tag requestBody: content: application/json: schema: $ref: '#/components/schemas/DualMethodActionParams' application/x-www-form-urlencoded: schema: $ref: '#/components/schemas/DualMethodActionParams' multipart/form-data: schema: $ref: '#/components/schemas/DualMethodActionParams' required: true security: - cookieAuth: [] - basicAuth: [] - {} responses: '200': content: application/json: schema: $ref: '#/components/schemas/ESV' description: '' /x/extended_action/: get: operationId: x_extended_action_retrieve description: view extended action description tags: - global-tag security: - cookieAuth: [] - basicAuth: [] - {} responses: '200': content: application/json: schema: type: string format: date description: '' /x/raw_action/: get: operationId: x_raw_action_retrieve description: view raw action description tags: - global-tag security: - cookieAuth: [] - basicAuth: [] - {} responses: '200': content: application/json: schema: $ref: '#/components/schemas/ESV' description: '' /y/: get: operationId: y_list tags: - y security: - cookieAuth: [] - basicAuth: [] - {} responses: '200': content: application/json: schema: type: array items: $ref: '#/components/schemas/ESV' description: '' post: operationId: y_create tags: - y requestBody: content: application/json: schema: $ref: '#/components/schemas/ESV' application/x-www-form-urlencoded: schema: $ref: '#/components/schemas/ESV' multipart/form-data: schema: $ref: '#/components/schemas/ESV' security: - cookieAuth: [] - basicAuth: [] - {} responses: '201': content: application/json: schema: $ref: '#/components/schemas/ESV' description: '' /y/{id}/: get: operationId: y_retrieve parameters: - in: path name: id schema: type: integer description: A unique integer value identifying this esv model. required: true tags: - y security: - cookieAuth: [] - basicAuth: [] - {} responses: '200': content: application/json: schema: $ref: '#/components/schemas/ESV' description: '' put: operationId: y_update parameters: - in: path name: id schema: type: integer description: A unique integer value identifying this esv model. required: true tags: - y requestBody: content: application/json: schema: $ref: '#/components/schemas/ESV' application/x-www-form-urlencoded: schema: $ref: '#/components/schemas/ESV' multipart/form-data: schema: $ref: '#/components/schemas/ESV' security: - cookieAuth: [] - basicAuth: [] - {} responses: '200': content: application/json: schema: $ref: '#/components/schemas/ESV' description: '' patch: operationId: y_partial_update parameters: - in: path name: id schema: type: integer description: A unique integer value identifying this esv model. required: true tags: - y requestBody: content: application/json: schema: $ref: '#/components/schemas/PatchedESV' application/x-www-form-urlencoded: schema: $ref: '#/components/schemas/PatchedESV' multipart/form-data: schema: $ref: '#/components/schemas/PatchedESV' security: - cookieAuth: [] - basicAuth: [] - {} responses: '200': content: application/json: schema: $ref: '#/components/schemas/ESV' description: '' delete: operationId: y_destroy parameters: - in: path name: id schema: type: integer description: A unique integer value identifying this esv model. required: true tags: - y security: - cookieAuth: [] - basicAuth: [] - {} responses: '204': description: No response body /z/{id}/: get: operationId: z_retrieve description: overridden description for child only parameters: - in: path name: id schema: type: integer description: A unique integer value identifying this esv model. required: true tags: - custom-retrieve-tag security: - cookieAuth: [] - basicAuth: [] - {} responses: '200': content: application/json: schema: $ref: '#/components/schemas/ESV' description: '' /z/dual_method_action/: get: operationId: z_dual_method_action_retrieve description: view dual method action description parameters: - in: query name: message schema: type: string minLength: 1 required: true tags: - global-tag security: - cookieAuth: [] - basicAuth: [] - {} responses: '200': content: application/json: schema: $ref: '#/components/schemas/ESV' description: '' post: operationId: z_dual_method_action_create description: view dual method action description tags: - global-tag requestBody: content: application/json: schema: $ref: '#/components/schemas/DualMethodActionParams' application/x-www-form-urlencoded: schema: $ref: '#/components/schemas/DualMethodActionParams' multipart/form-data: schema: $ref: '#/components/schemas/DualMethodActionParams' required: true security: - cookieAuth: [] - basicAuth: [] - {} responses: '200': content: application/json: schema: $ref: '#/components/schemas/ESV' description: '' /z/extended_action/: get: operationId: z_extended_action_retrieve description: view extended action description tags: - global-tag security: - cookieAuth: [] - basicAuth: [] - {} responses: '200': content: application/json: schema: type: string pattern: ^[0-9]{4}(?:-[0-9]{2}){2}$ description: '' /z/raw_action/: get: operationId: z_raw_action_retrieve summary: view raw action summary tags: - child-tag security: - cookieAuth: [] - basicAuth: [] - {} responses: '200': content: application/json: schema: $ref: '#/components/schemas/ESV' description: '' components: schemas: DualMethodActionParams: type: object properties: message: type: string required: - message ESV: type: object properties: id: type: integer readOnly: true required: - id PatchedESV: type: object properties: id: type: integer readOnly: true securitySchemes: basicAuth: type: http scheme: basic cookieAuth: type: apiKey in: cookie name: sessionid drf-spectacular-0.27.0/tests/test_extensions.py000066400000000000000000000346231453572150400216470ustar00rootroot00000000000000from typing import TYPE_CHECKING from unittest import mock from django.contrib.auth.models import User from rest_framework import fields, mixins, pagination, permissions, serializers, viewsets from rest_framework.authentication import BaseAuthentication from rest_framework.decorators import api_view from rest_framework.response import Response from rest_framework.views import APIView from drf_spectacular.extensions import ( OpenApiAuthenticationExtension, OpenApiSerializerExtension, OpenApiSerializerFieldExtension, OpenApiViewExtension, ) from drf_spectacular.plumbing import ( ResolvedComponent, build_array_type, build_basic_type, build_object_type, force_instance, ) from drf_spectacular.types import OpenApiTypes from drf_spectacular.utils import Direction, extend_schema, extend_schema_field, extend_schema_view from tests import generate_schema, get_response_schema from tests.models import SimpleModel, SimpleSerializer if TYPE_CHECKING: from drf_spectacular.openapi import AutoSchema class Base64Field(fields.Field): pass # pragma: no cover def test_serializer_field_extension(no_warnings): class Base64FieldExtension(OpenApiSerializerFieldExtension): target_class = 'tests.test_extensions.Base64Field' def map_serializer_field(self, auto_schema, direction): return build_basic_type(OpenApiTypes.BYTE) class XSerializer(serializers.Serializer): hash = Base64Field() class XViewset(mixins.ListModelMixin, viewsets.GenericViewSet): serializer_class = XSerializer schema = generate_schema('x', XViewset) assert schema['components']['schemas']['X']['properties']['hash']['type'] == 'string' assert schema['components']['schemas']['X']['properties']['hash']['format'] == 'byte' def test_serializer_field_extension_can_return_none(no_warnings): """Field extensions can return None, which should exclude them from schema""" class BlindBase64Field(fields.Field): pass # pragma: no cover class Base64FieldExtension(OpenApiSerializerFieldExtension): target_class = BlindBase64Field def map_serializer_field(self, auto_schema, direction): return None # At least 1 field is required to generate schema, include 'other' class XSerializer(serializers.Serializer): hash = BlindBase64Field() other = fields.CharField() class XViewset(mixins.ListModelMixin, viewsets.GenericViewSet): serializer_class = XSerializer schema = generate_schema('x', XViewset) # field 'hash' is missing from the schema output assert schema['components']['schemas']['X'] == { 'type': 'object', 'properties': {'other': {'type': 'string'}}, 'required': ['other'] } class Base32Field(fields.Field): pass # pragma: no cover def test_serializer_field_extension_with_breakout(no_warnings): class Base32FieldExtension(OpenApiSerializerFieldExtension): target_class = 'tests.test_extensions.Base32Field' def get_name(self): return 'FieldComponentName' def map_serializer_field(self, auto_schema, direction): return build_basic_type(OpenApiTypes.BYTE) class XSerializer(serializers.Serializer): hash = Base32Field() class XViewset(mixins.ListModelMixin, viewsets.GenericViewSet): serializer_class = XSerializer schema = generate_schema('x', XViewset) assert schema['components']['schemas']['FieldComponentName'] == { 'type': 'string', 'format': 'byte' } class XView(APIView): """ underspecified library view """ def get(self): """ docstring for GET """ return Response(1.234) # pragma: no cover def test_view_extension(no_warnings): class FixXView(OpenApiViewExtension): target_class = 'tests.test_extensions.XView' def view_replacement(self): class Fixed(self.target_class): @extend_schema(responses=OpenApiTypes.FLOAT) def get(self, request): pass # pragma: no cover return Fixed schema = generate_schema('x', view=XView) operation = schema['paths']['/x']['get'] assert get_response_schema(operation)['type'] == 'number' assert operation['description'] == 'docstring for GET' @api_view() def x_view_function(request): """ underspecified library view """ return Response(1.234) # pragma: no cover def test_view_function_extension(no_warnings): class FixXFunctionView(OpenApiViewExtension): target_class = 'tests.test_extensions.x_view_function' def view_replacement(self): fixed = extend_schema(responses=OpenApiTypes.FLOAT)(self.target_class) return fixed schema = generate_schema('x', view_function=x_view_function) operation = schema['paths']['/x']['get'] assert get_response_schema(operation)['type'] == 'number' assert operation['description'].strip() == 'underspecified library view' def test_extension_not_found_for_installed_app(capsys): class FixXFunctionView(OpenApiViewExtension): target_class = 'tests.test_extensions.NotExistingClass' def view_replacement(self): pass # pragma: no cover OpenApiViewExtension.get_match(object()) assert 'target class was not found' in capsys.readouterr().err class MultiHeaderAuth(BaseAuthentication): pass def test_multi_auth_scheme_extension(no_warnings): class MultiHeaderAuthExtension(OpenApiAuthenticationExtension): target_class = 'tests.test_extensions.MultiHeaderAuth' name = ['apiKey', 'appId'] def get_security_definition(self, auto_schema): return [ {'type': 'apiKey', 'in': 'header', 'name': 'X-API-KEY'}, {'type': 'apiKey', 'in': 'header', 'name': 'X-APP-ID'}, ] class XViewset(viewsets.ReadOnlyModelViewSet): queryset = SimpleModel.objects.none() serializer_class = SimpleSerializer authentication_classes = [MultiHeaderAuth] permission_classes = [permissions.IsAuthenticated] schema = generate_schema('/x', XViewset) assert schema['components']['securitySchemes'] == { 'apiKey': {'type': 'apiKey', 'in': 'header', 'name': 'X-API-KEY'}, 'appId': {'type': 'apiKey', 'in': 'header', 'name': 'X-APP-ID'} } assert schema['paths']['/x/']['get']['security'] == [{'apiKey': [], 'appId': []}] def test_serializer_list_extension(no_warnings): class CustomListSerializer(serializers.ListSerializer): def to_representation(self, data): return {'foo': 1, 'data': super().to_representation(data)} # pragma: no cover # ListSerializer can be injected either via Meta attribute or by overriding many_init() class XSerializer(serializers.ModelSerializer): class Meta: model = SimpleModel fields = '__all__' list_serializer_class = CustomListSerializer class CustomListExtension(OpenApiSerializerExtension): target_class = CustomListSerializer def map_serializer(self, auto_schema, direction): component = auto_schema.resolve_serializer(self.target.child, direction) schema = build_object_type( properties={'foo': build_basic_type(int), 'data': build_array_type(component.ref)} ) list_component = ResolvedComponent( name=f'{component.name}List', type=ResolvedComponent.SCHEMA, object=self.target.child, schema=schema ) auto_schema.registry.register_on_missing(list_component) return list_component.ref class XViewset(mixins.ListModelMixin, viewsets.GenericViewSet): serializer_class = XSerializer schema = generate_schema('x', XViewset) op_schema = get_response_schema(schema['paths']['/x/']['get']) assert op_schema == {'$ref': '#/components/schemas/XList'} assert schema['components']['schemas']['XList'] == { 'type': 'object', 'properties': { 'foo': {'type': 'integer'}, 'data': {'type': 'array', 'items': {'$ref': '#/components/schemas/X'}} } } @mock.patch('drf_spectacular.settings.spectacular_settings.COMPONENT_SPLIT_REQUEST', True) def test_serializer_envelope_through_extension(no_warnings): class EnvelopeMixin: pass # actual enveloping not implemented. This could be done internally with # to_representation or externally with a custom Renderer class XSerializer(EnvelopeMixin, serializers.ModelSerializer): name = serializers.CharField() class Meta: model = SimpleModel fields = '__all__' envelope = 'foo' # some arbitrary addition to Meta for example class EnvelopeFix(OpenApiSerializerExtension): target_class = EnvelopeMixin match_subclasses = True def get_name(self, auto_schema: 'AutoSchema', direction: Direction): if direction == 'request': return None else: return f"Enveloped{self.target.__class__.__name__}" def map_serializer(self, auto_schema: 'AutoSchema', direction: Direction): if direction == 'request': return auto_schema._map_serializer(self.target, direction, bypass_extensions=True) else: component = auto_schema.resolve_serializer(self.target, direction, bypass_extensions=True) if not component: return {} return build_object_type( properties={self.target.Meta.envelope: component.ref} ) class XViewset(viewsets.ModelViewSet): serializer_class = XSerializer queryset = SimpleModel.objects.none() schema = generate_schema('/x', XViewset) assert 'X' in schema['components']['schemas'] assert 'EnvelopedX' in schema['components']['schemas'] assert 'XRequest' in schema['components']['schemas'] assert 'PatchedXRequest' in schema['components']['schemas'] def test_serializer_method_pagination_through_extension(no_warnings): class PaginationWrapper(serializers.BaseSerializer): def __init__(self, serializer_class, pagination_class, **kwargs): self.serializer_class = serializer_class self.pagination_class = pagination_class super().__init__(**kwargs) class PaginationWrapperExtension(OpenApiSerializerExtension): target_class = PaginationWrapper # this can also be an import string def get_name(self, auto_schema, direction): return auto_schema.get_paginated_name( auto_schema._get_serializer_name( serializer=force_instance(self.target.serializer_class), direction=direction ) ) def map_serializer(self, auto_schema, direction): component = auto_schema.resolve_serializer(self.target.serializer_class, direction) paginated_schema = self.target.pagination_class().get_paginated_response_schema(component.ref) return paginated_schema class XSerializer(serializers.ModelSerializer): method = serializers.SerializerMethodField() @extend_schema_field( PaginationWrapper( serializer_class=SimpleSerializer, pagination_class=pagination.LimitOffsetPagination ) ) def get_method(self, obj): pass # pragma: no cover class Meta: fields = '__all__' model = SimpleModel class XViewset(viewsets.ModelViewSet): serializer_class = XSerializer queryset = SimpleModel.objects.none() schema = generate_schema('x', XViewset) assert 'Simple' in schema['components']['schemas'] assert 'PaginatedSimpleList' in schema['components']['schemas'] assert schema['components']['schemas']['PaginatedSimpleList']['properties']['results'] == { '$ref': '#/components/schemas/Simple' } def test_serializer_with_dynamic_fields(no_warnings): class DynamicFieldsModelSerializer(serializers.ModelSerializer): """ A ModelSerializer that takes an additional `fields` argument that controls which fields should be displayed. Taken from (only added ref_name) https://www.django-rest-framework.org/api-guide/serializers/#dynamically-modifying-fields """ def __init__(self, *args, **kwargs): # Don't pass the 'fields' arg up to the superclass fields = kwargs.pop('fields', None) self.ref_name = kwargs.pop('ref_name', None) # only change to original version! # Instantiate the superclass normally super().__init__(*args, **kwargs) if fields is not None: # Drop any fields that are not specified in the `fields` argument. allowed = set(fields) existing = set(self.fields) for field_name in existing - allowed: self.fields.pop(field_name) class UserSerializer(DynamicFieldsModelSerializer): class Meta: model = User fields = ['id', 'username', 'email'] class DynamicFieldsModelSerializerExtension(OpenApiSerializerExtension): target_class = DynamicFieldsModelSerializer # this can also be an import string match_subclasses = True def map_serializer(self, auto_schema: 'AutoSchema', direction: Direction): return auto_schema._map_serializer(self.target, direction, bypass_extensions=True) def get_name(self, auto_schema, direction): return self.target.ref_name @extend_schema_view( list=extend_schema(responses=UserSerializer(fields=['id'], ref_name='CompactUser')) ) class XViewset(viewsets.ModelViewSet): serializer_class = UserSerializer queryset = User.objects.none() schema = generate_schema('x', XViewset) assert schema['components']['schemas']['User']['properties'] == { 'id': {'type': 'integer', 'readOnly': True}, 'username': { 'type': 'string', 'description': 'Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', 'pattern': '^[\\w.@+-]+$', 'maxLength': 150 }, 'email': {'type': 'string', 'format': 'email', 'title': 'Email address', 'maxLength': 254} } assert schema['components']['schemas']['CompactUser']['properties'] == { 'id': {'type': 'integer', 'readOnly': True} } drf-spectacular-0.27.0/tests/test_fields.py000066400000000000000000000305471453572150400207170ustar00rootroot00000000000000import functools import json import sys import tempfile import uuid from datetime import timedelta from decimal import Decimal from typing import Optional from unittest import mock import pytest from django import __version__ as DJANGO_VERSION from django.core.files.base import ContentFile from django.core.files.storage import FileSystemStorage from django.db import models from django.urls import reverse from django.utils.functional import cached_property from rest_framework import serializers, viewsets from rest_framework.routers import SimpleRouter from rest_framework.test import APIClient from drf_spectacular.generators import SchemaGenerator from tests import assert_equal, assert_schema, build_absolute_file_path if sys.version_info >= (3, 8): functools_cached_property = functools.cached_property else: # We re-use Django's cached_property when it's not available to # keep tests unified across Python versions. functools_cached_property = cached_property fs = FileSystemStorage(location=tempfile.gettempdir()) class Aux(models.Model): id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) field_foreign = models.ForeignKey('Aux', null=True, on_delete=models.CASCADE) url = models.URLField(unique=True, help_text="URL identifier for Aux") class AuxSerializer(serializers.ModelSerializer): """ description for aux object """ class Meta: fields = '__all__' model = Aux class SubObject: def __init__(self, instance): self._instance = instance @property def calculated(self) -> int: """ My calculated property """ return self._instance.field_int @property def nested(self) -> 'SubObject': return self @property def model_instance(self) -> 'AllFields': return self._instance @property def optional_int(self) -> Optional[int]: return 1 class AllFields(models.Model): # basics field_int = models.IntegerField() field_float = models.FloatField() field_bool = models.BooleanField() field_char = models.CharField(max_length=100) field_text = models.TextField(verbose_name='a text field') # special field_slug = models.SlugField() field_email = models.EmailField() field_uuid = models.UUIDField() field_url = models.URLField() field_ip_generic = models.GenericIPAddressField(protocol='ipv6') field_decimal = models.DecimalField(max_digits=6, decimal_places=3) field_file = models.FileField(storage=fs) field_img = models.ImageField(storage=fs) field_date = models.DateField() field_datetime = models.DateTimeField() field_bigint = models.BigIntegerField() field_smallint = models.SmallIntegerField() field_posint = models.PositiveIntegerField() field_possmallint = models.PositiveSmallIntegerField() if DJANGO_VERSION > '3.1': field_nullbool = models.BooleanField(null=True) else: field_nullbool = models.NullBooleanField() field_time = models.TimeField() field_duration = models.DurationField() field_binary = models.BinaryField() # relations field_foreign = models.ForeignKey( Aux, on_delete=models.CASCADE, help_text='main aux object', related_name='ff' ) field_m2m = models.ManyToManyField( Aux, help_text='set of related aux objects', related_name='fm' ) field_o2o = models.OneToOneField( Aux, on_delete=models.CASCADE, help_text='bound aux object', related_name='fo' ) # overrides field_regex = models.CharField(max_length=50) field_bool_override = models.BooleanField() if DJANGO_VERSION >= '3.1': field_json = models.JSONField() else: @property def field_json(self): return {'A': 1, 'B': 2} @property def field_model_property_float(self) -> float: return 1.337 @cached_property def field_model_cached_property_float(self) -> float: return 1.337 @functools_cached_property def field_model_py_cached_property_float(self) -> float: return 1.337 @property def field_list(self): return [1.1, 2.2, 3.3] @property def field_list_object(self): return self.field_m2m.all() def model_function_basic(self) -> bool: return True def model_function_model(self) -> Aux: return self.field_foreign @property def model_property_model(self) -> Aux: return self.field_foreign @property def sub_object(self) -> SubObject: return SubObject(self) @cached_property def sub_object_cached(self) -> SubObject: return SubObject(self) @functools_cached_property def sub_object_py_cached(self) -> SubObject: return SubObject(self) @property def optional_sub_object(self) -> Optional[SubObject]: return SubObject(self) class AllFieldsSerializer(serializers.ModelSerializer): field_decimal_uncoerced = serializers.DecimalField( source='field_decimal', max_digits=6, decimal_places=3, coerce_to_string=False ) field_method_float = serializers.SerializerMethodField() def get_field_method_float(self, obj) -> float: return 1.3456 field_method_object = serializers.SerializerMethodField() def get_field_method_object(self, obj) -> dict: return {'key': 'value'} field_regex = serializers.RegexField(r'^[a-zA-z0-9]{10}\-[a-z]', label='A regex field') field_hidden = serializers.HiddenField(default='') # composite fields field_list = serializers.ListField( child=serializers.FloatField(), min_length=3, max_length=100, ) field_list_serializer = serializers.ListField( child=AuxSerializer(), source='field_list_object', ) # extra related fields field_related_slug = serializers.SlugRelatedField( read_only=True, source='field_foreign', slug_field='url', ) # type: ignore field_related_slug_queryset = serializers.SlugRelatedField( source='field_foreign', slug_field='url', queryset=Aux.objects.all() ) field_related_slug_many = serializers.SlugRelatedField( many=True, read_only=True, source='field_m2m', slug_field='url', ) # type: ignore field_related_string = serializers.StringRelatedField( source='field_foreign' ) # type: ignore field_related_hyperlink = serializers.HyperlinkedRelatedField( read_only=True, source='field_foreign', view_name='aux-detail' ) # type: ignore field_identity_hyperlink = serializers.HyperlinkedIdentityField( read_only=True, view_name='allfields-detail' ) # read only - model traversal field_read_only_nav_uuid = serializers.ReadOnlyField(source='field_foreign.id') field_read_only_nav_uuid_3steps = serializers.ReadOnlyField( source='field_foreign.field_foreign.field_foreign.id', allow_null=True, # force field output even if traversal fails ) field_read_only_model_function_basic = serializers.ReadOnlyField(source='model_function_basic') field_read_only_model_function_model = serializers.ReadOnlyField(source='model_function_model.id') field_read_only_model_property_model = serializers.ReadOnlyField(source='model_property_model.id') # override default writable bool field with readonly field_bool_override = serializers.ReadOnlyField() field_model_property_float = serializers.ReadOnlyField() field_model_cached_property_float = serializers.ReadOnlyField() field_model_py_cached_property_float = serializers.ReadOnlyField() field_dict_int = serializers.DictField( child=serializers.IntegerField(), source='field_json', ) # there is a JSON model field for django>=3.1 that would be placed automatically. for <=3.1 we # need to set the field explicitly. defined here for both cases to have consistent ordering. field_json = serializers.JSONField() # traversal of non-model types of complex object field_sub_object_calculated = serializers.ReadOnlyField(source='sub_object.calculated') field_sub_object_nested_calculated = serializers.ReadOnlyField(source='sub_object.nested.calculated') field_sub_object_model_int = serializers.ReadOnlyField(source='sub_object.model_instance.field_int') field_sub_object_cached_calculated = serializers.ReadOnlyField(source='sub_object_cached.calculated') field_sub_object_cached_nested_calculated = serializers.ReadOnlyField(source='sub_object_cached.nested.calculated') field_sub_object_cached_model_int = serializers.ReadOnlyField(source='sub_object_cached.model_instance.field_int') field_sub_object_py_cached_calculated = serializers.ReadOnlyField(source='sub_object_py_cached.calculated') field_sub_object_py_cached_nested_calculated = serializers.ReadOnlyField( source='sub_object_py_cached.nested.calculated', ) field_sub_object_py_cached_model_int = serializers.ReadOnlyField( source='sub_object_py_cached.model_instance.field_int', ) # typing.Optional field_optional_sub_object_calculated = serializers.ReadOnlyField( source='optional_sub_object.calculated', allow_null=True, ) field_sub_object_optional_int = serializers.ReadOnlyField( source='sub_object.optional_int', allow_null=True, ) class Meta: fields = '__all__' model = AllFields class AllFieldsModelViewset(viewsets.ReadOnlyModelViewSet): serializer_class = AllFieldsSerializer queryset = AllFields.objects.all() def retrieve(self, request, *args, **kwargs): return super().retrieve(request, *args, **kwargs) class AuxModelViewset(viewsets.ReadOnlyModelViewSet): serializer_class = AuxSerializer queryset = Aux.objects.all() router = SimpleRouter() router.register('allfields', AllFieldsModelViewset) router.register('aux', AuxModelViewset) urlpatterns = router.urls @pytest.mark.urls(__name__) def test_fields(no_warnings, django_transforms): assert_schema( SchemaGenerator().get_schema(request=None, public=True), 'tests/test_fields.yml', transforms=django_transforms, ) @pytest.mark.urls(__name__) @mock.patch('drf_spectacular.settings.spectacular_settings.OAS_VERSION', '3.1.0') def test_fields_oas_3_1(no_warnings, django_transforms): assert_schema( SchemaGenerator().get_schema(request=None, public=True), 'tests/test_fields_oas_3_1.yml', transforms=django_transforms, ) @pytest.mark.urls(__name__) @pytest.mark.django_db def test_model_setup_is_valid(): aux = Aux( id='0ac6930d-87f4-40e8-8242-10a3ed31a335', url='https://xkcd.com' ) aux.save() m = AllFields( # basics field_int=1, field_float=1.25, field_bool=True, field_char='char', field_text='text', # special field_slug='all_fields', field_email='test@example.com', field_uuid='00000000-00000000-00000000-00000000', field_url='https://github.com/tfranzel/drf-spectacular', field_ip_generic='2001:db8::8a2e:370:7334', field_decimal=Decimal('-666.333'), field_file=None, field_img=None, # TODO fill with data below field_date='2021-09-09', field_datetime='2021-09-09T10:15:26.049862', field_bigint=11111111111111, field_smallint=111111, field_posint=123, field_possmallint=1, field_nullbool=None, field_time='00:05:23.283', field_duration=timedelta(seconds=10), field_binary=b'\xce\x9c\xce\xb9\xce\xb1', # relations field_foreign=aux, field_o2o=aux, # overrides field_regex='12345asdfg-a', field_bool_override=True, ) if DJANGO_VERSION >= '3.1': m.field_json = {'A': 1, 'B': 2} m.field_file.save('hello.txt', ContentFile("hello world"), save=True) m.save() m.field_m2m.add(aux) response = APIClient().get(reverse('allfields-detail', args=(m.pk,))) assert response.status_code == 200 with open(build_absolute_file_path('tests/test_fields_response.json')) as fh: expected = json.load(fh) if DJANGO_VERSION < '3': expected['field_file'] = f'http://testserver/allfields/1/{m.field_file.name}' else: expected['field_file'] = f'http://testserver/{m.field_file.name}' if DJANGO_VERSION >= '5': expected['field_datetime'] = '2021-09-09T10:15:26.049862-05:00' assert_equal(json.loads(response.content), expected) drf-spectacular-0.27.0/tests/test_fields.yml000066400000000000000000000241701453572150400210630ustar00rootroot00000000000000openapi: 3.0.3 info: title: '' version: 0.0.0 paths: /allfields/: get: operationId: allfields_list tags: - allfields security: - cookieAuth: [] - basicAuth: [] - {} responses: '200': content: application/json: schema: type: array items: $ref: '#/components/schemas/AllFields' description: '' /allfields/{id}/: get: operationId: allfields_retrieve parameters: - in: path name: id schema: type: integer description: A unique integer value identifying this all fields. required: true tags: - allfields security: - cookieAuth: [] - basicAuth: [] - {} responses: '200': content: application/json: schema: $ref: '#/components/schemas/AllFields' description: '' /aux/: get: operationId: aux_list tags: - aux security: - cookieAuth: [] - basicAuth: [] - {} responses: '200': content: application/json: schema: type: array items: $ref: '#/components/schemas/Aux' description: '' /aux/{id}/: get: operationId: aux_retrieve parameters: - in: path name: id schema: type: string format: uuid description: A UUID string identifying this aux. required: true tags: - aux security: - cookieAuth: [] - basicAuth: [] - {} responses: '200': content: application/json: schema: $ref: '#/components/schemas/Aux' description: '' components: schemas: AllFields: type: object properties: id: type: integer readOnly: true field_decimal_uncoerced: type: number format: double maximum: 1000 minimum: -1000 exclusiveMaximum: true exclusiveMinimum: true field_method_float: type: number format: double readOnly: true field_method_object: type: object additionalProperties: {} readOnly: true field_regex: type: string title: A regex field pattern: ^[a-zA-z0-9]{10}\-[a-z] field_list: type: array items: type: number format: double maxItems: 100 minItems: 3 field_list_serializer: type: array items: $ref: '#/components/schemas/Aux' field_related_slug: type: string format: uri description: URL identifier for Aux readOnly: true field_related_slug_queryset: type: string format: uri description: URL identifier for Aux field_related_slug_many: type: array items: type: string format: uri description: URL identifier for Aux readOnly: true field_related_string: type: string readOnly: true field_related_hyperlink: type: string format: uri readOnly: true field_identity_hyperlink: type: string format: uri readOnly: true field_read_only_nav_uuid: type: string format: uuid readOnly: true field_read_only_nav_uuid_3steps: type: string format: uuid readOnly: true nullable: true field_read_only_model_function_basic: type: boolean readOnly: true field_read_only_model_function_model: type: string format: uuid readOnly: true field_read_only_model_property_model: type: string format: uuid readOnly: true field_bool_override: type: boolean readOnly: true field_model_property_float: type: number format: double readOnly: true field_model_cached_property_float: type: number format: double readOnly: true field_model_py_cached_property_float: type: number format: double readOnly: true field_dict_int: type: object additionalProperties: type: integer field_json: {} field_sub_object_calculated: type: integer description: My calculated property readOnly: true field_sub_object_nested_calculated: type: integer description: My calculated property readOnly: true field_sub_object_model_int: type: integer readOnly: true field_sub_object_cached_calculated: type: integer description: My calculated property readOnly: true field_sub_object_cached_nested_calculated: type: integer description: My calculated property readOnly: true field_sub_object_cached_model_int: type: integer readOnly: true field_sub_object_py_cached_calculated: type: integer description: My calculated property readOnly: true field_sub_object_py_cached_nested_calculated: type: integer description: My calculated property readOnly: true field_sub_object_py_cached_model_int: type: integer readOnly: true field_optional_sub_object_calculated: type: integer description: My calculated property readOnly: true nullable: true field_sub_object_optional_int: type: integer nullable: true readOnly: true field_int: type: integer field_float: type: number format: double field_bool: type: boolean field_char: type: string maxLength: 100 field_text: type: string title: A text field field_slug: type: string maxLength: 50 pattern: ^[-a-zA-Z0-9_]+$ field_email: type: string format: email maxLength: 254 field_uuid: type: string format: uuid field_url: type: string format: uri maxLength: 200 field_ip_generic: type: string field_decimal: type: string format: decimal pattern: ^-?\d{0,3}(?:\.\d{0,3})?$ field_file: type: string format: uri field_img: type: string format: uri field_date: type: string format: date field_datetime: type: string format: date-time field_bigint: type: integer field_smallint: type: integer field_posint: type: integer field_possmallint: type: integer field_nullbool: type: boolean nullable: true field_time: type: string format: time field_duration: type: string field_binary: type: string format: byte readOnly: true field_foreign: type: string format: uuid description: main aux object field_o2o: type: string format: uuid description: bound aux object field_m2m: type: array items: type: string format: uuid description: set of related aux objects required: - field_bigint - field_binary - field_bool - field_bool_override - field_char - field_date - field_datetime - field_decimal - field_decimal_uncoerced - field_dict_int - field_duration - field_email - field_file - field_float - field_foreign - field_identity_hyperlink - field_img - field_int - field_ip_generic - field_json - field_list - field_list_serializer - field_m2m - field_method_float - field_method_object - field_model_cached_property_float - field_model_property_float - field_model_py_cached_property_float - field_o2o - field_optional_sub_object_calculated - field_posint - field_possmallint - field_read_only_model_function_basic - field_read_only_model_function_model - field_read_only_model_property_model - field_read_only_nav_uuid - field_read_only_nav_uuid_3steps - field_regex - field_related_hyperlink - field_related_slug - field_related_slug_many - field_related_slug_queryset - field_related_string - field_slug - field_smallint - field_sub_object_cached_calculated - field_sub_object_cached_model_int - field_sub_object_cached_nested_calculated - field_sub_object_calculated - field_sub_object_model_int - field_sub_object_nested_calculated - field_sub_object_optional_int - field_sub_object_py_cached_calculated - field_sub_object_py_cached_model_int - field_sub_object_py_cached_nested_calculated - field_text - field_time - field_url - field_uuid - id Aux: type: object description: description for aux object properties: id: type: string format: uuid readOnly: true url: type: string format: uri description: URL identifier for Aux maxLength: 200 field_foreign: type: string format: uuid nullable: true required: - id - url securitySchemes: basicAuth: type: http scheme: basic cookieAuth: type: apiKey in: cookie name: sessionid drf-spectacular-0.27.0/tests/test_fields_oas_3_1.yml000066400000000000000000000241461453572150400223720ustar00rootroot00000000000000openapi: 3.1.0 info: title: '' version: 0.0.0 paths: /allfields/: get: operationId: allfields_list tags: - allfields security: - cookieAuth: [] - basicAuth: [] - {} responses: '200': content: application/json: schema: type: array items: $ref: '#/components/schemas/AllFields' description: '' /allfields/{id}/: get: operationId: allfields_retrieve parameters: - in: path name: id schema: type: integer description: A unique integer value identifying this all fields. required: true tags: - allfields security: - cookieAuth: [] - basicAuth: [] - {} responses: '200': content: application/json: schema: $ref: '#/components/schemas/AllFields' description: '' /aux/: get: operationId: aux_list tags: - aux security: - cookieAuth: [] - basicAuth: [] - {} responses: '200': content: application/json: schema: type: array items: $ref: '#/components/schemas/Aux' description: '' /aux/{id}/: get: operationId: aux_retrieve parameters: - in: path name: id schema: type: string format: uuid description: A UUID string identifying this aux. required: true tags: - aux security: - cookieAuth: [] - basicAuth: [] - {} responses: '200': content: application/json: schema: $ref: '#/components/schemas/Aux' description: '' components: schemas: AllFields: type: object properties: id: type: integer readOnly: true field_decimal_uncoerced: type: number format: double exclusiveMaximum: 1000 exclusiveMinimum: -1000 field_method_float: type: number format: double readOnly: true field_method_object: type: object additionalProperties: {} readOnly: true field_regex: type: string title: A regex field pattern: ^[a-zA-z0-9]{10}\-[a-z] field_list: type: array items: type: number format: double maxItems: 100 minItems: 3 field_list_serializer: type: array items: $ref: '#/components/schemas/Aux' field_related_slug: type: string format: uri description: URL identifier for Aux readOnly: true field_related_slug_queryset: type: string format: uri description: URL identifier for Aux field_related_slug_many: type: array items: type: string format: uri description: URL identifier for Aux readOnly: true field_related_string: type: string readOnly: true field_related_hyperlink: type: string format: uri readOnly: true field_identity_hyperlink: type: string format: uri readOnly: true field_read_only_nav_uuid: type: string format: uuid readOnly: true field_read_only_nav_uuid_3steps: type: - string - 'null' format: uuid readOnly: true field_read_only_model_function_basic: type: boolean readOnly: true field_read_only_model_function_model: type: string format: uuid readOnly: true field_read_only_model_property_model: type: string format: uuid readOnly: true field_bool_override: type: boolean readOnly: true field_model_property_float: type: number format: double readOnly: true field_model_cached_property_float: type: number format: double readOnly: true field_model_py_cached_property_float: type: number format: double readOnly: true field_dict_int: type: object additionalProperties: type: integer field_json: {} field_sub_object_calculated: type: integer description: My calculated property readOnly: true field_sub_object_nested_calculated: type: integer description: My calculated property readOnly: true field_sub_object_model_int: type: integer readOnly: true field_sub_object_cached_calculated: type: integer description: My calculated property readOnly: true field_sub_object_cached_nested_calculated: type: integer description: My calculated property readOnly: true field_sub_object_cached_model_int: type: integer readOnly: true field_sub_object_py_cached_calculated: type: integer description: My calculated property readOnly: true field_sub_object_py_cached_nested_calculated: type: integer description: My calculated property readOnly: true field_sub_object_py_cached_model_int: type: integer readOnly: true field_optional_sub_object_calculated: type: - integer - 'null' description: My calculated property readOnly: true field_sub_object_optional_int: type: - integer - 'null' readOnly: true field_int: type: integer field_float: type: number format: double field_bool: type: boolean field_char: type: string maxLength: 100 field_text: type: string title: A text field field_slug: type: string maxLength: 50 pattern: ^[-a-zA-Z0-9_]+$ field_email: type: string format: email maxLength: 254 field_uuid: type: string format: uuid field_url: type: string format: uri maxLength: 200 field_ip_generic: type: string field_decimal: type: string format: decimal pattern: ^-?\d{0,3}(?:\.\d{0,3})?$ field_file: type: string format: uri field_img: type: string format: uri field_date: type: string format: date field_datetime: type: string format: date-time field_bigint: type: integer field_smallint: type: integer field_posint: type: integer field_possmallint: type: integer field_nullbool: type: - boolean - 'null' field_time: type: string format: time field_duration: type: string field_binary: type: string format: byte readOnly: true field_foreign: type: string format: uuid description: main aux object field_o2o: type: string format: uuid description: bound aux object field_m2m: type: array items: type: string format: uuid description: set of related aux objects required: - field_bigint - field_binary - field_bool - field_bool_override - field_char - field_date - field_datetime - field_decimal - field_decimal_uncoerced - field_dict_int - field_duration - field_email - field_file - field_float - field_foreign - field_identity_hyperlink - field_img - field_int - field_ip_generic - field_json - field_list - field_list_serializer - field_m2m - field_method_float - field_method_object - field_model_cached_property_float - field_model_property_float - field_model_py_cached_property_float - field_o2o - field_optional_sub_object_calculated - field_posint - field_possmallint - field_read_only_model_function_basic - field_read_only_model_function_model - field_read_only_model_property_model - field_read_only_nav_uuid - field_read_only_nav_uuid_3steps - field_regex - field_related_hyperlink - field_related_slug - field_related_slug_many - field_related_slug_queryset - field_related_string - field_slug - field_smallint - field_sub_object_cached_calculated - field_sub_object_cached_model_int - field_sub_object_cached_nested_calculated - field_sub_object_calculated - field_sub_object_model_int - field_sub_object_nested_calculated - field_sub_object_optional_int - field_sub_object_py_cached_calculated - field_sub_object_py_cached_model_int - field_sub_object_py_cached_nested_calculated - field_text - field_time - field_url - field_uuid - id Aux: type: object description: description for aux object properties: id: type: string format: uuid readOnly: true url: type: string format: uri description: URL identifier for Aux maxLength: 200 field_foreign: type: - string - 'null' format: uuid required: - id - url securitySchemes: basicAuth: type: http scheme: basic cookieAuth: type: apiKey in: cookie name: sessionid drf-spectacular-0.27.0/tests/test_fields_response.json000066400000000000000000000053311453572150400231470ustar00rootroot00000000000000{ "id": 1, "field_decimal_uncoerced": -666.333, "field_method_float": 1.3456, "field_method_object": { "key": "value" }, "field_regex": "12345asdfg-a", "field_list": [ 1.1, 2.2, 3.3 ], "field_list_serializer": [ { "id": "0ac6930d-87f4-40e8-8242-10a3ed31a335", "url": "https://xkcd.com", "field_foreign": null } ], "field_related_slug": "https://xkcd.com", "field_related_slug_queryset": "https://xkcd.com", "field_related_slug_many": [ "https://xkcd.com" ], "field_related_string": "Aux object (0ac6930d-87f4-40e8-8242-10a3ed31a335)", "field_related_hyperlink": "http://testserver/aux/0ac6930d-87f4-40e8-8242-10a3ed31a335/", "field_identity_hyperlink": "http://testserver/allfields/1/", "field_read_only_nav_uuid": "0ac6930d-87f4-40e8-8242-10a3ed31a335", "field_read_only_nav_uuid_3steps": null, "field_read_only_model_function_basic": true, "field_read_only_model_function_model": "0ac6930d-87f4-40e8-8242-10a3ed31a335", "field_read_only_model_property_model": "0ac6930d-87f4-40e8-8242-10a3ed31a335", "field_bool_override": true, "field_model_property_float": 1.337, "field_model_cached_property_float": 1.337, "field_model_py_cached_property_float": 1.337, "field_dict_int": { "A": 1, "B": 2 }, "field_json": { "A": 1, "B": 2 }, "field_sub_object_calculated": 1, "field_sub_object_nested_calculated": 1, "field_sub_object_model_int": 1, "field_sub_object_cached_calculated": 1, "field_sub_object_cached_nested_calculated": 1, "field_sub_object_cached_model_int": 1, "field_sub_object_py_cached_calculated": 1, "field_sub_object_py_cached_nested_calculated": 1, "field_sub_object_py_cached_model_int": 1, "field_optional_sub_object_calculated": 1, "field_sub_object_optional_int": 1, "field_int": 1, "field_float": 1.25, "field_bool": true, "field_char": "char", "field_text": "text", "field_slug": "all_fields", "field_email": "test@example.com", "field_uuid": "00000000-0000-0000-0000-000000000000", "field_url": "https://github.com/tfranzel/drf-spectacular", "field_ip_generic": "2001:db8::8a2e:370:7334", "field_decimal": "-666.333", "field_file": "http://testserver/hello_p8Eotwr.txt", "field_img": null, "field_date": "2021-09-09", "field_datetime": "2021-09-09T10:15:26.049862", "field_bigint": 11111111111111, "field_smallint": 111111, "field_posint": 123, "field_possmallint": 1, "field_nullbool": null, "field_time": "00:05:23.283000", "field_duration": "00:00:10", "field_binary": "zpzOuc6x", "field_foreign": "0ac6930d-87f4-40e8-8242-10a3ed31a335", "field_o2o": "0ac6930d-87f4-40e8-8242-10a3ed31a335", "field_m2m": [ "0ac6930d-87f4-40e8-8242-10a3ed31a335" ] } drf-spectacular-0.27.0/tests/test_generator_stats.py000066400000000000000000000006151453572150400226460ustar00rootroot00000000000000import inspect import pytest from drf_spectacular.drainage import GENERATOR_STATS def test_known_attribute_access_succeeds(): assert hasattr(GENERATOR_STATS, 'silent') def test_unknown_attribute_access_fails(): with pytest.raises(AttributeError): getattr(GENERATOR_STATS, '__spam__') def test_inspect_unwrap(): assert inspect.unwrap(GENERATOR_STATS) is GENERATOR_STATS drf-spectacular-0.27.0/tests/test_i18n.py000066400000000000000000000057251453572150400202300ustar00rootroot00000000000000from unittest import mock import pytest import yaml from django.db import models from django.urls import include, path from django.utils import translation from django.utils.translation import gettext_lazy as _ from rest_framework import mixins, routers, serializers, viewsets from rest_framework.test import APIClient from drf_spectacular.utils import extend_schema from drf_spectacular.validation import validate_schema from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView from tests import assert_schema, generate_schema class I18nModel(models.Model): field_str = models.TextField() class Meta: verbose_name = _("Internationalization") verbose_name_plural = _('Internationalizations') class XSerializer(serializers.ModelSerializer): class Meta: model = I18nModel fields = '__all__' class XViewset(mixins.ListModelMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet): __doc__ = _(""" More lengthy explanation of the view """) # type: ignore serializer_class = XSerializer queryset = I18nModel.objects.none() @extend_schema( summary=_('Main endpoint for creating X'), responses=None ) def create(self, request, *args, **kwargs): pass # pragma: no cover router = routers.SimpleRouter() router.register('x', XViewset, basename='x') urlpatterns = [ path('api/', include(router.urls)), path('api/schema/', SpectacularAPIView.as_view(), name='schema'), path('api/schema/swagger-ui/', SpectacularSwaggerView.as_view()), ] @mock.patch( 'drf_spectacular.settings.spectacular_settings.DESCRIPTION', _('Lazy translated description with missing translation') ) def test_i18n_strings(no_warnings): with translation.override('de-de'): schema = generate_schema(None, patterns=urlpatterns) assert_schema(schema, 'tests/test_i18n.yml') @pytest.mark.parametrize(['url', 'header', 'translated'], [ ('/api/schema/', {}, False), ('/api/schema/?lang=de', {}, True), ('/api/schema/', {'HTTP_ACCEPT_LANGUAGE': 'de-de'}, True) ]) @pytest.mark.urls(__name__) def test_i18n_schema(no_warnings, url, header, translated): response = APIClient().get(url, **header) schema = yaml.load(response.content, Loader=yaml.SafeLoader) validate_schema(schema) operation = schema['paths']['/api/x/']['post'] if translated: assert 'Eine laengere Erklaerung' in operation['description'] assert 'Hauptendpunkt fuer' in operation['summary'] assert 'Kein Inhalt' in operation['responses']['201']['description'] else: assert 'More lengthy explanation' in operation['description'] assert 'Main endpoint' in operation['summary'] assert 'No response body' in operation['responses']['201']['description'] @pytest.mark.urls(__name__) def test_i18n_schema_ui(no_warnings): response = APIClient().get('/api/schema/swagger-ui/?lang=de') assert b'/api/schema/?lang\\u003Dde' in response.content drf-spectacular-0.27.0/tests/test_i18n.yml000066400000000000000000000064651453572150400204030ustar00rootroot00000000000000openapi: 3.0.3 info: title: '' version: 0.0.0 description: Lazy translated description with missing translation paths: /api/schema/: get: operationId: schema_retrieve description: |- OpenApi3 schema for this API. Format can be selected via content negotiation. - YAML: application/vnd.oai.openapi - JSON: application/vnd.oai.openapi+json parameters: - in: query name: format schema: type: string enum: - json - yaml - in: query name: lang schema: type: string enum: - de-de - en-us tags: - schema security: - cookieAuth: [] - basicAuth: [] - {} responses: '200': content: application/vnd.oai.openapi: schema: type: object additionalProperties: {} application/yaml: schema: type: object additionalProperties: {} application/vnd.oai.openapi+json: schema: type: object additionalProperties: {} application/json: schema: type: object additionalProperties: {} description: '' /api/x/: get: operationId: x_list description: Eine laengere Erklaerung des Views tags: - x security: - cookieAuth: [] - basicAuth: [] - {} responses: '200': content: application/json: schema: type: array items: $ref: '#/components/schemas/X' description: '' post: operationId: x_create description: Eine laengere Erklaerung des Views summary: Hauptendpunkt fuer die Erstellung von X tags: - x requestBody: content: application/json: schema: $ref: '#/components/schemas/X' application/x-www-form-urlencoded: schema: $ref: '#/components/schemas/X' multipart/form-data: schema: $ref: '#/components/schemas/X' required: true security: - cookieAuth: [] - basicAuth: [] - {} responses: '201': description: Kein Inhalt /api/x/{id}/: get: operationId: x_retrieve description: Eine laengere Erklaerung des Views parameters: - in: path name: id schema: type: integer description: A unique integer value identifying this Internätiönalisierung. required: true tags: - x security: - cookieAuth: [] - basicAuth: [] - {} responses: '200': content: application/json: schema: $ref: '#/components/schemas/X' description: '' components: schemas: X: type: object properties: id: type: integer readOnly: true field_str: type: string required: - field_str - id securitySchemes: basicAuth: type: http scheme: basic cookieAuth: type: apiKey in: cookie name: sessionid drf-spectacular-0.27.0/tests/test_meta.py000066400000000000000000000037321453572150400203730ustar00rootroot00000000000000from unittest import mock from rest_framework.views import APIView from tests import generate_schema META = { 'TITLE': 'Spectacular API', 'DESCRIPTION': 'auto-generated spectacular schema for your API', 'TOS': 'https://github.com/tfranzel/drf-spectacular/blob/master/LICENSE', 'CONTACT': { "name": "API Support", "url": "https://github.com/tfranzel/drf-spectacular/issues", "email": "support@example.com" }, 'LICENSE': { 'name': 'BSD 3-Clause', 'url': 'https://github.com/tfranzel/drf-spectacular/blob/master/LICENSE', }, 'VERSION': '1.0.0', 'SERVERS': [ { "url": "https://gigantic-server.com/v1", "description": "Production server" }, { "url": "https://development.gigantic-server.com/v1", "description": "Development server" } ], 'TAGS': [{ "name": "awesome-tag", "description": "Operations that are awesome" }], 'EXTERNAL_DOCS': { "url": "https://gigantic-server.com/documentation", "description": "Documentation for API usage" }, } def build_settings_mock(name): return (f'drf_spectacular.settings.spectacular_settings.{name}', META[name]) @mock.patch(*build_settings_mock('TITLE')) @mock.patch(*build_settings_mock('DESCRIPTION')) @mock.patch(*build_settings_mock('TOS')) @mock.patch(*build_settings_mock('CONTACT')) @mock.patch(*build_settings_mock('LICENSE')) @mock.patch(*build_settings_mock('VERSION')) @mock.patch(*build_settings_mock('SERVERS')) @mock.patch(*build_settings_mock('TAGS')) @mock.patch(*build_settings_mock('EXTERNAL_DOCS')) def test_append_extra_components(no_warnings): class XAPIView(APIView): pass schema = generate_schema('x', view=XAPIView) assert schema['info']['version'] == '1.0.0' assert schema['info']['description'] assert 'termsOfService' in schema['info'] assert 'servers' in schema assert 'externalDocs' in schema drf-spectacular-0.27.0/tests/test_mock_request.py000066400000000000000000000076221453572150400221500ustar00rootroot00000000000000import pytest import yaml from django.contrib.auth.models import User from django.urls import include, path from rest_framework import routers, viewsets from rest_framework.authentication import TokenAuthentication from rest_framework.authtoken.models import Token from rest_framework.permissions import IsAuthenticated from rest_framework.test import APIClient from rest_framework.versioning import AcceptHeaderVersioning from drf_spectacular.generators import SchemaGenerator from drf_spectacular.views import SpectacularAPIView from tests.models import SimpleModel, SimpleSerializer class AnotherSimpleSerializer(SimpleSerializer): pass class XViewset(viewsets.ModelViewSet): authentication_classes = [TokenAuthentication] permission_classes = [IsAuthenticated] queryset = SimpleModel.objects.none() def get_serializer_class(self): # make sure the mocked request possesses the correct path and # schema endpoint path does not leak in. assert self.request.path.startswith('/api/x/') # make schema dependent on request method if self.request.method == 'GET': return SimpleSerializer else: return AnotherSimpleSerializer router = routers.SimpleRouter() router.register('x', XViewset) urlpatterns = [ path('api/', include(router.urls)), path('api/schema-plain/', SpectacularAPIView.as_view()), path('api/schema-authenticated/', SpectacularAPIView.as_view( authentication_classes=[TokenAuthentication] )), path('api/schema-authenticated-private/', SpectacularAPIView.as_view( authentication_classes=[TokenAuthentication], serve_public=False, )), path('api/schema-versioned/', SpectacularAPIView.as_view( versioning_class=AcceptHeaderVersioning )) ] @pytest.mark.urls(__name__) def test_mock_request_symmetry_plain(no_warnings): response = APIClient().get('/api/schema-plain/', **{'HTTP_X_SPECIAL_HEADER': '1'}) assert response.status_code == 200 schema_online = yaml.load(response.content, Loader=yaml.SafeLoader) schema_offline = SchemaGenerator().get_schema(public=True) assert schema_offline == schema_online @pytest.mark.urls(__name__) def test_mock_request_symmetry_version(no_warnings): response = APIClient().get('/api/schema-versioned/', **{ 'HTTP_ACCEPT': 'application/json; version=v2', }) assert response.status_code == 200 schema_online = yaml.load(response.content, Loader=yaml.SafeLoader) schema_offline = SchemaGenerator(api_version='v2').get_schema(public=True) assert schema_offline == schema_online assert schema_online['info']['version'] == '0.0.0 (v2)' @pytest.mark.parametrize(['serve_public', 'authenticated', 'url', 'expected_endpoints'], [ (True, True, '/api/schema-authenticated/', 5), (True, False, '/api/schema-authenticated/', 5), (False, True, '/api/schema-authenticated-private/', None), (False, False, '/api/schema-authenticated-private/', 3), ]) @pytest.mark.urls(__name__) @pytest.mark.django_db def test_mock_request_symmetry_authentication( no_warnings, serve_public, authenticated, url, expected_endpoints ): user = User.objects.create(username='test') token, _ = Token.objects.get_or_create(user=user) auth_header = {'HTTP_AUTHORIZATION': f'Token {token}'} if authenticated else {} response = APIClient().get(url, **auth_header) assert response.status_code == 200 schema_online = yaml.load(response.content, Loader=yaml.SafeLoader) schema_offline = SchemaGenerator().get_schema(public=serve_public) if expected_endpoints: assert schema_offline == schema_online assert len(schema_online['paths']) == expected_endpoints else: # authenticated & non-public case does not really make sense for # offline generation as we have no request. assert len(schema_online['paths']) == 5 assert len(schema_offline['paths']) == 3 drf-spectacular-0.27.0/tests/test_oas31.py000066400000000000000000000041211453572150400203640ustar00rootroot00000000000000from unittest import mock from rest_framework import serializers from rest_framework.views import APIView from drf_spectacular.utils import extend_schema from tests import generate_schema @mock.patch('drf_spectacular.settings.spectacular_settings.OAS_VERSION', '3.1.0') def test_nullable_sub_serializer(no_warnings): class XSerializer(serializers.Serializer): f = serializers.FloatField(allow_null=True) class YSerializer(serializers.Serializer): x = XSerializer(allow_null=True) class XAPIView(APIView): @extend_schema(responses=YSerializer) def get(self, request): pass # pragma: no cover schema = generate_schema('x', view=XAPIView) assert schema['components']['schemas'] == { 'X': { 'properties': {'f': {'format': 'double', 'type': ['number', 'null']}}, 'required': ['f'], 'type': 'object' }, 'Y': { 'properties': {'x': {'oneOf': [{'$ref': '#/components/schemas/X'}, {'type': 'null'}]}}, 'required': ['x'], 'type': 'object' } } @mock.patch('drf_spectacular.settings.spectacular_settings.OAS_VERSION', '3.1.0') def test_nullable_enum_resolution(no_warnings): class XSerializer(serializers.Serializer): foo = serializers.ChoiceField( choices=[('A', 'A'), ('B', 'B')], allow_null=True ) class XAPIView(APIView): @extend_schema(responses=XSerializer) def get(self, request): pass # pragma: no cover schema = generate_schema('x', view=XAPIView) assert schema['components']['schemas']['FooEnum'] == { 'description': '* `A` - A\n* `B` - B', 'enum': ['A', 'B'], 'type': 'string', } assert schema['components']['schemas']['X'] == { 'properties': { 'foo': { 'oneOf': [ {'$ref': '#/components/schemas/FooEnum'}, {'$ref': '#/components/schemas/NullEnum'} ] } }, 'required': ['foo'], 'type': 'object' } drf-spectacular-0.27.0/tests/test_plumbing.py000066400000000000000000000277551453572150400212750ustar00rootroot00000000000000import collections import json import re import sys import typing from datetime import datetime from enum import Enum if sys.version_info >= (3, 8): from typing import TypedDict else: from typing_extensions import TypedDict import pytest from django import __version__ as DJANGO_VERSION from django.conf.urls import include from django.db import models from django.urls import re_path from rest_framework import generics, serializers from drf_spectacular.openapi import AutoSchema from drf_spectacular.plumbing import ( analyze_named_regex_pattern, build_basic_type, build_choice_field, detype_pattern, follow_field_source, force_instance, get_list_serializer, is_field, is_serializer, resolve_type_hint, safe_ref, ) from drf_spectacular.validation import validate_schema from tests import generate_schema def test_get_list_serializer_preserves_context(): serializer = serializers.Serializer(context={"foo": "bar"}) list_serializer = get_list_serializer(serializer) assert list_serializer.context == {"foo": "bar"} def test_is_serializer(): assert not is_serializer(serializers.SlugField) assert not is_serializer(serializers.SlugField()) assert not is_serializer(models.CharField) assert not is_serializer(models.CharField()) assert is_serializer(serializers.Serializer) assert is_serializer(serializers.Serializer()) def test_is_field(): assert is_field(serializers.SlugField) assert is_field(serializers.SlugField()) assert not is_field(models.CharField) assert not is_field(models.CharField()) assert not is_field(serializers.Serializer) assert not is_field(serializers.Serializer()) def test_force_instance(): assert isinstance(force_instance(serializers.CharField), serializers.CharField) assert force_instance(5) == 5 assert force_instance(dict) == dict def test_follow_field_source_forward_reverse(no_warnings): class FFS1(models.Model): id = models.UUIDField(primary_key=True) field_bool = models.BooleanField() class FFS2(models.Model): ffs1 = models.ForeignKey(FFS1, on_delete=models.PROTECT) class FFS3(models.Model): id = models.CharField(primary_key=True, max_length=3) ffs2 = models.ForeignKey(FFS2, on_delete=models.PROTECT) field_float = models.FloatField() forward_field = follow_field_source(FFS3, ['ffs2', 'ffs1', 'field_bool']) reverse_field = follow_field_source(FFS1, ['ffs2', 'ffs3', 'field_float']) forward_model = follow_field_source(FFS3, ['ffs2', 'ffs1']) reverse_model = follow_field_source(FFS1, ['ffs2', 'ffs3']) assert isinstance(forward_field, models.BooleanField) assert isinstance(reverse_field, models.FloatField) assert isinstance(forward_model, models.UUIDField) assert isinstance(reverse_model, models.CharField) auto_schema = AutoSchema() assert auto_schema._map_model_field(forward_field, None)['type'] == 'boolean' assert auto_schema._map_model_field(reverse_field, None)['type'] == 'number' assert auto_schema._map_model_field(forward_model, None)['type'] == 'string' assert auto_schema._map_model_field(reverse_model, None)['type'] == 'string' def test_detype_patterns_with_module_includes(no_warnings): detype_pattern( pattern=re_path(r'^', include('tests.test_fields')) ) NamedTupleA = collections.namedtuple("NamedTupleA", "a, b") class NamedTupleB(typing.NamedTuple): a: int b: str class LanguageEnum(str, Enum): EN = 'en' DE = 'de' # Make sure we can deal with plain Enums that are not handled by DRF. # The second base class makes this work for DRF. class InvalidLanguageEnum(Enum): EN = 'en' DE = 'de' TD1 = TypedDict('TD1', {"foo": int, "bar": typing.List[str]}) TD2 = TypedDict('TD2', {"foo": str, "bar": typing.Dict[str, int]}) TYPE_HINT_TEST_PARAMS = [ ( typing.Optional[int], {'type': 'integer', 'nullable': True} ), ( typing.List[int], {'type': 'array', 'items': {'type': 'integer'}} ), ( typing.List[typing.Dict[str, int]], {'type': 'array', 'items': {'type': 'object', 'additionalProperties': {'type': 'integer'}}} ), ( list, {'type': 'array', 'items': {}} ), ( typing.Tuple[int, int, int], {'type': 'array', 'items': {'type': 'integer'}, 'minLength': 3, 'maxLength': 3} ), ( typing.Set[datetime], {'type': 'array', 'items': {'type': 'string', 'format': 'date-time'}} ), ( typing.FrozenSet[datetime], {'type': 'array', 'items': {'type': 'string', 'format': 'date-time'}} ), ( typing.Dict[str, int], {'type': 'object', 'additionalProperties': {'type': 'integer'}} ), ( typing.Dict[str, str], {'type': 'object', 'additionalProperties': {'type': 'string'}} ), ( typing.Dict[str, typing.List[int]], {'type': 'object', 'additionalProperties': {'type': 'array', 'items': {'type': 'integer'}}} ), ( dict, {'type': 'object', 'additionalProperties': {}} ), ( typing.Union[int, str], {'oneOf': [{'type': 'integer'}, {'type': 'string'}]} ), ( typing.Union[int, str, None], {'oneOf': [{'type': 'integer'}, {'type': 'string'}], 'nullable': True} ), ( typing.Optional[typing.Union[str, int]], {'oneOf': [{'type': 'string'}, {'type': 'integer'}], 'nullable': True} ), ( LanguageEnum, {'enum': ['en', 'de'], 'type': 'string'} ), ( InvalidLanguageEnum, {'enum': ['en', 'de']} ), ( NamedTupleB, { 'type': 'object', 'properties': {'a': {'type': 'integer'}, 'b': {'type': 'string'}}, 'required': ['a', 'b'] } ) ] if DJANGO_VERSION > '3': from django.db.models.enums import TextChoices # only available in Django>3 class LanguageChoices(TextChoices): EN = 'en' DE = 'de' TYPE_HINT_TEST_PARAMS.append(( LanguageChoices, {'enum': ['en', 'de'], 'type': 'string'} )) if sys.version_info >= (3, 7): TYPE_HINT_TEST_PARAMS.append(( typing.Iterable[NamedTupleA], { 'type': 'array', 'items': {'type': 'object', 'properties': {'a': {}, 'b': {}}, 'required': ['a', 'b']} } )) if sys.version_info >= (3, 8): # Literal only works for python >= 3.8 despite typing_extensions, because it # behaves slightly different w.r.t. __origin__ TYPE_HINT_TEST_PARAMS.append(( typing.Literal['x', 'y'], {'enum': ['x', 'y'], 'type': 'string'} )) class TD3(TypedDict, total=False): """a test description""" a: str TYPE_HINT_TEST_PARAMS.append(( TD3, { 'type': 'object', 'description': 'a test description', 'properties': { 'a': {'type': 'string'}, } } )) if sys.version_info >= (3, 9): TYPE_HINT_TEST_PARAMS.append(( dict[str, int], {'type': 'object', 'additionalProperties': {'type': 'integer'}} )) # typing.TypedDict for py==3.8 is missing the __required_keys__ feature. # below that we use typing_extensions.TypedDict, which does contain it. if sys.version_info >= (3, 9) or sys.version_info < (3, 8): class TD4Optional(TypedDict, total=False): a: str class TD4(TD4Optional): """A test description2""" b: bool TYPE_HINT_TEST_PARAMS.append(( TD1, { 'type': 'object', 'properties': { 'foo': {'type': 'integer'}, 'bar': {'type': 'array', 'items': {'type': 'string'}} }, 'required': ['bar', 'foo'] } )) TYPE_HINT_TEST_PARAMS.append(( typing.List[TD2], { 'type': 'array', 'items': { 'type': 'object', 'properties': { 'foo': {'type': 'string'}, 'bar': {'type': 'object', 'additionalProperties': {'type': 'integer'}} }, 'required': ['bar', 'foo'], } } )) TYPE_HINT_TEST_PARAMS.append(( TD4, { 'type': 'object', 'description': 'A test description2', 'properties': { 'a': {'type': 'string'}, 'b': {'type': 'boolean'} }, 'required': ['b'], }) ) else: TYPE_HINT_TEST_PARAMS.append(( TD1, { 'type': 'object', 'properties': { 'foo': {'type': 'integer'}, 'bar': {'type': 'array', 'items': {'type': 'string'}} }, } )) TYPE_HINT_TEST_PARAMS.append(( typing.List[TD2], { 'type': 'array', 'items': { 'type': 'object', 'properties': { 'foo': {'type': 'string'}, 'bar': {'type': 'object', 'additionalProperties': {'type': 'integer'}} } }, } )) # New X | Y union syntax in Python 3.10+ (PEP 604) if sys.version_info >= (3, 10): TYPE_HINT_TEST_PARAMS.extend([ ( int | None, {'type': 'integer', 'nullable': True} ), ( int | str, {'oneOf': [{'type': 'integer'}, {'type': 'string'}]} ), ( int | str | None, {'oneOf': [{'type': 'integer'}, {'type': 'string'}], 'nullable': True} ), ( list[int | str], {"type": "array", "items": {"oneOf": [{"type": "integer"}, {"type": "string"}]}} ) ]) @pytest.mark.parametrize(['type_hint', 'ref_schema'], TYPE_HINT_TEST_PARAMS) def test_type_hint_extraction(no_warnings, type_hint, ref_schema): def func() -> type_hint: pass # pragma: no cover # check expected resolution schema = resolve_type_hint(typing.get_type_hints(func).get('return')) assert json.dumps(schema) == json.dumps(ref_schema) # check schema validity class XSerializer(serializers.Serializer): x = serializers.SerializerMethodField() XSerializer.get_x = func class XView(generics.RetrieveAPIView): serializer_class = XSerializer validate_schema(generate_schema('/x', view=XView)) @pytest.mark.parametrize(['pattern', 'output'], [ ('(?P<,()(())(),)', {'t1': '<,()(())(),'}), (r'(?P.\\)', {'t1': r'.\\'}), (r'(?P.\\\\)', {'t1': r'.\\\\'}), (r'(?P.\))', {'t1': r'.\)'}), (r'(?P)', {'t1': r''}), (r'(?P.[\(]{2})', {'t1': r'.[\(]{2}'}), (r'(?P(.))/\(t/(?P\){2}()\({2}().*)', {'t1': '(.)', 't2': r'\){2}()\({2}().*'}), ]) def test_analyze_named_regex_pattern(no_warnings, pattern, output): re.compile(pattern) # check validity of regex assert analyze_named_regex_pattern(pattern) == output def test_unknown_basic_type(capsys): build_basic_type(object) assert 'could not resolve type for "' in capsys.readouterr().err def test_choicefield_choices_enum(): schema = build_choice_field(serializers.ChoiceField(['bluepill', 'redpill'])) assert schema['enum'] == ['bluepill', 'redpill'] assert schema['type'] == 'string' schema = build_choice_field(serializers.ChoiceField( ['bluepill', 'redpill'], allow_null=True, allow_blank=True )) assert schema['enum'] == ['bluepill', 'redpill', '', None] assert schema['type'] == 'string' schema = build_choice_field(serializers.ChoiceField( choices=['bluepill', 'redpill', '', None], allow_null=True, allow_blank=True )) assert schema['enum'] == ['bluepill', 'redpill', '', None] assert 'type' not in schema def test_safe_ref(): schema = build_basic_type(str) schema['$ref'] = '#/components/schemas/Foo' schema = safe_ref(schema) assert schema == { 'allOf': [{'$ref': '#/components/schemas/Foo'}], 'type': 'string' } del schema['type'] schema = safe_ref(schema) assert schema == {'$ref': '#/components/schemas/Foo'} assert safe_ref(schema) == safe_ref(schema) drf-spectacular-0.27.0/tests/test_polymorphic.py000066400000000000000000000341011453572150400220040ustar00rootroot00000000000000from unittest import mock import pytest from django.db import models from rest_framework import serializers, viewsets from rest_framework.decorators import api_view from rest_framework.response import Response from drf_spectacular.openapi import AutoSchema from drf_spectacular.utils import ( OpenApiParameter, PolymorphicProxySerializer, extend_schema, extend_schema_field, ) from tests import assert_schema, generate_schema, get_request_schema, get_response_schema class LegalPerson2(models.Model): company_name = models.CharField(max_length=30) class NaturalPerson2(models.Model): first_name = models.CharField(max_length=30) last_name = models.CharField(max_length=30) class LegalPersonSerializer(serializers.ModelSerializer): type = serializers.SerializerMethodField() class Meta: model = LegalPerson2 fields = ('id', 'company_name', 'type') def get_type(self, obj) -> str: return 'legal' class NaturalPersonSerializer(serializers.ModelSerializer): type = serializers.SerializerMethodField() class Meta: model = NaturalPerson2 fields = ('id', 'first_name', 'last_name', 'type') def get_type(self, obj) -> str: return 'natural' PROXY_SERIALIZER_PARAMS = { 'component_name': 'MetaPerson', 'serializers': [LegalPersonSerializer, NaturalPersonSerializer], 'resource_type_field_name': 'type', } with mock.patch('rest_framework.settings.api_settings.DEFAULT_SCHEMA_CLASS', AutoSchema): implicit_poly_proxy = PolymorphicProxySerializer( component_name='MetaPerson', serializers=[LegalPersonSerializer, NaturalPersonSerializer], resource_type_field_name='type', ) class ImplicitPersonViewSet(viewsets.GenericViewSet): @extend_schema(request=implicit_poly_proxy, responses=implicit_poly_proxy) def create(self, request, *args, **kwargs): return Response({}) # pragma: no cover @extend_schema( request=implicit_poly_proxy, responses=implicit_poly_proxy, parameters=[OpenApiParameter('id', int, OpenApiParameter.PATH)], ) def partial_update(self, request, *args, **kwargs): return Response({}) # pragma: no cover explicit_poly_proxy = PolymorphicProxySerializer( component_name='MetaPerson', serializers={ 'legal': LegalPersonSerializer, 'natural': NaturalPersonSerializer, }, resource_type_field_name='type', ) class ExplicitPersonViewSet(viewsets.GenericViewSet): @extend_schema(request=explicit_poly_proxy, responses=explicit_poly_proxy) def create(self, request, *args, **kwargs): return Response({}) # pragma: no cover @extend_schema( request=explicit_poly_proxy, responses=explicit_poly_proxy, parameters=[OpenApiParameter('id', int, OpenApiParameter.PATH)], ) def partial_update(self, request, *args, **kwargs): return Response({}) # pragma: no cover lambda_poly_proxy = PolymorphicProxySerializer( component_name='MetaPerson', serializers=lambda: [LegalPersonSerializer, NaturalPersonSerializer], resource_type_field_name='type', ) class LambdaPersonViewSet(viewsets.GenericViewSet): @extend_schema(request=lambda_poly_proxy, responses=lambda_poly_proxy) def create(self, request, *args, **kwargs): return Response({}) # pragma: no cover @extend_schema( request=lambda_poly_proxy, responses=lambda_poly_proxy, parameters=[OpenApiParameter('id', int, OpenApiParameter.PATH)], ) def partial_update(self, request, *args, **kwargs): return Response({}) # pragma: no cover @pytest.mark.parametrize('viewset', [ImplicitPersonViewSet, ExplicitPersonViewSet, LambdaPersonViewSet]) def test_polymorphic(no_warnings, viewset): assert_schema( generate_schema('persons', viewset), 'tests/test_polymorphic.yml' ) def test_polymorphic_serializer_as_field_via_extend_schema_field(no_warnings): @extend_schema_field( PolymorphicProxySerializer( component_name='MetaPerson', serializers=[LegalPersonSerializer, NaturalPersonSerializer], resource_type_field_name='type', ) ) class XField(serializers.DictField): pass # pragma: no cover class XSerializer(serializers.Serializer): field = XField() @extend_schema(request=XSerializer, responses=XSerializer) @api_view(['GET']) def view_func(request, format=None): pass # pragma: no cover schema = generate_schema('x', view_function=view_func) assert 'MetaPerson' in schema['components']['schemas'] assert 'MetaPerson' in schema['components']['schemas']['X']['properties']['field']['$ref'] def test_polymorphic_serializer_as_method_field_via_extend_schema_field(no_warnings): class XSerializer(serializers.Serializer): field = serializers.SerializerMethodField() @extend_schema_field( PolymorphicProxySerializer( component_name='MetaPerson', serializers=[LegalPersonSerializer, NaturalPersonSerializer], resource_type_field_name='type', ) ) def get_field(self, request): pass # pragma: no cover @extend_schema(request=XSerializer, responses=XSerializer) @api_view(['GET']) def view_func(request, format=None): pass # pragma: no cover schema = generate_schema('x', view_function=view_func) assert 'MetaPerson' in schema['components']['schemas'] assert schema['components']['schemas']['X'] == { 'type': 'object', 'properties': { 'field': {'allOf': [{'$ref': '#/components/schemas/MetaPerson'}], 'readOnly': True} }, 'required': ['field'] } def test_stripped_down_polymorphic_serializer(no_warnings): @extend_schema_field( PolymorphicProxySerializer( component_name='MetaPerson', serializers=[LegalPersonSerializer, NaturalPersonSerializer], resource_type_field_name=None, ) ) class XField(serializers.DictField): pass # pragma: no cover class XSerializer(serializers.Serializer): field = XField() @extend_schema(request=XSerializer, responses=XSerializer) @api_view(['GET']) def view_func(request, format=None): pass # pragma: no cover schema = generate_schema('x', view_function=view_func) assert schema['components']['schemas']['MetaPerson'] == {'oneOf': [ {'$ref': '#/components/schemas/LegalPerson'}, {'$ref': '#/components/schemas/NaturalPerson'} ]} @pytest.mark.parametrize('explicit', [True, False]) def test_many_polymorphic_serializer_extend_schema(no_warnings, explicit): if explicit: proxy_serializer = serializers.ListSerializer( child=PolymorphicProxySerializer(**PROXY_SERIALIZER_PARAMS) ) else: proxy_serializer = PolymorphicProxySerializer(**PROXY_SERIALIZER_PARAMS, many=True) @extend_schema(request=proxy_serializer, responses=proxy_serializer) @api_view(['POST']) def view_func(request, format=None): pass # pragma: no cover schema = generate_schema('/x/', view_function=view_func) assert 'MetaPerson' in schema['components']['schemas'] op = schema['paths']['/x/']['post'] assert get_response_schema(op) == { 'type': 'array', 'items': {'$ref': '#/components/schemas/MetaPerson'} } assert get_request_schema(op) == { 'type': 'array', 'items': {'$ref': '#/components/schemas/MetaPerson'} } @pytest.mark.parametrize('explicit', [True, False]) def test_many_polymorphic_proxy_serializer_extend_schema_field(no_warnings, explicit): if explicit: proxy_serializer = serializers.ListField( child=PolymorphicProxySerializer(**PROXY_SERIALIZER_PARAMS) ) else: proxy_serializer = PolymorphicProxySerializer(**PROXY_SERIALIZER_PARAMS, many=True) @extend_schema_field(proxy_serializer) class XField(serializers.DictField): pass # pragma: no cover class XSerializer(serializers.Serializer): field = XField() @extend_schema(request=XSerializer, responses=XSerializer) @api_view(['POST']) def view_func(request, format=None): pass # pragma: no cover schema = generate_schema('/x/', view_function=view_func) assert 'MetaPerson' in schema['components']['schemas'] assert schema['components']['schemas']['X'] == { 'type': 'object', 'properties': { 'field': {'type': 'array', 'items': {'$ref': '#/components/schemas/MetaPerson'}} }, 'required': ['field'] } op = schema['paths']['/x/']['post'] assert get_request_schema(op) == {'$ref': '#/components/schemas/X'} assert get_response_schema(op) == {'$ref': '#/components/schemas/X'} def test_polymorphic_proxy_serializer_misusage(no_warnings): with pytest.raises(AssertionError): PolymorphicProxySerializer(**PROXY_SERIALIZER_PARAMS).data with pytest.raises(AssertionError): PolymorphicProxySerializer(**PROXY_SERIALIZER_PARAMS).to_representation(None) with pytest.raises(AssertionError): PolymorphicProxySerializer(**PROXY_SERIALIZER_PARAMS).to_internal_value(None) @mock.patch('drf_spectacular.settings.spectacular_settings.COMPONENT_SPLIT_REQUEST', True) @pytest.mark.parametrize('explicit', [True, False]) def test_polymorphic_split_request_with_ro_serializer(no_warnings, explicit): class BasicPersonSerializer(serializers.ModelSerializer): type = serializers.SerializerMethodField() class Meta: model = NaturalPerson2 fields = ('id', 'type') def get_type(self, obj) -> str: return 'basic' if explicit: poly_proxy = PolymorphicProxySerializer( component_name='MetaPerson', serializers={'natural': NaturalPersonSerializer, 'basic': BasicPersonSerializer}, resource_type_field_name='type', ) else: poly_proxy = PolymorphicProxySerializer( component_name='MetaPerson', serializers=[NaturalPersonSerializer, BasicPersonSerializer], resource_type_field_name='type', ) @extend_schema(request=poly_proxy, responses=poly_proxy) @api_view(['POST']) def view_func(request, format=None): pass # pragma: no cover schema = generate_schema('x', view_function=view_func) components = schema['components']['schemas'] assert 'BasicPersonRequest' not in components assert components['MetaPerson']['oneOf'] == [ {'$ref': '#/components/schemas/NaturalPerson'}, {'$ref': '#/components/schemas/BasicPerson'} ] assert components['MetaPerson']['discriminator']['mapping'] == { 'natural': '#/components/schemas/NaturalPerson', 'basic': '#/components/schemas/BasicPerson' } assert components['MetaPersonRequest']['oneOf'] == [ {'$ref': '#/components/schemas/NaturalPersonRequest'}, ] assert components['MetaPersonRequest']['discriminator']['mapping'] == { 'natural': '#/components/schemas/NaturalPersonRequest', } def test_polymorphic_forced_many_false(no_warnings): class XViewSet(viewsets.GenericViewSet): @extend_schema( responses=PolymorphicProxySerializer( component_name='MetaPerson', serializers=[NaturalPersonSerializer, LegalPersonSerializer], resource_type_field_name='type', many=False ) ) def list(self, request, *args, **kwargs): return Response({}) # pragma: no cover schema = generate_schema('x', XViewSet) assert get_response_schema(schema['paths']['/x/']['get']) == { '$ref': '#/components/schemas/MetaPerson' } def test_polymorphic_manual_many(no_warnings): mixed_poly = PolymorphicProxySerializer( component_name='MetaLegalPerson', serializers=[NaturalPersonSerializer, NaturalPersonSerializer(many=True)], resource_type_field_name=None, many=False, ) class XViewSet(viewsets.GenericViewSet): @extend_schema(request=mixed_poly, responses=mixed_poly) def create(self, request, *args, **kwargs): return Response({}) # pragma: no cover @extend_schema(responses=mixed_poly) def list(self, request, *args, **kwargs): return Response({}) # pragma: no cover schema = generate_schema('x', XViewSet) response_schema = get_response_schema(schema['paths']['/x/']['post']) request_schema = get_request_schema(schema['paths']['/x/']['post']) assert response_schema == request_schema == {'$ref': '#/components/schemas/MetaLegalPerson'} assert schema['components']['schemas']['MetaLegalPerson'] == { 'oneOf': [ {'$ref': '#/components/schemas/NaturalPerson'}, {'type': 'array', 'items': {'$ref': '#/components/schemas/NaturalPerson'}} ] } def test_polymorphic_implicit_many_through_list_method_decoration(no_warnings): @extend_schema(responses=PolymorphicProxySerializer(**PROXY_SERIALIZER_PARAMS)) class XViewSet(viewsets.ReadOnlyModelViewSet): queryset = LegalPerson2.objects.none() serializer_class = LegalPersonSerializer schema = generate_schema('/x', XViewSet) assert get_response_schema(schema['paths']['/x/']['get']) == { 'items': {'$ref': '#/components/schemas/MetaPerson'}, 'type': 'array' } assert get_response_schema(schema['paths']['/x/{id}/']['get']) == { '$ref': '#/components/schemas/MetaPerson' } assert schema['components']['schemas']['MetaPerson'] == { 'discriminator': { 'mapping': { 'legal': '#/components/schemas/LegalPerson', 'natural': '#/components/schemas/NaturalPerson' }, 'propertyName': 'type' }, 'oneOf': [ {'$ref': '#/components/schemas/LegalPerson'}, {'$ref': '#/components/schemas/NaturalPerson'} ] } drf-spectacular-0.27.0/tests/test_polymorphic.yml000066400000000000000000000071521453572150400221630ustar00rootroot00000000000000openapi: 3.0.3 info: title: '' version: 0.0.0 paths: /persons/: post: operationId: persons_create tags: - persons requestBody: content: application/json: schema: $ref: '#/components/schemas/MetaPerson' application/x-www-form-urlencoded: schema: $ref: '#/components/schemas/MetaPerson' multipart/form-data: schema: $ref: '#/components/schemas/MetaPerson' security: - cookieAuth: [] - basicAuth: [] - {} responses: '201': content: application/json: schema: $ref: '#/components/schemas/MetaPerson' description: '' /persons/{id}/: patch: operationId: persons_partial_update parameters: - in: path name: id schema: type: integer required: true tags: - persons requestBody: content: application/json: schema: $ref: '#/components/schemas/PatchedMetaPerson' application/x-www-form-urlencoded: schema: $ref: '#/components/schemas/PatchedMetaPerson' multipart/form-data: schema: $ref: '#/components/schemas/PatchedMetaPerson' security: - cookieAuth: [] - basicAuth: [] - {} responses: '200': content: application/json: schema: $ref: '#/components/schemas/MetaPerson' description: '' components: schemas: LegalPerson: type: object properties: id: type: integer readOnly: true company_name: type: string maxLength: 30 type: type: string readOnly: true required: - company_name - id - type MetaPerson: oneOf: - $ref: '#/components/schemas/LegalPerson' - $ref: '#/components/schemas/NaturalPerson' discriminator: propertyName: type mapping: legal: '#/components/schemas/LegalPerson' natural: '#/components/schemas/NaturalPerson' NaturalPerson: type: object properties: id: type: integer readOnly: true first_name: type: string maxLength: 30 last_name: type: string maxLength: 30 type: type: string readOnly: true required: - first_name - id - last_name - type PatchedLegalPerson: type: object properties: id: type: integer readOnly: true company_name: type: string maxLength: 30 type: type: string readOnly: true PatchedMetaPerson: oneOf: - $ref: '#/components/schemas/PatchedLegalPerson' - $ref: '#/components/schemas/PatchedNaturalPerson' discriminator: propertyName: type mapping: legal: '#/components/schemas/PatchedLegalPerson' natural: '#/components/schemas/PatchedNaturalPerson' PatchedNaturalPerson: type: object properties: id: type: integer readOnly: true first_name: type: string maxLength: 30 last_name: type: string maxLength: 30 type: type: string readOnly: true securitySchemes: basicAuth: type: http scheme: basic cookieAuth: type: apiKey in: cookie name: sessionid drf-spectacular-0.27.0/tests/test_postprocessing.py000066400000000000000000000325401453572150400225260ustar00rootroot00000000000000import typing from enum import Enum from unittest import mock import pytest from django import __version__ as DJANGO_VERSION from django.utils.translation import gettext_lazy as _ from rest_framework import generics, mixins, serializers, viewsets from rest_framework.decorators import action from rest_framework.views import APIView try: from django.db.models.enums import IntegerChoices, TextChoices except ImportError: TextChoices = object # type: ignore # django < 3.0 handling IntegerChoices = object # type: ignore # django < 3.0 handling from drf_spectacular.plumbing import list_hash, load_enum_name_overrides from drf_spectacular.utils import OpenApiParameter, extend_schema from tests import assert_schema, generate_schema language_choices = ( ('en', 'en'), ('es', 'es'), ('ru', 'ru'), ('cn', 'cn'), ) blank_null_language_choices = ( ('en', 'en'), ('es', 'es'), ('ru', 'ru'), ('cn', 'cn'), ('', 'not provided'), (None, 'unknown'), ) vote_choices = ( (1, 'Positive'), (0, 'Neutral'), (-1, 'Negative'), ) language_list = ['en'] class LanguageEnum(Enum): EN = 'en' class LanguageStrEnum(str, Enum): EN = 'en' class LanguageChoices(TextChoices): EN = 'en' blank_null_language_list = ['en', '', None] class BlankNullLanguageEnum(Enum): EN = 'en' BLANK = '' NULL = None class BlankNullLanguageStrEnum(str, Enum): EN = 'en' BLANK = '' # These will still be included since the values get cast to strings so 'None' != None NULL = None if '3' < DJANGO_VERSION < '5': # Django 5 added a sanity check that prohibits None class BlankNullLanguageChoices(TextChoices): EN = 'en' BLANK = '' # These will still be included since the values get cast to strings so 'None' != None NULL = None class ASerializer(serializers.Serializer): language = serializers.ChoiceField(choices=language_choices) vote = serializers.ChoiceField(choices=vote_choices) class BSerializer(serializers.Serializer): language = serializers.ChoiceField(choices=language_choices, allow_blank=True, allow_null=True) class AViewset(mixins.ListModelMixin, viewsets.GenericViewSet): serializer_class = ASerializer @extend_schema(responses=BSerializer) @action(detail=False, serializer_class=BSerializer) def selection(self, request): pass # pragma: no cover def test_postprocessing(no_warnings): schema = generate_schema('a', AViewset) assert_schema(schema, 'tests/test_postprocessing.yml') @mock.patch( 'drf_spectacular.settings.spectacular_settings.ENUM_ADD_EXPLICIT_BLANK_NULL_CHOICE', False ) def test_no_blank_and_null_in_enum_choices(no_warnings): schema = generate_schema('a', AViewset) assert 'NullEnum' not in schema['components']['schemas'] assert 'BlankEnum' not in schema['components']['schemas'] assert 'oneOf' not in schema['components']['schemas']['B']['properties']['language'] @mock.patch('drf_spectacular.settings.spectacular_settings.ENUM_NAME_OVERRIDES', { 'LanguageEnum': 'tests.test_postprocessing.language_choices' }) def test_global_enum_naming_override(no_warnings, clear_caches): # the override will prevent the warning for multiple names class XSerializer(serializers.Serializer): foo = serializers.ChoiceField(choices=language_choices) bar = serializers.ChoiceField(choices=language_choices) class XView(generics.RetrieveAPIView): serializer_class = XSerializer schema = generate_schema('/x', view=XView) assert 'LanguageEnum' in schema['components']['schemas']['X']['properties']['foo']['$ref'] assert 'LanguageEnum' in schema['components']['schemas']['X']['properties']['bar']['$ref'] assert len(schema['components']['schemas']) == 2 @mock.patch('drf_spectacular.settings.spectacular_settings.ENUM_NAME_OVERRIDES', { 'LanguageEnum': 'tests.test_postprocessing.blank_null_language_choices' }) def test_global_enum_naming_override_with_blank_and_none(no_warnings, clear_caches): """Test that choices with blank values can still have their name overridden.""" class XSerializer(serializers.Serializer): foo = serializers.ChoiceField(choices=blank_null_language_choices) bar = serializers.ChoiceField(choices=blank_null_language_choices) class XView(generics.RetrieveAPIView): serializer_class = XSerializer schema = generate_schema('/x', view=XView) foo_data = schema['components']['schemas']['X']['properties']['foo'] bar_data = schema['components']['schemas']['X']['properties']['bar'] assert len(foo_data['oneOf']) == 3 assert len(bar_data['oneOf']) == 3 foo_ref_values = [ref_object['$ref'] for ref_object in foo_data['oneOf']] bar_ref_values = [ref_object['$ref'] for ref_object in bar_data['oneOf']] assert foo_ref_values == [ '#/components/schemas/LanguageEnum', '#/components/schemas/BlankEnum', '#/components/schemas/NullEnum' ] assert bar_ref_values == [ '#/components/schemas/LanguageEnum', '#/components/schemas/BlankEnum', '#/components/schemas/NullEnum' ] def test_enum_name_reuse_warning(capsys): class XSerializer(serializers.Serializer): foo = serializers.ChoiceField(choices=language_choices) bar = serializers.ChoiceField(choices=language_choices) class XView(generics.RetrieveAPIView): serializer_class = XSerializer generate_schema('/x', view=XView) assert 'encountered multiple names for the same choice set' in capsys.readouterr().err def test_enum_collision_without_override(capsys): class XSerializer(serializers.Serializer): foo = serializers.ChoiceField(choices=[('A', 'A')]) class YSerializer(serializers.Serializer): foo = serializers.ChoiceField(choices=[('A', 'A'), ('B', 'B')]) class ZSerializer(serializers.Serializer): foo = serializers.ChoiceField(choices=[('A', 'A'), ('B', 'B')]) class XAPIView(APIView): @extend_schema(responses=ZSerializer) def get(self, request): pass # pragma: no cover @extend_schema(request=XSerializer, responses=YSerializer) def post(self, request): pass # pragma: no cover generate_schema('x', view=XAPIView) assert 'enum naming encountered a non-optimally resolvable' in capsys.readouterr().err def test_resolvable_enum_collision(no_warnings): class XSerializer(serializers.Serializer): foo = serializers.ChoiceField(choices=[('A', 'A')]) class YSerializer(serializers.Serializer): foo = serializers.ChoiceField(choices=[('A', 'A'), ('B', 'B')]) class XAPIView(APIView): @extend_schema(request=XSerializer, responses=YSerializer) def post(self, request): pass # pragma: no cover schema = generate_schema('x', view=XAPIView) assert 'XFooEnum' in schema['components']['schemas'] assert 'YFooEnum' in schema['components']['schemas'] @mock.patch('drf_spectacular.settings.spectacular_settings.COMPONENT_SPLIT_PATCH', True) @mock.patch('drf_spectacular.settings.spectacular_settings.COMPONENT_SPLIT_REQUEST', True) def test_enum_resolvable_collision_with_patched_and_request_splits(): class XSerializer(serializers.Serializer): foo = serializers.ChoiceField(choices=[('A', 'A')]) class YSerializer(serializers.Serializer): foo = serializers.ChoiceField(choices=[('A', 'A'), ('B', 'B')]) class XViewset(viewsets.GenericViewSet): @extend_schema(request=XSerializer, responses=YSerializer) def create(self, request): pass # pragma: no cover @extend_schema( request=XSerializer, responses=YSerializer, parameters=[OpenApiParameter('id', int, OpenApiParameter.PATH)] ) def partial_update(self, request): pass # pragma: no cover schema = generate_schema('/x', XViewset) components = schema['components']['schemas'] assert 'XFooEnum' in components and 'YFooEnum' in components assert '/XFooEnum' in components['XRequest']['properties']['foo']['$ref'] assert '/XFooEnum' in components['PatchedXRequest']['properties']['foo']['$ref'] def test_enum_override_variations(no_warnings): enum_override_variations = [ ('language_list', [('en', 'en')]), ('LanguageEnum', [('en', 'EN')]), ('LanguageStrEnum', [('en', 'EN')]), ] if DJANGO_VERSION > '3': enum_override_variations += [ ('LanguageChoices', [('en', 'En')]), ('LanguageChoices.choices', [('en', 'En')]) ] for variation, expected_hashed_keys in enum_override_variations: with mock.patch( 'drf_spectacular.settings.spectacular_settings.ENUM_NAME_OVERRIDES', {'LanguageEnum': f'tests.test_postprocessing.{variation}'} ): load_enum_name_overrides.cache_clear() assert list_hash(expected_hashed_keys) in load_enum_name_overrides() def test_enum_override_variations_with_blank_and_null(no_warnings): enum_override_variations = [ ('blank_null_language_list', [('en', 'en')]), ('BlankNullLanguageEnum', [('en', 'EN')]), ('BlankNullLanguageStrEnum', [('en', 'EN'), ('None', 'NULL')]) ] if '3' < DJANGO_VERSION < '5': # Django 5 added a sanity check that prohibits None enum_override_variations += [ ('BlankNullLanguageChoices', [('en', 'En'), ('None', 'Null')]), ('BlankNullLanguageChoices.choices', [('en', 'En'), ('None', 'Null')]) ] for variation, expected_hashed_keys in enum_override_variations: with mock.patch( 'drf_spectacular.settings.spectacular_settings.ENUM_NAME_OVERRIDES', {'LanguageEnum': f'tests.test_postprocessing.{variation}'} ): load_enum_name_overrides.cache_clear() # Should match after None and blank strings are removed assert list_hash(expected_hashed_keys) in load_enum_name_overrides() @mock.patch('drf_spectacular.settings.spectacular_settings.ENUM_NAME_OVERRIDES', { 'LanguageEnum': 'tests.test_postprocessing.NOTEXISTING' }) def test_enum_override_loading_fail(capsys, clear_caches): load_enum_name_overrides() assert 'unable to load choice override for LanguageEnum' in capsys.readouterr().err @pytest.mark.skipif(DJANGO_VERSION < '3', reason='Not available before Django 3.0') def test_textchoice_annotation(no_warnings): class QualityChoices(TextChoices): GOOD = 'GOOD' BAD = 'BAD' class XSerializer(serializers.Serializer): quality_levels = serializers.SerializerMethodField() def get_quality_levels(self, obj) -> typing.List[QualityChoices]: return [QualityChoices.GOOD, QualityChoices.BAD] # pragma: no cover class XAPIView(APIView): @extend_schema(responses=XSerializer) def get(self, request): pass # pragma: no cover schema = generate_schema('x', view=XAPIView) assert 'QualityLevelsEnum' in schema['components']['schemas'] assert schema['components']['schemas']['X']['properties']['quality_levels'] == { 'type': 'array', 'items': {'$ref': '#/components/schemas/QualityLevelsEnum'}, 'readOnly': True } def test_uuid_choices(no_warnings): import uuid class XSerializer(serializers.Serializer): foo = serializers.ChoiceField( choices=[ (uuid.UUID('93d7527f-de3c-4a76-9cc2-5578675630d4'), 'baz'), (uuid.UUID('47a4b873-409e-4e43-81d5-fafc3faeb849'), 'bar') ] ) class XAPIView(APIView): @extend_schema(responses=XSerializer) def get(self, request): pass # pragma: no cover schema = generate_schema('x', view=XAPIView) assert 'FooEnum' in schema['components']['schemas'] assert schema['components']['schemas']['FooEnum']['enum'] == [ uuid.UUID('93d7527f-de3c-4a76-9cc2-5578675630d4'), uuid.UUID('47a4b873-409e-4e43-81d5-fafc3faeb849') ] @pytest.mark.skipif(DJANGO_VERSION < '3', reason='Not available before Django 3.0') def test_equal_choices_different_semantics(no_warnings): class Health(IntegerChoices): OK = 0 FAIL = 1 class Status(IntegerChoices): GREEN = 0 RED = 1 class Test(IntegerChoices): A = 0, _("test group A") B = 1, _("test group B") class XSerializer(serializers.Serializer): some_health = serializers.ChoiceField(choices=Health.choices) some_status = serializers.ChoiceField(choices=Status.choices) some_test = serializers.ChoiceField(choices=Test.choices) class XAPIView(APIView): @extend_schema(responses=XSerializer) def get(self, request): pass # pragma: no cover # This should not generate a warning even though the enum list is identical # in both Enums. We now also differentiate the Enums by their labels. schema = generate_schema('x', view=XAPIView) assert schema['components']['schemas']['SomeHealthEnum'] == { 'enum': [0, 1], 'type': 'integer', 'description': '* `0` - Ok\n* `1` - Fail' } assert schema['components']['schemas']['SomeStatusEnum'] == { 'enum': [0, 1], 'type': 'integer', 'description': '* `0` - Green\n* `1` - Red' } assert schema['components']['schemas']['SomeTestEnum'] == { 'enum': [0, 1], 'type': 'integer', 'description': '* `0` - test group A\n* `1` - test group B', } drf-spectacular-0.27.0/tests/test_postprocessing.yml000066400000000000000000000035361453572150400227020ustar00rootroot00000000000000openapi: 3.0.3 info: title: '' version: 0.0.0 paths: /a/: get: operationId: a_list tags: - a security: - cookieAuth: [] - basicAuth: [] - {} responses: '200': content: application/json: schema: type: array items: $ref: '#/components/schemas/A' description: '' /a/selection/: get: operationId: a_selection_retrieve tags: - a security: - cookieAuth: [] - basicAuth: [] - {} responses: '200': content: application/json: schema: $ref: '#/components/schemas/B' description: '' components: schemas: A: type: object properties: language: $ref: '#/components/schemas/LanguageEnum' vote: $ref: '#/components/schemas/VoteEnum' required: - language - vote B: type: object properties: language: nullable: true oneOf: - $ref: '#/components/schemas/LanguageEnum' - $ref: '#/components/schemas/BlankEnum' - $ref: '#/components/schemas/NullEnum' required: - language BlankEnum: enum: - '' LanguageEnum: enum: - en - es - ru - cn type: string description: |- * `en` - en * `es` - es * `ru` - ru * `cn` - cn NullEnum: enum: - null VoteEnum: enum: - 1 - 0 - -1 type: integer description: |- * `1` - Positive * `0` - Neutral * `-1` - Negative securitySchemes: basicAuth: type: http scheme: basic cookieAuth: type: apiKey in: cookie name: sessionid drf-spectacular-0.27.0/tests/test_recursion.py000066400000000000000000000027721453572150400214610ustar00rootroot00000000000000import uuid import pytest from django.db import models from rest_framework import mixins, serializers, viewsets from rest_framework.renderers import JSONRenderer from tests import assert_schema, generate_schema class TreeNode(models.Model): id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) label = models.TextField() parent = models.ForeignKey( 'TreeNode', null=True, blank=True, related_name='children', on_delete=models.DO_NOTHING ) class TreeNodeSerializer(serializers.ModelSerializer): class Meta: fields = ['id', 'label', 'parent', 'children'] model = TreeNode def get_fields(self): fields = super(TreeNodeSerializer, self).get_fields() fields['children'] = TreeNodeSerializer(many=True) return fields class TreeNodeViewset(mixins.RetrieveModelMixin, viewsets.GenericViewSet): serializer_class = TreeNodeSerializer queryset = TreeNode.objects.none() def test_recursion(no_warnings): assert_schema( generate_schema('nodes', TreeNodeViewset), 'tests/test_recursion.yml' ) @pytest.mark.django_db def test_model_setup_is_valid(): root = TreeNode(label='root') root.save() leaf1 = TreeNode(label='leaf1', parent=root) leaf1.save() leaf2 = TreeNode(label='leaf2', parent=root) leaf2.save() JSONRenderer().render( TreeNodeSerializer(root).data, accepted_media_type='application/json; indent=4' ).decode() drf-spectacular-0.27.0/tests/test_recursion.yml000066400000000000000000000022451453572150400216250ustar00rootroot00000000000000openapi: 3.0.3 info: title: '' version: 0.0.0 paths: /nodes/{id}/: get: operationId: nodes_retrieve parameters: - in: path name: id schema: type: string format: uuid description: A UUID string identifying this tree node. required: true tags: - nodes security: - cookieAuth: [] - basicAuth: [] - {} responses: '200': content: application/json: schema: $ref: '#/components/schemas/TreeNode' description: '' components: schemas: TreeNode: type: object properties: id: type: string format: uuid readOnly: true label: type: string parent: type: string format: uuid nullable: true children: type: array items: $ref: '#/components/schemas/TreeNode' required: - children - id - label securitySchemes: basicAuth: type: http scheme: basic cookieAuth: type: apiKey in: cookie name: sessionid drf-spectacular-0.27.0/tests/test_regressions.py000066400000000000000000003514461453572150400220200ustar00rootroot00000000000000import datetime import re import typing import uuid from decimal import Decimal from functools import partialmethod from unittest import mock import pytest from django import __version__ as DJANGO_VERSION from django.core import validators from django.db import models from django.db.models import fields from django.urls import path, re_path, register_converter from django.urls.converters import StringConverter from rest_framework import ( filters, generics, mixins, pagination, parsers, renderers, routers, serializers, views, viewsets, ) from rest_framework.authentication import BasicAuthentication, TokenAuthentication from rest_framework.decorators import action, api_view from rest_framework.views import APIView from drf_spectacular.extensions import OpenApiSerializerExtension from drf_spectacular.helpers import forced_singular_serializer from drf_spectacular.hooks import preprocess_exclude_path_format from drf_spectacular.openapi import AutoSchema from drf_spectacular.renderers import OpenApiJsonRenderer, OpenApiYamlRenderer from drf_spectacular.settings import IMPORT_STRINGS, SPECTACULAR_DEFAULTS from drf_spectacular.types import OpenApiTypes from drf_spectacular.utils import ( OpenApiExample, OpenApiParameter, OpenApiRequest, OpenApiResponse, extend_schema, extend_schema_field, extend_schema_serializer, extend_schema_view, inline_serializer, ) from tests import generate_schema, get_request_schema, get_response_schema, strip_int64_details from tests.models import SimpleModel, SimpleSerializer def test_primary_key_read_only_queryset_not_found(no_warnings): # the culprit - looks like a feature not a bug. # https://github.com/encode/django-rest-framework/blame/4d9f9eb192c5c1ffe4fa9210b90b9adbb00c3fdd/rest_framework/utils/field_mapping.py#L271 class M1(models.Model): pass # pragma: no cover class M2(models.Model): id = models.UUIDField() m1_r = models.ForeignKey(M1, on_delete=models.CASCADE) m1_rw = models.ForeignKey(M1, on_delete=models.CASCADE) class M2Serializer(serializers.ModelSerializer): class Meta: fields = ['m1_rw', 'm1_r'] read_only_fields = ['m1_r'] # this produces the bug model = M2 class M2Viewset(viewsets.ReadOnlyModelViewSet): serializer_class = M2Serializer queryset = M2.objects.none() schema = generate_schema('m2', M2Viewset) props = schema['components']['schemas']['M2']['properties'] assert props['m1_rw']['type'] == 'integer' assert props['m1_r']['type'] == 'integer' def test_multi_step_serializer_primary_key_related_field(no_warnings): class MA1(models.Model): id = models.UUIDField(primary_key=True) class MA2(models.Model): m1 = models.ForeignKey(MA1, on_delete=models.CASCADE) class MA3(models.Model): m2 = models.ForeignKey(MA2, on_delete=models.CASCADE) class M3Serializer(serializers.ModelSerializer): # this scenario looks explicitly at multi-step sources with read_only=True m1 = serializers.PrimaryKeyRelatedField(source='m2.m1', required=False, read_only=True) class Meta: fields = ['m1', 'm2'] model = MA3 class M3Viewset(viewsets.ReadOnlyModelViewSet): serializer_class = M3Serializer queryset = MA3.objects.none() schema = generate_schema('m3', M3Viewset) properties = schema['components']['schemas']['M3']['properties'] assert properties['m1']['format'] == 'uuid' assert properties['m2']['type'] == 'integer' def test_serializer_reverse_relations_including_read_only(no_warnings): class M5(models.Model): pass class M5One(models.Model): id = models.CharField(primary_key=True, max_length=10) field = models.OneToOneField(M5, on_delete=models.CASCADE) class M5Many(models.Model): id = models.UUIDField(primary_key=True, default=uuid.uuid4) field = models.ManyToManyField(M5) class M5Foreign(models.Model): id = models.FloatField(primary_key=True) field = models.ForeignKey(M5, on_delete=models.CASCADE) class XSerializer(serializers.ModelSerializer): m5foreign_set_explicit = serializers.PrimaryKeyRelatedField( many=True, source='m5foreign_set', queryset=M5Foreign.objects.all() ) m5foreign_set_ro = serializers.PrimaryKeyRelatedField( many=True, source='m5foreign_set', read_only=True, ) m5many_set_explicit = serializers.PrimaryKeyRelatedField( many=True, source='m5many_set', queryset=M5Many.objects.all() ) m5many_set_ro = serializers.PrimaryKeyRelatedField( many=True, source='m5many_set', read_only=True, ) m5one_ro = serializers.PrimaryKeyRelatedField( source='m5one', read_only=True, ) class Meta: model = M5 fields = [ 'm5many_set', 'm5many_set_explicit', 'm5many_set_ro', 'm5foreign_set', 'm5foreign_set_explicit', 'm5foreign_set_ro', 'm5one', 'm5one_ro', ] class TestViewSet(mixins.CreateModelMixin, viewsets.GenericViewSet): queryset = M5.objects.all() serializer_class = XSerializer schema = generate_schema('/x/', TestViewSet) properties = schema['components']['schemas']['X']['properties'] m5many_pk = {'type': 'string', 'format': 'uuid'} assert properties['m5many_set']['items'] == m5many_pk assert properties['m5many_set_ro']['items'] == m5many_pk assert properties['m5many_set_explicit']['items'] == m5many_pk m5foreign_pk = {'type': 'number', 'format': 'double'} assert properties['m5foreign_set']['items'] == m5foreign_pk assert properties['m5foreign_set_ro']['items'] == m5foreign_pk assert properties['m5foreign_set_explicit']['items'] == m5foreign_pk assert properties['m5one'] == {'type': 'string'} assert properties['m5one_ro'] == {'readOnly': True, 'type': 'string'} def test_serializer_forward_relations_including_read_only(no_warnings): class M6One(models.Model): id = models.CharField(primary_key=True, max_length=10) class M6Many(models.Model): id = models.UUIDField(primary_key=True, default=uuid.uuid4) class M6Foreign(models.Model): id = models.FloatField(primary_key=True) class M6(models.Model): field_one = models.OneToOneField(M6One, on_delete=models.CASCADE) field_many = models.ManyToManyField(M6Many) field_foreign = models.ForeignKey(M6Foreign, on_delete=models.CASCADE) class XSerializer(serializers.ModelSerializer): field_one_ro = serializers.PrimaryKeyRelatedField( source='field_one', read_only=True ) field_foreign_ro = serializers.PrimaryKeyRelatedField( source='field_foreign', read_only=True ) field_many_ro = serializers.PrimaryKeyRelatedField( source='field_many', read_only=True, many=True ) class Meta: model = M6 fields = [ 'field_one', 'field_one_ro', 'field_many', 'field_many_ro', 'field_foreign', 'field_foreign_ro', ] class TestViewSet(mixins.CreateModelMixin, viewsets.GenericViewSet): queryset = M6.objects.all() serializer_class = XSerializer schema = generate_schema('/x/', TestViewSet) properties = schema['components']['schemas']['X']['properties'] assert properties['field_one'] == {'type': 'string'} assert properties['field_one_ro'] == {'type': 'string', 'readOnly': True} assert properties['field_foreign'] == {'type': 'number', 'format': 'double'} assert properties['field_foreign_ro'] == {'type': 'number', 'format': 'double', 'readOnly': True} assert properties['field_many'] == {'type': 'array', 'items': {'type': 'string', 'format': 'uuid'}} assert properties['field_many_ro'] == { 'type': 'array', 'items': {'type': 'string', 'format': 'uuid'}, 'readOnly': True } def test_path_implicit_required(no_warnings): class M2Serializer(serializers.Serializer): pass # pragma: no cover class M2Viewset(viewsets.GenericViewSet): serializer_class = M2Serializer @extend_schema(parameters=[OpenApiParameter('id', str, 'path')]) def retrieve(self, request, *args, **kwargs): pass # pragma: no cover generate_schema('m2', M2Viewset) def test_free_form_responses(no_warnings): class XAPIView(APIView): @extend_schema(responses={200: OpenApiTypes.OBJECT}) def get(self, request): pass # pragma: no cover class YAPIView(APIView): @extend_schema(responses=OpenApiTypes.OBJECT) def get(self, request): pass # pragma: no cover generate_schema(None, patterns=[ re_path(r'^x$', XAPIView.as_view(), name='x'), re_path(r'^y$', YAPIView.as_view(), name='y'), ]) @mock.patch( target='drf_spectacular.settings.spectacular_settings.APPEND_COMPONENTS', new={'schemas': {'SomeExtraComponent': {'type': 'integer'}}} ) def test_append_extra_components(no_warnings): class XSerializer(serializers.Serializer): id = serializers.UUIDField() class XAPIView(APIView): @extend_schema(responses={200: XSerializer}) def get(self, request): pass # pragma: no cover schema = generate_schema(None, patterns=[ re_path(r'^x$', XAPIView.as_view(), name='x'), ]) assert len(schema['components']['schemas']) == 2 def test_serializer_retrieval_from_view(no_warnings): class UnusedSerializer(serializers.Serializer): pass # pragma: no cover class XSerializer(serializers.Serializer): id = serializers.UUIDField() class YSerializer(serializers.Serializer): id = serializers.UUIDField() class X1Viewset(mixins.ListModelMixin, viewsets.GenericViewSet): serializer_class = UnusedSerializer def get_serializer(self, *args, **kwargs): assert 'request' in kwargs['context'] return XSerializer(*args, **kwargs) class X2Viewset(mixins.ListModelMixin, viewsets.GenericViewSet): def get_serializer_class(self): return YSerializer router = routers.SimpleRouter() router.register('x1', X1Viewset, basename='x1') router.register('x2', X2Viewset, basename='x2') schema = generate_schema(None, patterns=router.urls) assert len(schema['components']['schemas']) == 2 assert 'Unused' not in schema['components']['schemas'] def test_retrieve_on_apiview_get(no_warnings): class XSerializer(serializers.Serializer): id = serializers.UUIDField() class XApiView(APIView): authentication_classes = [] @extend_schema( parameters=[OpenApiParameter('id', OpenApiTypes.INT, OpenApiParameter.PATH)], responses={200: XSerializer}, ) def get(self, request): pass # pragma: no cover schema = generate_schema('x', view=XApiView) operation = schema['paths']['/x']['get'] assert operation['operationId'] == 'x_retrieve' operation_schema = get_response_schema(operation) assert '$ref' in operation_schema and 'type' not in operation_schema def test_list_on_apiview_get(no_warnings): class XSerializer(serializers.Serializer): id = serializers.UUIDField() class XApiView(APIView): authentication_classes = [] @extend_schema( parameters=[OpenApiParameter('id', OpenApiTypes.INT, OpenApiParameter.PATH)], responses={200: XSerializer(many=True)}, ) def get(self, request): pass # pragma: no cover schema = generate_schema('x', view=XApiView) operation = schema['paths']['/x']['get'] assert operation['operationId'] == 'x_list' operation_schema = get_response_schema(operation) assert operation_schema['type'] == 'array' def test_multi_method_action(no_warnings): class DummySerializer(serializers.Serializer): id = serializers.UUIDField() class UpdateSerializer(serializers.Serializer): id = serializers.UUIDField() class CreateSerializer(serializers.Serializer): id = serializers.UUIDField() class XViewset(viewsets.GenericViewSet): serializer_class = DummySerializer # basic usage @extend_schema(request=UpdateSerializer, methods=['PUT']) @extend_schema(request=CreateSerializer, methods=['POST']) @action(detail=False, methods=['PUT', 'POST']) def multi(self, request, *args, **kwargs): pass # pragma: no cover # bolt-on decorator variation @extend_schema(request=CreateSerializer) @action(detail=False, methods=['POST']) def multi2(self, request, *args, **kwargs): pass # pragma: no cover @extend_schema(request=UpdateSerializer) @multi2.mapping.put def multi2put(self, request, *args, **kwargs): pass # pragma: no cover schema = generate_schema('x', XViewset) def get_req_body(s): return s['requestBody']['content']['application/json']['schema']['$ref'] assert get_req_body(schema['paths']['/x/multi/']['put']) == '#/components/schemas/Update' assert get_req_body(schema['paths']['/x/multi/']['post']) == '#/components/schemas/Create' assert get_req_body(schema['paths']['/x/multi2/']['put']) == '#/components/schemas/Update' assert get_req_body(schema['paths']['/x/multi2/']['post']) == '#/components/schemas/Create' def test_serializer_class_on_apiview(no_warnings): class XSerializer(serializers.Serializer): field = serializers.UUIDField() class XView(views.APIView): serializer_class = XSerializer # not supported by DRF but pick it up anyway def get(self, request): pass # pragma: no cover def post(self, request): pass # pragma: no cover schema = generate_schema('x', view=XView) comp = '#/components/schemas/X' assert get_response_schema(schema['paths']['/x']['get'])['$ref'] == comp assert get_response_schema(schema['paths']['/x']['post'])['$ref'] == comp assert schema['paths']['/x']['post']['requestBody']['content']['application/json']['schema']['$ref'] == comp def test_customized_list_serializer(): class X(models.Model): position = models.IntegerField() class XSerializer(serializers.ModelSerializer): class Meta: model = X fields = ("id", "position") class XListUpdateSerializer(serializers.ListSerializer): child = XSerializer() class XAPIView(generics.GenericAPIView): model = X serializer_class = XListUpdateSerializer def put(self, request, *args, **kwargs): pass # pragma: no cover schema = generate_schema('x', view=XAPIView) operation = schema['paths']['/x']['put'] comp = '#/components/schemas/X' assert get_request_schema(operation)['type'] == 'array' assert get_request_schema(operation)['items']['$ref'] == comp assert get_response_schema(operation)['type'] == 'array' assert get_response_schema(operation)['items']['$ref'] == comp assert operation['operationId'] == 'x_update' assert len(schema['components']['schemas']) == 1 and 'X' in schema['components']['schemas'] def test_api_view_decorator(no_warnings): @extend_schema(responses=OpenApiTypes.FLOAT) @api_view(['GET']) def pi(request): pass # pragma: no cover schema = generate_schema('x', view_function=pi) operation = schema['paths']['/x']['get'] assert get_response_schema(operation)['type'] == 'number' def test_api_view_decorator_multi(no_warnings): @extend_schema(request=OpenApiTypes.FLOAT, responses=OpenApiTypes.INT, methods=['POST']) @extend_schema(responses=OpenApiTypes.FLOAT, methods=['GET']) @api_view(['GET', 'POST']) def pi(request): pass # pragma: no cover schema = generate_schema('x', view_function=pi) operation = schema['paths']['/x']['get'] assert get_response_schema(operation)['type'] == 'number' operation = schema['paths']['/x']['post'] assert get_request_schema(operation)['type'] == 'number' assert get_response_schema(operation)['type'] == 'integer' def test_pk_and_no_id(no_warnings): class XModel(models.Model): id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) class YModel(models.Model): x = models.OneToOneField(XModel, primary_key=True, on_delete=models.CASCADE) class YSerializer(serializers.ModelSerializer): class Meta: model = YModel fields = '__all__' class YViewSet(viewsets.ReadOnlyModelViewSet): serializer_class = YSerializer queryset = YModel.objects.all() schema = generate_schema('y', YViewSet) assert schema['components']['schemas']['Y']['properties']['x']['format'] == 'uuid' @pytest.mark.parametrize('allowed', [None, ['json', 'NoRendererAvailable']]) def test_drf_format_suffix_parameter(no_warnings, allowed): from rest_framework.urlpatterns import format_suffix_patterns @extend_schema(responses=OpenApiTypes.FLOAT) @api_view(['GET']) def view_func(request, format=None): pass # pragma: no cover urlpatterns = [ path('pi/', view_func), path('pi/subpath', view_func), path('pick', view_func), ] urlpatterns = format_suffix_patterns(urlpatterns, allowed=allowed) schema = generate_schema(None, patterns=urlpatterns) # Only seven alternatives are created, as /pi/{format} would be # /pi/.json which is not supported. assert list(schema['paths'].keys()) == [ '/pi/', '/pi{format}', '/pi/subpath', '/pi/subpath{format}', '/pick', '/pick{format}', ] assert schema['paths']['/pi/']['get']['operationId'] == 'pi_retrieve' assert schema['paths']['/pi{format}']['get']['operationId'] == 'pi_formatted_retrieve' format_parameter = schema['paths']['/pi{format}']['get']['parameters'][0] assert format_parameter['name'] == 'format' assert format_parameter['required'] is True assert format_parameter['in'] == 'path' assert format_parameter['schema']['type'] == 'string' # When allowed is not specified, all of the default formats are possible. # Even if other values are provided, only the valid formats are possible. assert format_parameter['schema']['enum'] == ['.json'] @mock.patch( 'drf_spectacular.settings.spectacular_settings.PREPROCESSING_HOOKS', [preprocess_exclude_path_format] ) def test_drf_format_suffix_parameter_exclude(no_warnings): from rest_framework.urlpatterns import format_suffix_patterns @extend_schema(responses=OpenApiTypes.FLOAT) @api_view(['GET']) def view_func(request, format=None): pass # pragma: no cover urlpatterns = format_suffix_patterns([ path('pi', view_func), ]) schema = generate_schema(None, patterns=urlpatterns) assert list(schema['paths'].keys()) == ['/pi'] def test_regex_path_parameter_discovery(no_warnings): @extend_schema(responses=OpenApiTypes.FLOAT) @api_view(['GET']) def pi(request, foo): pass # pragma: no cover urlpatterns = [re_path(r'^/pi/', pi)] schema = generate_schema(None, patterns=urlpatterns) parameter = schema['paths']['/pi/{precision}']['get']['parameters'][0] assert parameter['name'] == 'precision' assert parameter['in'] == 'path' assert parameter['schema']['type'] == 'integer' def test_lib_serializer_naming_collision_resolution(no_warnings): """ parity test in tests.test_warnings.test_serializer_name_reuse """ def x_lib1(): class XSerializer(serializers.Serializer): x = serializers.UUIDField() return XSerializer def x_lib2(): class XSerializer(serializers.Serializer): x = serializers.IntegerField() return XSerializer x_lib1, x_lib2 = x_lib1(), x_lib2() class XAPIView(APIView): @extend_schema(request=x_lib1, responses=x_lib2) def post(self, request): pass # pragma: no cover class Lib2XSerializerRename(OpenApiSerializerExtension): target_class = x_lib2 # also accepts import strings def get_name(self): return 'RenamedLib2X' schema = generate_schema('x', view=XAPIView) operation = schema['paths']['/x']['post'] assert get_request_schema(operation)['$ref'] == '#/components/schemas/X' assert get_response_schema(operation)['$ref'] == '#/components/schemas/RenamedLib2X' def test_owned_serializer_naming_override_with_ref_name(no_warnings): def x_owned1(): class XSerializer(serializers.Serializer): x = serializers.UUIDField() return XSerializer def x_owned2(): class XSerializer(serializers.Serializer): x = serializers.IntegerField() class Meta: ref_name = 'Y' return XSerializer x_owned1, x_owned2 = x_owned1(), x_owned2() class XAPIView(APIView): @extend_schema(request=x_owned1, responses=x_owned2) def post(self, request): pass # pragma: no cover schema = generate_schema('x', view=XAPIView) operation = schema['paths']['/x']['post'] assert get_request_schema(operation)['$ref'] == '#/components/schemas/X' assert get_response_schema(operation)['$ref'] == '#/components/schemas/Y' def test_custom_model_field_from_typed_field(no_warnings): class CustomIntegerField(fields.IntegerField): pass # pragma: no cover class CustomTypedFieldModel(models.Model): custom_int_field = CustomIntegerField() class XSerializer(serializers.ModelSerializer): class Meta: model = CustomTypedFieldModel fields = '__all__' class XAPIView(APIView): @extend_schema(responses=XSerializer) def get(self, request): pass # pragma: no cover schema = generate_schema('x', view=XAPIView) component = schema['components']['schemas']['X'] assert component['properties']['custom_int_field']['type'] == 'integer' def test_custom_model_field_from_base_field(no_warnings): class CustomIntegerField(fields.Field): def get_internal_type(self): return 'IntegerField' class CustomBaseFieldModel(models.Model): custom_int_field = CustomIntegerField() class XSerializer(serializers.ModelSerializer): class Meta: model = CustomBaseFieldModel fields = '__all__' class XAPIView(APIView): @extend_schema(responses=XSerializer) def get(self, request): pass # pragma: no cover schema = generate_schema('x', view=XAPIView) component = schema['components']['schemas']['X'] assert component['properties']['custom_int_field']['type'] == 'integer' def test_follow_field_source_through_intermediate_property_or_function(no_warnings): class FieldSourceTraversalModel2(models.Model): x = models.IntegerField(choices=[(1, '1'), (2, '2')]) y = models.IntegerField(choices=[(1, '1'), (2, '2'), (3, '3')]) class FieldSourceTraversalModel1(models.Model): @property def prop(self) -> FieldSourceTraversalModel2: # property is required for traversal return # type: ignore # pragma: no cover def func(self) -> FieldSourceTraversalModel2: # property is required for traversal return # type: ignore # pragma: no cover class XSerializer(serializers.ModelSerializer): prop = serializers.ReadOnlyField(source='prop.x') func = serializers.ReadOnlyField(source='func.y') class Meta: model = FieldSourceTraversalModel1 fields = '__all__' class XAPIView(APIView): @extend_schema(responses=XSerializer) def get(self, request): pass # pragma: no cover # this checks if field type is correctly estimated AND field was initialized # with the model parameters (choices) schema = generate_schema('x', view=XAPIView) assert schema['components']['schemas']['X']['properties']['func']['readOnly'] is True assert schema['components']['schemas']['X']['properties']['prop']['readOnly'] is True assert 'enum' in schema['components']['schemas']['PropEnum'] assert 'enum' in schema['components']['schemas']['FuncEnum'] assert schema['components']['schemas']['PropEnum']['type'] == 'integer' assert schema['components']['schemas']['FuncEnum']['type'] == 'integer' def test_viewset_list_with_envelope(no_warnings): class XSerializer(serializers.Serializer): x = serializers.IntegerField() def enveloper(serializer_class, many): component_name = 'Enveloped{}{}'.format( serializer_class.__name__.replace("Serializer", ""), "List" if many else "", ) @extend_schema_serializer(many=False, component_name=component_name) class EnvelopeSerializer(serializers.Serializer): status = serializers.BooleanField() data = serializer_class(many=many) return EnvelopeSerializer class XViewset(mixins.ListModelMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet): @extend_schema(responses=enveloper(XSerializer, True)) def list(self, request, *args, **kwargs): return super().list(request, *args, **kwargs) # pragma: no cover @extend_schema( responses=enveloper(XSerializer, False), parameters=[OpenApiParameter('id', int, OpenApiParameter.PATH)], ) def retrieve(self, request, *args, **kwargs): return super().retrieve(request, *args, **kwargs) # pragma: no cover schema = generate_schema('x', viewset=XViewset) operation_list = schema['paths']['/x/']['get'] assert operation_list['operationId'] == 'x_list' assert get_response_schema(operation_list)['$ref'] == '#/components/schemas/EnvelopedXList' operation_retrieve = schema['paths']['/x/{id}/']['get'] assert operation_retrieve['operationId'] == 'x_retrieve' assert get_response_schema(operation_retrieve)['$ref'] == '#/components/schemas/EnvelopedX' @mock.patch('drf_spectacular.settings.spectacular_settings.COMPONENT_SPLIT_REQUEST', True) def test_component_split_request(): class XSerializer(serializers.Serializer): ro = serializers.IntegerField(read_only=True) rw = serializers.IntegerField() wo = serializers.IntegerField(write_only=True) @extend_schema(request=XSerializer, responses=XSerializer) @api_view(['POST']) def pi(request, format=None): pass # pragma: no cover schema = generate_schema('/x', view_function=pi) operation = schema['paths']['/x']['post'] assert get_response_schema(operation)['$ref'] == '#/components/schemas/X' assert get_request_schema(operation)['$ref'] == '#/components/schemas/XRequest' assert len(schema['components']['schemas']['X']['properties']) == 2 assert 'wo' not in schema['components']['schemas']['X']['properties'] assert len(schema['components']['schemas']['XRequest']['properties']) == 2 assert 'ro' not in schema['components']['schemas']['XRequest']['properties'] def test_list_api_view(no_warnings): class XSerializer(serializers.Serializer): id = serializers.IntegerField() class XView(generics.ListAPIView): serializer_class = XSerializer schema = generate_schema('/x', view=XView) operation = schema['paths']['/x']['get'] assert operation['operationId'] == 'x_list' assert get_response_schema(operation)['type'] == 'array' @mock.patch('drf_spectacular.settings.spectacular_settings.COMPONENT_SPLIT_REQUEST', True) def test_file_field_duality_on_split_request(no_warnings): class XSerializer(serializers.Serializer): file = serializers.FileField() class XView(generics.ListCreateAPIView): serializer_class = XSerializer parser_classes = [parsers.MultiPartParser] schema = generate_schema('/x', view=XView) assert get_response_schema( schema['paths']['/x']['get'] )['items']['$ref'] == '#/components/schemas/X' assert get_request_schema( schema['paths']['/x']['post'], content_type='multipart/form-data' )['$ref'] == '#/components/schemas/XRequest' assert schema['components']['schemas']['X']['properties']['file']['format'] == 'uri' assert schema['components']['schemas']['XRequest']['properties']['file']['format'] == 'binary' @mock.patch('drf_spectacular.settings.spectacular_settings.COMPONENT_SPLIT_REQUEST', True) def test_component_split_nested_ro_wo_serializer(no_warnings): class RoSerializer(serializers.Serializer): ro_field = serializers.IntegerField(read_only=True) class WoSerializer(serializers.Serializer): wo_field = serializers.IntegerField(write_only=True) class XSerializer(serializers.Serializer): ro = RoSerializer() wo = WoSerializer() class XView(generics.ListCreateAPIView): serializer_class = XSerializer schema = generate_schema('/x', view=XView) assert 'RoRequest' not in schema['components']['schemas'] assert 'Wo' not in schema['components']['schemas'] assert len(schema['components']['schemas']['X']['properties']) == 1 assert len(schema['components']['schemas']['XRequest']['properties']) == 1 @mock.patch('drf_spectacular.settings.spectacular_settings.COMPONENT_SPLIT_REQUEST', True) def test_component_split_nested_explicit_ro_wo_serializer(no_warnings): class NestedSerializer(serializers.Serializer): field = serializers.IntegerField() class XSerializer(serializers.Serializer): ro = NestedSerializer(read_only=True) wo = NestedSerializer(write_only=True, required=False) class XView(generics.ListCreateAPIView): serializer_class = XSerializer schema = generate_schema('/x', view=XView) assert 'NestedRequest' in schema['components']['schemas'] assert 'Nested' in schema['components']['schemas'] assert len(schema['components']['schemas']['X']['properties']) == 1 assert len(schema['components']['schemas']['XRequest']['properties']) == 1 def test_read_only_many_related_field(no_warnings): class ManyRelatedTargetModel(models.Model): field = models.IntegerField() class ManyRelatedModel(models.Model): field_m2m = models.ManyToManyField(ManyRelatedTargetModel) field_m2m_ro = models.ManyToManyField(ManyRelatedTargetModel) class XSerializer(serializers.ModelSerializer): class Meta: model = ManyRelatedModel fields = '__all__' read_only_fields = ['field_m2m_ro'] class XAPIView(APIView): @extend_schema(responses=XSerializer) def get(self, request): pass # pragma: no cover schema = generate_schema('x', view=XAPIView) properties = schema['components']['schemas']['X']['properties'] # readOnly only needed on outer object, not in items assert properties['field_m2m'] == {'type': 'array', 'items': {'type': 'integer'}} assert properties['field_m2m_ro'] == { 'type': 'array', 'items': {'type': 'integer'}, 'readOnly': True } def test_extension_subclass_discovery(no_warnings): from rest_framework.authentication import TokenAuthentication class CustomAuth(TokenAuthentication): pass class XSerializer(serializers.Serializer): field = serializers.IntegerField() class XAPIView(APIView): authentication_classes = [CustomAuth] @extend_schema(responses=XSerializer) def get(self, request): pass # pragma: no cover generate_schema('x', view=XAPIView) def test_extend_schema_no_req_no_res(no_warnings): class XAPIView(APIView): @extend_schema(request=None, responses=None) def post(self, request): pass # pragma: no cover schema = generate_schema('/x', view=XAPIView) operation = schema['paths']['/x']['post'] assert 'requestBody' not in operation assert len(operation['responses']['200']) == 1 assert 'description' in operation['responses']['200'] def test_extend_schema_field_exclusion(no_warnings): @extend_schema_field(None) class CustomField(serializers.IntegerField): pass # pragma: no cover class XSerializer(serializers.Serializer): id = serializers.IntegerField() hidden = CustomField() class XView(generics.CreateAPIView): serializer_class = XSerializer schema = generate_schema('/x', view=XView) assert 'hidden' not in schema['components']['schemas']['X']['properties'] def test_extend_schema_serializer_field_exclusion(no_warnings): @extend_schema_serializer(exclude_fields=['hidden1', 'hidden2']) class XSerializer(serializers.Serializer): integer = serializers.IntegerField() hidden1 = serializers.IntegerField() hidden2 = serializers.CharField() class XView(generics.ListCreateAPIView): serializer_class = XSerializer schema = generate_schema('/x', view=XView) assert 'integer' in schema['components']['schemas']['X']['properties'] assert 'hidden1' not in schema['components']['schemas']['X']['properties'] assert 'hidden2' not in schema['components']['schemas']['X']['properties'] def test_schema_contains_only_urlpatterns_first_match(no_warnings): class XSerializer(serializers.Serializer): integer = serializers.IntegerField() class XAPIView(APIView): @extend_schema(responses=XSerializer) def get(self, request): pass # pragma: no cover class YSerializer(serializers.Serializer): integer = serializers.DateTimeField() class YAPIView(APIView): @extend_schema(responses=YSerializer) def get(self, request): pass # pragma: no cover urlpatterns = [ path('api/x/', XAPIView.as_view()), # only first occurrence is used path('api/x/', YAPIView.as_view()), ] schema = generate_schema(None, patterns=urlpatterns) assert len(schema['components']['schemas']) == 1 assert 'X' in schema['components']['schemas'] operation = schema['paths']['/api/x/']['get'] assert '#/components/schemas/X' in get_response_schema(operation)['$ref'] def test_schema_contains_only_allowed_methods(no_warnings): class XSerializer(serializers.Serializer): integer = serializers.IntegerField() class X(models.Model): integer = models.IntegerField() class XAPIView(generics.ListCreateAPIView): model = X serializer_class = XSerializer urlpatterns = [ path('api/x/', XAPIView.as_view()), path('api/x1/', XAPIView.as_view(http_method_names=['post'])), ] schema = generate_schema(None, patterns=urlpatterns) assert sorted(schema['paths']['/api/x/'].keys()) == sorted(['get', 'post']) assert list(schema['paths']['/api/x1/'].keys()) == ['post'] assert 'X' in schema['components']['schemas'] def test_auto_schema_and_extend_parameters(no_warnings): class CustomAutoSchema(AutoSchema): def get_override_parameters(self): return [ OpenApiParameter("id", str, OpenApiParameter.PATH), OpenApiParameter("foo", str, deprecated=True), OpenApiParameter("bar", str), ] class XSerializer(serializers.Serializer): id = serializers.IntegerField() with mock.patch('rest_framework.settings.api_settings.DEFAULT_SCHEMA_CLASS', CustomAutoSchema): class XViewSet(viewsets.GenericViewSet): serializer_class = XSerializer @extend_schema(parameters=[OpenApiParameter("bar", int)]) def list(self, request, *args, **kwargs): pass # pragma: no cover schema = generate_schema('x', XViewSet) parameters = schema['paths']['/x/']['get']['parameters'] assert parameters[0]['name'] == 'bar' and parameters[0]['schema']['type'] == 'integer' assert parameters[1]['name'] == 'foo' and parameters[1]['schema']['type'] == 'string' assert parameters[1]['deprecated'] is True assert parameters[2]['name'] == 'id' def test_manually_set_auto_schema_with_extend_schema(no_warnings): class CustomSchema(AutoSchema): def get_override_parameters(self): if self.method.lower() == "delete": return [OpenApiParameter("custom_param", str)] return super().get_override_parameters() @extend_schema_view( list=extend_schema(summary="list summary"), destroy=extend_schema(summary="delete summary"), ) class XViewSet(mixins.ListModelMixin, mixins.DestroyModelMixin, viewsets.ViewSet): queryset = SimpleModel.objects.all() serializer_class = SimpleSerializer schema = CustomSchema() schema = generate_schema('x', XViewSet) assert schema['paths']['/x/']['get']['summary'] == 'list summary' assert schema['paths']['/x/{id}/']['delete']['summary'] == 'delete summary' assert schema['paths']['/x/{id}/']['delete']['parameters'][0]['name'] == 'custom_param' assert schema['paths']['/x/{id}/']['delete']['parameters'][1]['name'] == 'id' def test_list_serializer_with_field_child(): class XSerializer(serializers.Serializer): field = serializers.ListSerializer(child=serializers.IntegerField()) class XAPIView(views.APIView): serializer_class = XSerializer def post(self, request, *args, **kwargs): pass # pragma: no cover # assumption on Serializer functionality assert XSerializer({'field': [1, 2, 3]}).data['field'] == [1, 2, 3] schema = generate_schema('x', view=XAPIView) assert get_request_schema(schema['paths']['/x']['post'])['$ref'] == '#/components/schemas/X' assert get_response_schema(schema['paths']['/x']['post'])['$ref'] == '#/components/schemas/X' properties = schema['components']['schemas']['X']['properties'] assert properties['field']['type'] == 'array' assert properties['field']['items']['type'] == 'integer' def test_list_serializer_with_field_child_on_extend_schema(no_warnings): class XAPIView(APIView): @extend_schema( request=serializers.ListSerializer(child=serializers.IntegerField()), responses=serializers.ListSerializer(child=serializers.IntegerField()), ) def post(self, request): pass # pragma: no cover schema = generate_schema('x', view=XAPIView) req_schema = get_request_schema(schema['paths']['/x']['post']) res_schema = get_response_schema(schema['paths']['/x']['post']) for s in [req_schema, res_schema]: assert s['type'] == 'array' assert s['items']['type'] == 'integer' def test_list_serializer_with_pagination(no_warnings): class GenreSerializer(serializers.Serializer): genre = serializers.CharField() class XViewSet(viewsets.GenericViewSet): pagination_class = pagination.LimitOffsetPagination @extend_schema(responses=GenreSerializer(many=True)) @action(methods=["GET"], detail=False) def genre(self, request, *args, **kwargs): pass # pragma: no cover schema = generate_schema('/x', XViewSet) response = get_response_schema(schema['paths']['/x/genre/']['get']) assert response['$ref'] == '#/components/schemas/PaginatedGenreList' assert 'PaginatedGenreList' in schema['components']['schemas'] assert 'Genre' in schema['components']['schemas'] def test_inline_serializer(no_warnings): @extend_schema( responses=inline_serializer( name='InlineOneOffSerializer', fields={ 'char': serializers.CharField(), 'choice': serializers.ChoiceField(choices=(('A', 'A'), ('B', 'B'))), 'nested_inline': inline_serializer( name='NestedInlineOneOffSerializer', fields={ 'char': serializers.CharField(), 'int': serializers.IntegerField(), }, allow_null=True, ) } ) ) @api_view(['GET']) def one_off(request, foo): pass # pragma: no cover schema = generate_schema('x', view_function=one_off) assert get_response_schema(schema['paths']['/x']['get'])['$ref'] == ( '#/components/schemas/InlineOneOff' ) assert len(schema['components']['schemas']) == 3 one_off = schema['components']['schemas']['InlineOneOff'] one_off_nested = schema['components']['schemas']['NestedInlineOneOff'] assert len(one_off['properties']) == 3 assert one_off['properties']['nested_inline']['nullable'] is True assert one_off['properties']['nested_inline']['allOf'][0]['$ref'] == ( '#/components/schemas/NestedInlineOneOff' ) assert len(one_off_nested['properties']) == 2 @mock.patch('drf_spectacular.settings.spectacular_settings.CAMELIZE_NAMES', True) def test_camelize_names(no_warnings): @extend_schema(responses=OpenApiTypes.FLOAT) @api_view(['GET']) def view_func(request, format=None): pass # pragma: no cover schema = generate_schema('/multi/step/path//', view_function=view_func) operation = schema['paths']['/multi/step/path/{someName}/']['get'] assert operation['parameters'][0]['name'] == 'someName' assert operation['operationId'] == 'multiStepPathRetrieve' def test_mocked_request_with_get_queryset_get_serializer_class(no_warnings): class XViewset(viewsets.ReadOnlyModelViewSet): def get_serializer_class(self): assert not self.request.user.is_authenticated assert self.action in ['retrieve', 'list'] assert getattr(self, 'swagger_fake_view', False) # drf-yasg comp return SimpleSerializer def get_queryset(self): assert not self.request.user.is_authenticated assert self.request.method == 'GET' assert getattr(self, 'swagger_fake_view', False) # drf-yasg comp return SimpleModel.objects.none() generate_schema('x', XViewset) def test_queryset_filter_and_ordering_only_on_list(no_warnings): class XViewset(viewsets.ReadOnlyModelViewSet): queryset = SimpleModel.objects.none() serializer_class = SimpleSerializer filter_backends = (filters.SearchFilter, filters.OrderingFilter) schema = generate_schema('x', XViewset) list_parameters = schema['paths']['/x/']['get']['parameters'] assert len(list_parameters) == 2 assert list_parameters[0]['name'] == 'ordering' assert list_parameters[1]['name'] == 'search' retrieve_parameters = schema['paths']['/x/{id}/']['get']['parameters'] assert len(retrieve_parameters) == 1 assert retrieve_parameters[0]['name'] == 'id' def test_pagination(no_warnings): class XViewset(viewsets.ReadOnlyModelViewSet): queryset = SimpleModel.objects.none() serializer_class = SimpleSerializer pagination_class = pagination.LimitOffsetPagination schema = generate_schema('x', XViewset) # query params only on list retrieve_parameters = schema['paths']['/x/']['get']['parameters'] assert len(retrieve_parameters) == 2 assert retrieve_parameters[0]['name'] == 'limit' assert retrieve_parameters[1]['name'] == 'offset' # no query params on retrieve list_parameters = schema['paths']['/x/{id}/']['get']['parameters'] assert len(list_parameters) == 1 assert list_parameters[0]['name'] == 'id' # substituted component on list assert 'Simple' in schema['components']['schemas'] assert 'PaginatedSimpleList' in schema['components']['schemas'] substitution = schema['components']['schemas']['PaginatedSimpleList'] assert substitution['type'] == 'object' assert substitution['properties']['results']['items']['$ref'] == '#/components/schemas/Simple' def test_pagination_reusage(no_warnings): class XViewset(viewsets.ReadOnlyModelViewSet): queryset = SimpleModel.objects.all() serializer_class = SimpleSerializer pagination_class = pagination.LimitOffsetPagination @extend_schema(responses={'200': SimpleSerializer(many=True)}) @action(methods=['GET'], detail=False) def custom_action(self): pass # pragma: no cover class YViewset(XViewset): serializer_class = SimpleSerializer router = routers.SimpleRouter() router.register('x', XViewset, basename='x') router.register('y', YViewset, basename='y') generate_schema(None, patterns=router.urls) def test_pagination_disabled_on_action(no_warnings): class XViewset(viewsets.ReadOnlyModelViewSet): queryset = SimpleModel.objects.all() serializer_class = SimpleSerializer pagination_class = pagination.LimitOffsetPagination @extend_schema(responses={'200': SimpleSerializer(many=True)}) @action(methods=['GET'], detail=False, pagination_class=None) def custom_action(self): pass # pragma: no cover class YViewset(XViewset): serializer_class = SimpleSerializer schema = generate_schema('x', YViewset) assert 'PaginatedSimpleList' in get_response_schema(schema['paths']['/x/']['get'])['$ref'] assert 'Simple' in get_response_schema( schema['paths']['/x/custom_action/']['get'] )['items']['$ref'] @mock.patch( 'drf_spectacular.settings.spectacular_settings.SECURITY', [{'apiKeyAuth': []}] ) @mock.patch( 'drf_spectacular.settings.spectacular_settings.APPEND_COMPONENTS', {"securitySchemes": {"apiKeyAuth": {"type": "apiKey", "in": "header", "name": "Authorization"}}} ) def test_manual_security_method_addition(no_warnings): @extend_schema(responses=OpenApiTypes.FLOAT) @api_view(['GET']) def view_func(request, format=None): pass # pragma: no cover schema = generate_schema('/x/', view_function=view_func) operation_security = schema['paths']['/x/']['get']['security'] schema_security = schema['components']['securitySchemes'] assert len(operation_security) == 4 and any(['apiKeyAuth' in os for os in operation_security]) assert len(schema_security) == 3 and 'apiKeyAuth' in schema_security def test_basic_viewset_without_queryset_with_explicit_pk_typing(no_warnings): class XSerializer(serializers.Serializer): field = serializers.IntegerField() class XViewset(viewsets.ViewSet): serializer_class = XSerializer def retrieve(self, request, *args, **kwargs): pass # pragma: no cover urlpatterns = [ path("api///", XViewset.as_view({"get": "retrieve"})) ] schema = generate_schema(None, patterns=urlpatterns) operation = schema['paths']['/api/{some_var}/{id}/']['get'] assert operation['parameters'][0]['name'] == 'id' assert operation['parameters'][0]['schema']['format'] == 'uuid' def test_multiple_media_types(no_warnings): @extend_schema(responses={ (200, 'application/json'): OpenApiTypes.OBJECT, (200, 'application/pdf'): OpenApiTypes.BINARY, }) class XAPIView(APIView): def get(self, request): pass # pragma: no cover schema = generate_schema('x', view=XAPIView) content = schema['paths']['/x']['get']['responses']['200']['content'] assert content['application/pdf']['schema']['format'] == 'binary' assert content['application/json']['schema']['type'] == 'object' def test_token_auth_with_bearer_keyword(no_warnings): class CustomTokenAuthentication(TokenAuthentication): keyword = 'Bearer' @extend_schema(responses=OpenApiTypes.FLOAT) @api_view(['GET']) def view_func(request, format=None): pass # pragma: no cover view_func.cls.authentication_classes = [CustomTokenAuthentication] schema = generate_schema('x', view_function=view_func) assert schema['components']['securitySchemes']['tokenAuth']['scheme'] == 'bearer' @pytest.mark.parametrize('responses', [ str, OpenApiTypes.STR, {'200': str}, {'200': OpenApiTypes.STR}, ]) def test_string_response_variations(no_warnings, responses): @extend_schema(responses=responses) @api_view(['GET']) def view_func(request, format=None): pass # pragma: no cover schema = generate_schema('x', view_function=view_func) assert get_response_schema(schema['paths']['/x']['get'])['type'] == 'string' def test_exclude_discovered_parameter(no_warnings): @extend_schema_view(list=extend_schema(parameters=[ # keep 'offset', remove 'limit', and add 'random' OpenApiParameter('limit', exclude=True), OpenApiParameter('random', bool), ])) class XViewset(viewsets.ReadOnlyModelViewSet): queryset = SimpleModel.objects.all() serializer_class = SimpleSerializer pagination_class = pagination.LimitOffsetPagination schema = generate_schema('x', XViewset) parameters = schema['paths']['/x/']['get']['parameters'] assert len(parameters) == 2 assert parameters[0]['name'] == 'offset' assert parameters[1]['name'] == 'random' def test_exclude_parameter_from_customized_autoschema(no_warnings): class CustomSchema(AutoSchema): def get_override_parameters(self): return [OpenApiParameter('test')] @extend_schema_view(list=extend_schema(parameters=[ OpenApiParameter('test', exclude=True), # exclude from class override OpenApiParameter('never_existed', exclude=True), # provoke error OpenApiParameter('keep', bool), # for sanity check ])) class XViewset(viewsets.ReadOnlyModelViewSet): queryset = SimpleModel.objects.all() serializer_class = SimpleSerializer schema = CustomSchema() schema = generate_schema('x', XViewset) parameters = schema['paths']['/x/']['get']['parameters'] assert len(parameters) == 1 assert parameters[0]['name'] == 'keep' def test_manual_decimal_validator(): # manually test this validator as it is not part of the default workflow class XSerializer(serializers.Serializer): field = serializers.FloatField( validators=[validators.DecimalValidator(max_digits=4, decimal_places=2)] ) @extend_schema(request=XSerializer, responses=XSerializer) @api_view(['POST']) def view_func(request, format=None): pass # pragma: no cover schema = generate_schema('x', view_function=view_func) field = schema['components']['schemas']['X']['properties']['field'] assert field['maximum'] == 100 assert field['minimum'] == -100 def test_serialization_with_decimal_values(no_warnings): class XSerializer(serializers.Serializer): field = serializers.DecimalField( decimal_places=2, min_value=Decimal('1'), max_value=Decimal('100.00'), max_digits=5, coerce_to_string=False, ) field_coerced = serializers.DecimalField( decimal_places=2, min_value=Decimal('1'), max_value=Decimal('100.00'), max_digits=5, coerce_to_string=True, ) @extend_schema(responses=XSerializer) @api_view(['GET']) def view_func(request): pass # pragma: no cover schema = generate_schema('/x/', view_function=view_func) assert schema['components']['schemas']['X']['properties']['field'] == { 'type': 'number', 'format': 'double', 'maximum': Decimal('100.00'), 'minimum': Decimal('1'), } assert schema['components']['schemas']['X']['properties']['field_coerced'] == { 'type': 'string', 'format': 'decimal', 'pattern': r'^-?\d{0,3}(?:\.\d{0,2})?$', } schema_yml = OpenApiYamlRenderer().render(schema, renderer_context={}) assert b'maximum: 100.00\n' in schema_yml assert b'minimum: 1\n' in schema_yml def test_non_supported_http_verbs(no_warnings): HTTP_METHODS = [ 'GET', 'HEAD', 'POST', 'PUT', 'DELETE', 'CONNECT', 'OPTIONS', 'TRACE', 'PATCH' ] for x in HTTP_METHODS: @extend_schema(request=int, responses=int) @api_view([x]) def view_func(request, format=None): pass # pragma: no cover generate_schema('x', view_function=view_func) def test_nested_ro_serializer_has_required_fields_on_patch(no_warnings): # issue #249 raised a disparity problem between serializer name # generation and the actual serializer construction on PATCH. class XSerializer(serializers.Serializer): field = serializers.CharField() class YSerializer(serializers.Serializer): x_field = XSerializer(read_only=True) class YViewSet(viewsets.GenericViewSet): serializer_class = YSerializer queryset = SimpleModel.objects.all() def partial_update(self, request, *args, **kwargs): pass # pragma: no cover schema = generate_schema('x', YViewSet) assert len(schema['components']['schemas']) == 3 assert 'Y' in schema['components']['schemas'] assert 'PatchedY' in schema['components']['schemas'] assert 'required' in schema['components']['schemas']['X'] class M3(models.Model): """ test_path_param_from_related_model_pk_without_primary_key_true """ related_field = models.ForeignKey(SimpleModel, on_delete=models.PROTECT, editable=False) many_related = models.ManyToManyField(SimpleModel, related_name='+') @pytest.mark.parametrize('path', [ r'x/(?P[0-9a-f-]{36})', r'x/', ]) def test_path_param_from_related_model_pk_without_primary_key_true(no_warnings, path): class M3Serializer(serializers.ModelSerializer): class Meta: fields = '__all__' model = M3 class XViewset(viewsets.ModelViewSet): serializer_class = M3Serializer queryset = M3.objects.none() router = routers.SimpleRouter() router.register(path, XViewset) schema = generate_schema(None, patterns=router.urls) assert '/x/{related_field}/' in schema['paths'] assert '/x/{related_field}/{id}/' in schema['paths'] def test_path_parameter_with_relationships(no_warnings): class PathParamParent(models.Model): pass class PathParamChild(models.Model): parent = models.ForeignKey(PathParamParent, on_delete=models.CASCADE) class PathParamGrandChild(models.Model): parent = models.ForeignKey(PathParamChild, on_delete=models.CASCADE) class PathParamChildSerializer(serializers.ModelSerializer): class Meta: fields = '__all__' model = PathParamChild class XViewset1(viewsets.ModelViewSet): serializer_class = PathParamChildSerializer queryset = PathParamChild.objects.none() lookup_field = 'id' class XViewset2(viewsets.ModelViewSet): serializer_class = PathParamChildSerializer queryset = PathParamChild.objects.none() lookup_field = 'parent' class XViewset3(viewsets.ModelViewSet): serializer_class = PathParamChildSerializer queryset = PathParamChild.objects.none() lookup_field = 'parent__id' # Functionally the same as above class PathParamGrandChildSerializer(serializers.ModelSerializer): class Meta: fields = '__all__' model = PathParamGrandChild class XViewset4(viewsets.ModelViewSet): serializer_class = PathParamGrandChildSerializer queryset = PathParamGrandChild.objects.none() lookup_field = 'parent__parent' class XViewset5(viewsets.ModelViewSet): serializer_class = PathParamGrandChildSerializer queryset = PathParamGrandChild.objects.none() lookup_field = 'parent__parent__id' router = routers.SimpleRouter() router.register('child_by_id', XViewset1) router.register('child_by_parent_id', XViewset2) router.register('child_by_parent_id_alt', XViewset3) router.register('grand_child_by_grand_parent_id', XViewset4) router.register('grand_child_by_grand_parent_id_alt', XViewset5) schema = generate_schema(None, patterns=router.urls) # Basic cases: assert schema['paths']['/child_by_id/{id}/']['get']['parameters'][0] == { 'description': 'A unique integer value identifying this path param child.', 'in': 'path', 'name': 'id', 'schema': {'type': 'integer'}, 'required': True } assert schema['paths']['/child_by_parent_id/{parent}/']['get']['parameters'][0] == { 'in': 'path', 'name': 'parent', 'schema': {'type': 'integer'}, 'required': True } # Can we traverse relationships? assert schema['paths']['/grand_child_by_grand_parent_id/{parent__parent}/']['get']['parameters'][0] == { 'in': 'path', 'name': 'parent__parent', 'schema': {'type': 'integer'}, 'required': True } # Explicit `__id` handling: assert schema['paths']['/grand_child_by_grand_parent_id_alt/{parent__parent__id}/']['get']['parameters'][0] == { 'description': 'A unique integer value identifying this path param grand child.', 'in': 'path', 'name': 'parent__parent__id', 'schema': {'type': 'integer'}, 'required': True } assert schema['paths']['/child_by_parent_id_alt/{parent__id}/']['get']['parameters'][0] == { 'description': 'A unique integer value identifying this path param child.', 'in': 'path', 'name': 'parent__id', 'schema': {'type': 'integer'}, 'required': True } def test_path_parameter_with_lookup_field(no_warnings): class JournalEntry(models.Model): recorded_at = models.DateTimeField() class JournalEntrySerializer(serializers.ModelSerializer): class Meta: fields = '__all__' model = JournalEntry class JournalEntryViewset(viewsets.ModelViewSet): serializer_class = JournalEntrySerializer queryset = JournalEntry.objects.none() lookup_field = 'recorded_at__date' class JournalEntryAltViewset(viewsets.ModelViewSet): serializer_class = JournalEntrySerializer queryset = JournalEntry.objects.none() lookup_field = 'recorded_at__date' lookup_url_kwarg = 'on' router = routers.SimpleRouter() router.register('journal', JournalEntryViewset) router.register('journal_alt', JournalEntryAltViewset) schema = generate_schema(None, patterns=router.urls) # TODO this is not 100% correct since "__date" transforms datetime to date, # but most SQL modifiers don't change the type and we will tolerate that # slight problem for now. assert schema['paths']['/journal/{recorded_at__date}/']['get']['parameters'][0] == { 'in': 'path', 'name': 'recorded_at__date', 'required': True, 'schema': {'format': 'date-time', 'type': 'string'}, } assert schema['paths']['/journal_alt/{on}/']['get']['parameters'][0] == { 'in': 'path', 'name': 'on', 'required': True, 'schema': {'format': 'date-time', 'type': 'string'}, } @pytest.mark.contrib('psycopg2') def test_multiple_choice_enum(no_warnings): from django.contrib.postgres.fields import ArrayField class M4(models.Model): multi = ArrayField( models.CharField(max_length=10, choices=(('A', 'A'), ('B', 'B'))), size=8, ) class M4Serializer(serializers.ModelSerializer): class Meta: fields = '__all__' model = M4 class XViewset(viewsets.ModelViewSet): serializer_class = M4Serializer queryset = M4.objects.none() schema = generate_schema('x', XViewset) assert 'MultiEnum' in schema['components']['schemas'] prop = schema['components']['schemas']['M4']['properties']['multi'] assert prop['type'] == 'array' assert prop['items']['$ref'] == '#/components/schemas/MultiEnum' def test_explode_style_parameter_with_custom_schema(no_warnings): @extend_schema( parameters=[OpenApiParameter( name='bbox', type={'type': 'array', 'minItems': 4, 'maxItems': 6, 'items': {'type': 'number'}}, location=OpenApiParameter.QUERY, required=False, style='form', explode=False, )], responses=OpenApiTypes.OBJECT, ) @api_view(['GET']) def view_func(request, format=None): pass # pragma: no cover schema = generate_schema('/x/', view_function=view_func) parameter = schema['paths']['/x/']['get']['parameters'][0] assert 'explode' in parameter assert 'style' in parameter assert parameter['schema']['type'] == 'array' def test_incorrect_foreignkey_type_on_readonly_field(no_warnings): class ReferencingModel(models.Model): id = models.UUIDField(primary_key=True) referenced_model = models.ForeignKey(SimpleModel, on_delete=models.CASCADE) referenced_model_ro = models.ForeignKey(SimpleModel, on_delete=models.CASCADE) referenced_model_m2m = models.ManyToManyField(SimpleModel) referenced_model_m2m_ro = models.ManyToManyField(SimpleModel) class ReferencingModelSerializer(serializers.ModelSerializer): indirect_referenced_model_ro = serializers.PrimaryKeyRelatedField( source='referenced_model', read_only=True, ) class Meta: fields = '__all__' read_only_fields = ['id', 'referenced_model_ro', 'referenced_model_m2m_ro'] model = ReferencingModel class ReferencingModelViewset(viewsets.ModelViewSet): serializer_class = ReferencingModelSerializer queryset = ReferencingModel.objects.all() schema = generate_schema('/x/', ReferencingModelViewset) properties = schema['components']['schemas']['ReferencingModel']['properties'] assert properties['referenced_model']['type'] == 'integer' assert properties['referenced_model_ro']['type'] == 'integer' assert properties['referenced_model_m2m']['items']['type'] == 'integer' assert properties['referenced_model_m2m_ro']['items']['type'] == 'integer' assert properties['indirect_referenced_model_ro']['type'] == 'integer' @pytest.mark.parametrize(['sorting', 'result'], [ (True, ['a', 'b', 'c']), (False, ['b', 'c', 'a']), (lambda x: (x['in'], x['name']), ['b', 'a', 'c']), ]) def test_parameter_sorting_setting(no_warnings, sorting, result): @extend_schema( parameters=[OpenApiParameter('b', str, 'header'), OpenApiParameter('c'), OpenApiParameter('a')], responses=OpenApiTypes.FLOAT ) @api_view(['GET']) def view_func(request, format=None): pass # pragma: no cover with mock.patch( 'drf_spectacular.settings.spectacular_settings.SORT_OPERATION_PARAMETERS', sorting ): schema = generate_schema('/x/', view_function=view_func) parameters = schema['paths']['/x/']['get']['parameters'] assert [p['name'] for p in parameters] == result @pytest.mark.parametrize(['sorting', 'result'], [ (True, ['/a/', '/b/', '/c/']), (False, ['/c/', '/a/', '/b/']), (lambda x: {'/c/': 1, '/b/': 2, '/a/': 3}.get(x[0]), ['/c/', '/b/', '/a/']), ]) def test_operation_sorting_setting(no_warnings, sorting, result): @extend_schema(responses=OpenApiTypes.ANY) @api_view(['GET']) def view_func(request, format=None): pass # pragma: no cover urlpatterns = [ path('/c/', view_func), path('/a/', view_func), path('/b/', view_func), ] with mock.patch( 'drf_spectacular.settings.spectacular_settings.SORT_OPERATIONS', sorting ): schema = generate_schema(None, patterns=urlpatterns) assert list(schema['paths'].keys()) == result def test_response_headers_without_response_body(no_warnings): @extend_schema( responses={301: None}, tags=["Registration"], parameters=[ OpenApiParameter( name="Location", type=OpenApiTypes.URI, location=OpenApiParameter.HEADER, description="/", response=[301] ) ] ) @api_view(['GET']) def view_func(request, format=None): pass # pragma: no cover schema = generate_schema('/x/', view_function=view_func) assert 'Location' in schema['paths']['/x/']['get']['responses']['301']['headers'] assert 'content' not in schema['paths']['/x/']['get']['responses']['301'] def test_customized_parsers_and_renderers_on_viewset(no_warnings): class XViewset(viewsets.ModelViewSet): serializer_class = SimpleSerializer queryset = SimpleModel.objects.none() parser_classes = [parsers.MultiPartParser] def get_renderers(self): if self.action == 'json_in_multi_out': return [renderers.MultiPartRenderer()] else: return [renderers.HTMLFormRenderer()] @action(methods=['POST'], detail=False, parser_classes=[parsers.JSONParser]) def json_in_multi_out(self, request): pass # pragma: no cover schema = generate_schema('x', XViewset) create_op = schema['paths']['/x/']['post'] assert len(create_op['requestBody']['content']) == 1 assert 'multipart/form-data' in create_op['requestBody']['content'] assert len(create_op['responses']['201']['content']) == 1 assert 'text/html' in create_op['responses']['201']['content'] action_op = schema['paths']['/x/json_in_multi_out/']['post'] assert len(action_op['requestBody']['content']) == 1 assert 'application/json' in action_op['requestBody']['content'] assert len(action_op['responses']['200']['content']) == 1 assert 'multipart/form-data' in action_op['responses']['200']['content'] def test_technically_unnecessary_serializer_patch(no_warnings): # ideally this extend_schema would not be necessary @extend_schema_view(delete=extend_schema(responses=None)) class XAPIView(generics.DestroyAPIView): queryset = SimpleModel.objects.none() schema = generate_schema('/x/', view=XAPIView) assert 'No response' in schema['paths']['/x/']['delete']['responses']['204']['description'] def test_any_placeholder_on_request_response(no_warnings): @extend_schema_field(OpenApiTypes.ANY) class CustomField(serializers.IntegerField): pass # pragma: no cover class XSerializer(serializers.Serializer): method_field = serializers.SerializerMethodField(help_text='Any') custom_field = CustomField() def get_method_field(self, obj) -> typing.Any: return # pragma: no cover @extend_schema(request=typing.Any, responses=XSerializer) @api_view(['POST']) def view_func(request, format=None): pass # pragma: no cover schema = generate_schema('/x/', view_function=view_func) assert get_request_schema(schema['paths']['/x/']['post']) == {} properties = schema['components']['schemas']['X']['properties'] assert properties['custom_field'] == {} assert properties['method_field'] == {'readOnly': True, 'description': 'Any'} def test_categorized_choices(no_warnings, clear_caches): media_choices = [ ('Audio', (('vinyl', 'Vinyl'), ('cd', 'CD'))), ('Video', (('vhs', 'VHS Tape'), ('dvd', 'DVD'))), ('unknown', 'Unknown'), ] media_choices_audio = [ ('Audio', (('vinyl', 'Vinyl'), ('cd', 'CD'))), ('unknown', 'Unknown'), ] class M9(models.Model): cat_choice = models.CharField(max_length=10, choices=media_choices) class M9Serializer(serializers.ModelSerializer): audio_choice = serializers.ChoiceField(choices=media_choices_audio) class Meta: fields = '__all__' model = M9 class XViewset(viewsets.ModelViewSet): serializer_class = M9Serializer queryset = M9.objects.none() with mock.patch( 'drf_spectacular.settings.spectacular_settings.ENUM_NAME_OVERRIDES', {'MediaEnum': media_choices} ): schema = generate_schema('x', XViewset) # test base functionality of flattening categories assert schema['components']['schemas']['AudioChoiceEnum']['enum'] == [ 'vinyl', 'cd', 'unknown' ] # test override match works synchronously assert schema['components']['schemas']['MediaEnum']['enum'] == [ 'vinyl', 'cd', 'vhs', 'dvd', 'unknown' ] @mock.patch('drf_spectacular.settings.spectacular_settings.SCHEMA_PATH_PREFIX', '/api/v[0-9]/') @mock.patch('drf_spectacular.settings.spectacular_settings.SCHEMA_PATH_PREFIX_TRIM', True) def test_schema_path_prefix_trim(no_warnings): @extend_schema(request=typing.Any, responses=typing.Any) @api_view(['POST']) def view_func(request, format=None): pass # pragma: no cover schema = generate_schema('/api/v1/x/', view_function=view_func) assert '/x/' in schema['paths'] def test_nameless_root_endpoint(no_warnings): @extend_schema(request=typing.Any, responses=typing.Any) @api_view(['POST']) def view_func(request, format=None): pass # pragma: no cover schema = generate_schema('/', view_function=view_func) assert schema['paths']['/']['post']['operationId'] == 'root_create' def test_list_and_pagination_on_non_2XX_schemas(no_warnings): @extend_schema_view( list=extend_schema(responses={ 200: SimpleSerializer, 400: {'type': 'object', 'properties': {'code': {'type': 'string'}}}, 403: OpenApiTypes.OBJECT }) ) class XViewset(mixins.ListModelMixin, viewsets.GenericViewSet): serializer_class = SimpleSerializer queryset = SimpleModel.objects.none() pagination_class = pagination.LimitOffsetPagination schema = generate_schema('x', XViewset) assert get_response_schema(schema['paths']['/x/']['get']) == { '$ref': '#/components/schemas/PaginatedSimpleList' } assert get_response_schema(schema['paths']['/x/']['get'], '400') == { 'type': 'object', 'properties': {'code': {'type': 'string'}} } assert get_response_schema(schema['paths']['/x/']['get'], '403') == { 'type': 'object', 'additionalProperties': {} } def test_openapi_response_wrapper(no_warnings): @extend_schema_view( create=extend_schema(description='creation description', responses={ 201: OpenApiResponse(response=int, description='creation with int response.'), 222: OpenApiResponse(description='creation with no response.'), 223: None, 224: int, }), list=extend_schema(responses=OpenApiResponse( response=OpenApiTypes.INT, description='a list that actually returns numbers', examples=[OpenApiExample('One', 1), OpenApiExample('Two', 2)], )), ) class XViewset(viewsets.ModelViewSet): serializer_class = SimpleSerializer queryset = SimpleModel.objects.none() schema = generate_schema('/x', XViewset) assert schema['paths']['/x/']['get']['responses'] == { '200': { 'content': { 'application/json': { 'schema': {'type': 'integer'}, 'examples': {'One': {'value': 1}, 'Two': {'value': 2}} } }, 'description': 'a list that actually returns numbers' } } assert schema['paths']['/x/']['post']['description'] == 'creation description' assert schema['paths']['/x/']['post']['responses'] == { '201': { 'content': {'application/json': {'schema': {'type': 'integer'}}}, 'description': 'creation with int response.' }, '222': {'description': 'creation with no response.'}, '223': {'description': 'No response body'}, '224': {'content': {'application/json': {'schema': {'type': 'integer'}}}, 'description': ''} } def test_openapi_response_without_description_string(no_warnings): class XViewSet(viewsets.GenericViewSet): queryset = SimpleModel.objects.all() serializer_class = SimpleSerializer @extend_schema( responses={ 200: OpenApiResponse( response=SimpleSerializer, examples=[OpenApiExample("Example1", value={"field": 1})], ) } ) def retrieve(self, request, *args, **kwargs): pass # pragma: no cover schema = generate_schema('/x', XViewSet) assert schema['paths']['/x/{id}/']['get']['responses']['200']['description'] == '' def test_prefix_estimation_with_re_special_chars_as_literals_in_path(no_warnings): # make sure prefix estimation logic does not choke on reserved RE chars @extend_schema(request=typing.Any, responses=typing.Any) @api_view(['POST']) def view_func1(request, format=None): pass # pragma: no cover @extend_schema(request=typing.Any, responses=typing.Any) @api_view(['POST']) def view_func2(request, format=None): pass # pragma: no cover schema = generate_schema(None, patterns=[ path('/\\/x/', view_func1), path('/\\/y/', view_func2) ]) assert schema['paths']['/\\/x/']['post']['tags'] == ['x'] def test_nested_router_urls(no_warnings): # somewhat tailored to drf-nested-routers but also serves a generic purpose # as "id" coercion also makes sense for "_pk" suffixes. class RouteNestedMaildropModel(models.Model): renamed_id = models.IntegerField(primary_key=True) class RouteNestedClientModel(models.Model): id = models.UUIDField(primary_key=True) class RouteNestedModel(models.Model): client = models.ForeignKey(RouteNestedClientModel, on_delete=models.CASCADE) maildrop = models.ForeignKey(RouteNestedMaildropModel, on_delete=models.CASCADE) class RouteNestedViewset(viewsets.ModelViewSet): queryset = RouteNestedModel.objects.all() serializer_class = SimpleSerializer urlpatterns = [ path( '/clients/{client_pk}/maildrops/{maildrop_pk}/recipients/{pk}/', RouteNestedViewset.as_view({'get': 'retrieve'}) ), ] schema = generate_schema(None, patterns=urlpatterns) operation = schema['paths']['/clients/{client_pk}/maildrops/{maildrop_pk}/recipients/{id}/']['get'] assert operation['parameters'][0]['name'] == 'client_pk' assert operation['parameters'][0]['schema'] == {'format': 'uuid', 'type': 'string'} assert operation['parameters'][2]['name'] == 'maildrop_pk' assert operation['parameters'][2]['schema']['type'] == 'integer' @pytest.mark.parametrize('value', [ datetime.datetime(year=2021, month=1, day=1), datetime.date(year=2021, month=1, day=1), datetime.time(), datetime.timedelta(days=1), uuid.uuid4(), Decimal(), b'deadbeef' ]) def test_yaml_encoder_parity(no_warnings, value): # make sure our YAML renderer does not choke on objects that are fine with # rest_framework.encoders.JSONEncoder assert OpenApiJsonRenderer().render(value) assert OpenApiYamlRenderer().render(value) @pytest.mark.parametrize(['comp_schema', 'discarded'], [ ({'type': 'object'}, True), ({'type': 'object', 'properties': {}}, True), ({'type': 'object', 'additionalProperties': {}}, False), ({'type': 'object', 'additionalProperties': {'type': 'number'}}, False), ({'type': 'number'}, False), ({'type': 'array', 'items': {'type': 'number'}}, False), ]) def test_serializer_extension_with_non_object_schema(no_warnings, comp_schema, discarded): class XSerializer(serializers.Serializer): field = serializers.CharField() class XExtension(OpenApiSerializerExtension): target_class = XSerializer def map_serializer(self, auto_schema, direction): return comp_schema class XAPIView(APIView): @extend_schema(request=XSerializer, responses=XSerializer) def post(self, request): pass # pragma: no cover schema = generate_schema('x', view=XAPIView) operation = schema['paths']['/x']['post'] if discarded: assert 'requestBody' not in operation else: assert get_request_schema(operation)['$ref'] == '#/components/schemas/X' assert schema['components']['schemas']['X'] == comp_schema def test_response_header_with_serializer_component(no_warnings): class XSerializer(serializers.Serializer): field = serializers.CharField() @extend_schema( request=OpenApiTypes.ANY, responses=OpenApiTypes.ANY, parameters=[OpenApiParameter( name='test', type=XSerializer, location=OpenApiParameter.HEADER, response=True, )] ) @api_view(['POST']) def view_func(request, format=None): pass # pragma: no cover schema = generate_schema('x', view_function=view_func) assert 'X' in schema['components']['schemas'] assert schema['paths']['/x']['post']['responses']['200']['headers'] == { 'test': {'schema': {'$ref': '#/components/schemas/X'}} } def test_extend_schema_noop_request_content_type(no_warnings): @extend_schema( request={ 'application/json': None, # for completeness, not necessary 'application/pdf': OpenApiTypes.BINARY }, responses=OpenApiTypes.ANY, ) @api_view(['POST']) def view_func(request, format=None): pass # pragma: no cover schema = generate_schema('x', view_function=view_func) assert 'application/pdf' in schema['paths']['/x']['post']['requestBody']['content'] assert 'application/json' not in schema['paths']['/x']['post']['requestBody']['content'] def test_viewset_reverse_list_detection_override(no_warnings): class XViewset(viewsets.ReadOnlyModelViewSet): queryset = SimpleModel.objects.all() serializer_class = SimpleSerializer @extend_schema( # without explicit operation_id, this operation is detected as non-list and thus # will be named "x_retrieve", which create a collision with the actual retrieve. operation_id='x_list', parameters=[OpenApiParameter("format")], responses={(200, "*/*"): OpenApiTypes.STR}, ) def list(self, request, *args, **kwargs): pass # pragma: no cover schema = generate_schema('/x', XViewset) assert schema['paths']['/x/']['get']['parameters'][0]['name'] == 'format' assert schema['paths']['/x/']['get']['operationId'] == 'x_list' assert schema['paths']['/x/{id}/']['get']['operationId'] == 'x_retrieve' def test_list_serializer_with_read_only_field_on_model_property(no_warnings): class M7Model(models.Model): @property def all_groups(self) -> typing.List[int]: return [1, 2, 3] # pragma: no cover class XField(serializers.ReadOnlyField): pass class XSerializer(serializers.ModelSerializer): groups = serializers.ListSerializer(source="all_groups", child=XField(), read_only=True) class Meta: model = M7Model fields = '__all__' class XViewset(viewsets.ReadOnlyModelViewSet): queryset = M7Model.objects.none() serializer_class = XSerializer schema = generate_schema('x', XViewset) assert schema['components']['schemas']['X']['properties']['groups'] == { 'type': 'array', 'items': {'type': 'array', 'items': {'type': 'integer'}, 'readOnly': True}, 'readOnly': True } def test_extend_schema_serializer_field_deprecation(no_warnings): @extend_schema_serializer(deprecate_fields=['old']) class XSerializer(serializers.Serializer): old = serializers.IntegerField() new = serializers.IntegerField() class XView(generics.ListCreateAPIView): serializer_class = XSerializer schema = generate_schema('/x', view=XView) assert schema['components']['schemas']['X']['properties']['new'] == { 'type': 'integer', } assert schema['components']['schemas']['X']['properties']['old'] == { 'type': 'integer', 'deprecated': True } def test_paginated_list_serializer_with_dict_field(no_warnings): class XAPIView(generics.ListAPIView): pagination_class = pagination.LimitOffsetPagination @extend_schema(responses=serializers.ListSerializer(child=serializers.DictField())) def get(self, request): pass # pragma: no cover schema = generate_schema('/x/', view=XAPIView) assert get_response_schema(schema['paths']['/x/']['get'])['properties']['results'] == { 'type': 'array', 'items': {'type': 'object', 'additionalProperties': {}} } def test_serializer_method_field_with_functools_partial(no_warnings): class XSerializer(serializers.Serializer): foo = serializers.SerializerMethodField() bar = serializers.SerializerMethodField() @extend_schema_field(OpenApiTypes.DATE) def _private_method_foo(self, field, extra_param): return 'foo' # pragma: no cover def _private_method_bar(self, field, extra_param) -> int: return 1 # pragma: no cover get_foo = partialmethod(_private_method_foo, extra_param='foo') get_bar = partialmethod(_private_method_bar, extra_param='bar') @extend_schema(request=XSerializer, responses=XSerializer) @api_view(['POST']) def view_func(request, format=None): pass # pragma: no cover schema = generate_schema('/x/', view_function=view_func) assert schema['components']['schemas']['X']['properties'] == { 'foo': {'type': 'string', 'format': 'date', 'readOnly': True}, 'bar': {'type': 'integer', 'readOnly': True} } @mock.patch( 'drf_spectacular.settings.spectacular_settings.ENABLE_LIST_MECHANICS_ON_NON_2XX', True ) def test_disable_list_mechanics_on_non_2XX(no_warnings): @extend_schema( request=SimpleSerializer, responses={ 200: SimpleSerializer(many=True), 400: SimpleSerializer(many=True), } ) @api_view(['POST']) def view_func(request, format=None): pass # pragma: no cover schema = generate_schema('/x/', view_function=view_func) assert get_response_schema(schema['paths']['/x/']['post'], status='200') == { 'type': 'array', 'items': {'$ref': '#/components/schemas/Simple'} } assert get_response_schema(schema['paths']['/x/']['post'], status='400') == { 'type': 'array', 'items': {'$ref': '#/components/schemas/Simple'} } @mock.patch( 'drf_spectacular.settings.spectacular_settings.AUTHENTICATION_WHITELIST', [TokenAuthentication] ) def test_authentication_whitelist(no_warnings): class XViewset(viewsets.ReadOnlyModelViewSet): serializer_class = SimpleSerializer queryset = SimpleModel.objects.none() authentication_classes = [BasicAuthentication, TokenAuthentication] schema = generate_schema('/x', XViewset) assert list(schema['components']['securitySchemes']) == ['tokenAuth'] assert schema['paths']['/x/']['get']['security'] == [{'tokenAuth': []}, {}] @mock.patch( 'drf_spectacular.settings.spectacular_settings.AUTHENTICATION_WHITELIST', [] ) def test_authentication_empty_whitelist(no_warnings): class XViewset(viewsets.ReadOnlyModelViewSet): serializer_class = SimpleSerializer queryset = SimpleModel.objects.none() authentication_classes = [BasicAuthentication, TokenAuthentication] schema = generate_schema('/x', XViewset) assert 'securitySchemes' not in schema['components'] assert schema['paths']['/x/']['get']['security'] == [{}] def test_request_response_raw_schema_annotation(no_warnings): @extend_schema( request={'application/pdf': {'type': 'string', 'format': 'binary'}}, responses={(200, 'application/pdf'): {'type': 'string', 'format': 'binary'}} ) @api_view(['POST']) def view_func(request, format=None): pass # pragma: no cover schema = generate_schema('/x/', view_function=view_func) op = schema['paths']['/x/']['post'] assert op['requestBody']['content']['application/pdf']['schema'] == { 'type': 'string', 'format': 'binary' } assert op['responses']['200']['content']['application/pdf']['schema'] == { 'type': 'string', 'format': 'binary' } def test_serializer_modelfield_and_methodfield_with_default_value(no_warnings): class M8Model(models.Model): field = models.IntegerField() class XSerializer(serializers.ModelSerializer): field = serializers.ModelField( model_field=M8Model()._meta.get_field('field'), default=3 ) field_smf = serializers.SerializerMethodField(default=4) def get_field_smf(self, obj) -> int: return 0 # pragma: no cover class Meta: model = M8Model fields = '__all__' class XViewset(viewsets.ModelViewSet): serializer_class = XSerializer queryset = M8Model.objects.all() schema = generate_schema('x', XViewset) assert strip_int64_details(schema['components']['schemas']['X']['properties']['field']) == { 'type': 'integer', 'default': 3 } assert strip_int64_details(schema['components']['schemas']['X']['properties']['field_smf']) == { 'type': 'integer', 'readOnly': True, 'default': 4 } def test_literal_dot_in_regex_path(no_warnings): @extend_schema( responses=OpenApiTypes.ANY, parameters=[ OpenApiParameter('filename', str, OpenApiParameter.PATH), OpenApiParameter('ext', str, OpenApiParameter.PATH) ] ) @api_view(['GET']) def view_func(request, format=None): pass # pragma: no cover urlpatterns = [ re_path('^file/(?P.*)\\.(?P\\w+)$', view_func) ] schema = generate_schema(None, patterns=urlpatterns) assert '/file/{filename}.{ext}' in schema['paths'] def test_customized_lookup_url_kwarg(no_warnings): class XViewset(viewsets.ModelViewSet): serializer_class = SimpleSerializer queryset = SimpleModel.objects.all() lookup_url_kwarg = 'custom_name' schema = generate_schema('/x', XViewset) assert schema['paths']['/x/{custom_name}/']['get']['parameters'][0] == { 'in': 'path', 'name': 'custom_name', 'schema': {'type': 'integer'}, 'description': 'A unique integer value identifying this simple model.', 'required': True } @pytest.mark.skipif(DJANGO_VERSION < '3', reason='Bug in Django\'s simplify_regex()') def test_regex_path_parameter_discovery_pattern(no_warnings): @extend_schema(responses=OpenApiTypes.FLOAT) @api_view(['GET']) def pi(request, foo): pass # pragma: no cover urlpatterns = [ re_path(r'^/pi/(?P(\d+)-[\w|\.]+(failed|success))', pi) ] schema = generate_schema(None, patterns=urlpatterns) assert schema['paths']['/pi/{precision}']['get']['parameters'][0] == { 'in': 'path', 'name': 'precision', 'schema': {'type': 'string', 'pattern': '^(\\d+)-[\\w|\\.]+(failed|success)$'}, 'required': True } class PathParameterLookupModel(models.Model): """ test_path_parameter_priority_matching """ field = models.IntegerField() @pytest.mark.parametrize(['path_func', 'path_str', 'pattern', 'parameter_types'], [ # django typed -> use (path, '/{id}/', '/', ['integer']), # untyped -> get from model (path, '/{id}/', '/', ['integer']), # non-default pattern -> use (re_path, '/{id}/', r'(?P[a-z]{2}(-[a-z]{2})?)/', ['string']), # default pattern -> get from model (re_path, '/{id}/', r'(?P[^/.]+)/$', ['integer']), # same mechanics for non-pk field discovery from model (re_path, '/{field}/t/{id}/', r'^(?P[^/.]+)/t/(?P[a-z]+)/', ['integer', 'string']), (re_path, '/{field}/t/{id}/', r'^(?P[A-Z\(\)]+)/t/(?P[^/.]+)/', ['string', 'integer']), ]) def test_path_parameter_priority_matching(no_warnings, path_func, path_str, pattern, parameter_types): class LookupSerializer(serializers.ModelSerializer): class Meta: model = PathParameterLookupModel fields = '__all__' class XAPIView(generics.RetrieveAPIView): serializer_class = LookupSerializer queryset = PathParameterLookupModel.objects.all() # make sure regex are valid if path_func == re_path: re.compile(pattern) urlpatterns = [path_func(pattern, XAPIView.as_view())] schema = generate_schema(None, patterns=urlpatterns) parameters = schema['paths'][path_str]['get']['parameters'] assert len(parameters) == len(parameter_types) for parameter_type, parameter in zip(parameter_types, parameters): assert parameter['schema']['type'] == parameter_type assert parameter_type != 'string' or 'pattern' in parameter['schema'] @pytest.mark.parametrize('import_string', IMPORT_STRINGS) def test_import_strings_in_default_settings(import_string): assert import_string in SPECTACULAR_DEFAULTS @mock.patch( 'drf_spectacular.settings.spectacular_settings.PATH_CONVERTER_OVERRIDES', { 'int': str, # override default behavior 'signed_int': {'type': 'integer', 'format': 'signed'}, } ) def test_path_converter_override(no_warnings): @extend_schema(responses=OpenApiTypes.FLOAT) @api_view(['GET']) def pi(request, foo): pass # pragma: no cover class SignedIntConverter(StringConverter): regex = r'\-[0-9]+' class HexConverter(StringConverter): regex = r'[a-f0-9]+' register_converter(SignedIntConverter, 'signed_int') register_converter(HexConverter, 'hex') urlpatterns = [ path('/a//', pi), path('/b//', pi), path('/c//', pi), ] schema = generate_schema(None, patterns=urlpatterns) assert schema['paths']['/a/{var}/']['get']['parameters'][0]['schema'] == { 'type': 'string', } assert schema['paths']['/b/{var}/']['get']['parameters'][0]['schema'] == { 'type': 'integer', 'format': 'signed' } assert schema['paths']['/c/{var}/']['get']['parameters'][0]['schema'] == { 'type': 'string', 'pattern': '^[a-f0-9]+$' } @pytest.mark.parametrize('kwargs,expected', [ ( {'max_value': -2147483648}, {'type': 'integer', 'maximum': -2147483648}, ), ( {'max_value': -2147483649}, {'type': 'integer', 'maximum': -2147483649, 'format': 'int64'}, ), ( {'max_value': 2147483647}, {'type': 'integer', 'maximum': 2147483647}, ), ( {'max_value': 2147483648}, {'type': 'integer', 'maximum': 2147483648, 'format': 'int64'}, ), ( {'min_value': -2147483648}, {'type': 'integer', 'minimum': -2147483648}, ), ( {'min_value': -2147483649}, {'type': 'integer', 'minimum': -2147483649, 'format': 'int64'}, ), ( {'min_value': 2147483647}, {'type': 'integer', 'minimum': 2147483647}, ), ( {'min_value': 2147483648}, {'type': 'integer', 'minimum': 2147483648, 'format': 'int64'}, ), ]) def test_int64_detection(kwargs, expected, no_warnings): class XSerializer(serializers.Serializer): field = serializers.IntegerField(**kwargs) @extend_schema(request=XSerializer, responses=XSerializer) @api_view(['GET']) def view_func(request, format=None): pass # pragma: no cover schema = generate_schema('x', view_function=view_func) assert schema['components']['schemas']['X']['properties']['field'] == expected def test_description_whitespace_stripping(no_warnings): class XViewset(viewsets.ModelViewSet): """ view: oneliner with leading/trailing whitespace """ serializer_class = SimpleSerializer queryset = SimpleModel.objects.none() def retrieve(self, request): """ retrieve: oneliner with leading/trailing whitespace """ pass # pragma: no cover def create(self, request): """ create: multi line indented description docstring """ # noqa: W291 pass # pragma: no cover schema = generate_schema('/x', XViewset) assert schema['paths']['/x/']['get']['description'] == ( 'view: oneliner with leading/trailing whitespace' ) assert schema['paths']['/x/{id}/']['get']['description'] == ( 'retrieve: oneliner with leading/trailing whitespace' ) assert schema['paths']['/x/']['post']['description'] == ( 'create: multi line indented\ndescription docstring' ) @pytest.mark.parametrize('list_variation', [ serializers.ListField, serializers.ListSerializer ]) def test_double_nested_list_serializer(no_warnings, list_variation): class XSerializer(serializers.Serializer): id = serializers.IntegerField() class XNestedListSerializer(serializers.Serializer): nested_xs = list_variation(child=XSerializer(many=True)) class XAPIView(generics.GenericAPIView): @extend_schema(request=XNestedListSerializer, responses=XNestedListSerializer) def post(self, request, *args, **kwargs): pass # pragma: no cover schema = generate_schema('x', view=XAPIView) operation = schema['paths']['/x']['post'] assert get_request_schema(operation) == {'$ref': '#/components/schemas/XNestedList'} assert get_response_schema(operation) == {'$ref': '#/components/schemas/XNestedList'} assert schema['components']['schemas']['XNestedList']['properties']['nested_xs'] == { 'type': 'array', 'items': {'type': 'array', 'items': {'$ref': '#/components/schemas/X'}} } @pytest.mark.parametrize('extend_method, api_view_method', [ ('get', 'GET'), ('GET', 'get'), ]) def test_api_view_decorator_case_insensitive(no_warnings, extend_method, api_view_method): @extend_schema(methods=[extend_method], responses=OpenApiTypes.FLOAT) @api_view([api_view_method]) def pi(request): pass # pragma: no cover schema = generate_schema('x', view_function=pi) operation = schema['paths']['/x']['get'] assert get_response_schema(operation) == {'type': 'number', 'format': 'float'} @pytest.mark.parametrize('extend_method, action_method', [ ('get', 'GET'), ('GET', 'get'), ]) def test_action_decorator_case_insensitive(no_warnings, extend_method, action_method): class XViewSet(viewsets.ReadOnlyModelViewSet): queryset = SimpleModel.objects.all() serializer_class = SimpleSerializer @extend_schema(methods=[extend_method], summary='A custom action!') @action(methods=[action_method], detail=True) def custom_action(self): pass # pragma: no cover schema = generate_schema('x', viewset=XViewSet) assert schema['paths']['/x/{id}/custom_action/']['get']['summary'] == 'A custom action!' def test_extend_schema_view_isolation(no_warnings): class AnimalViewSet(viewsets.GenericViewSet): serializer_class = SimpleSerializer queryset = SimpleModel.objects.all() @action(detail=False) def notes(self, request): pass # pragma: no cover @extend_schema_view(notes=extend_schema(summary='List mammals.')) class MammalViewSet(AnimalViewSet): pass @extend_schema_view(notes=extend_schema(summary='List insects.')) class InsectViewSet(AnimalViewSet): pass router = routers.SimpleRouter() router.register('api/mammals', MammalViewSet) router.register('api/insects', InsectViewSet) schema = generate_schema(None, patterns=router.urls) assert schema['paths']['/api/mammals/notes/']['get']['summary'] == 'List mammals.' assert schema['paths']['/api/insects/notes/']['get']['summary'] == 'List insects.' def test_extend_schema_view_layering(no_warnings): class YSerializer(serializers.Serializer): field = serializers.FloatField() class ZSerializer(serializers.Serializer): field = serializers.UUIDField() class XViewSet(viewsets.ReadOnlyModelViewSet): queryset = SimpleModel.objects.all() serializer_class = SimpleSerializer @extend_schema_view(retrieve=extend_schema(responses=YSerializer)) class YViewSet(XViewSet): pass @extend_schema_view(retrieve=extend_schema(responses=ZSerializer)) class ZViewSet(YViewSet): pass router = routers.SimpleRouter() router.register('x', XViewSet) router.register('y', YViewSet) router.register('z', ZViewSet) schema = generate_schema(None, patterns=router.urls) resp = { c: get_response_schema(schema['paths'][f'/{c.lower()}/{{id}}/']['get']) for c in ['X', 'Y', 'Z'] } assert resp['X'] == {'$ref': '#/components/schemas/Simple'} assert resp['Y'] == {'$ref': '#/components/schemas/Y'} assert resp['Z'] == {'$ref': '#/components/schemas/Z'} def test_extend_schema_view_extend_schema_crosstalk(no_warnings): class XSerializer(serializers.Serializer): field = serializers.FloatField() # extend_schema_view provokes decorator reordering in extend_schema @extend_schema(tags=['X']) @extend_schema_view(retrieve=extend_schema(responses=XSerializer)) class XViewSet(viewsets.ReadOnlyModelViewSet): queryset = SimpleModel.objects.all() serializer_class = SimpleSerializer @extend_schema(tags=['Y']) class YViewSet(XViewSet): pass router = routers.SimpleRouter() router.register('x', XViewSet) router.register('y', YViewSet) schema = generate_schema(None, patterns=router.urls) op = { c: schema['paths'][f'/{c.lower()}/{{id}}/']['get'] for c in ['X', 'Y'] } assert op['X']['tags'] == ['X'] assert op['Y']['tags'] == ['Y'] def test_extend_schema_view_on_api_view(no_warnings): @extend_schema_view( get=extend_schema(description='get desc', responses=OpenApiTypes.FLOAT), post=extend_schema(description='post desc', request=OpenApiTypes.INT, responses=OpenApiTypes.UUID), ) @api_view(['GET', 'POST']) def view_func(request, format=None): pass # pragma: no cover schema = generate_schema('/x/', view_function=view_func) op_get = schema['paths']['/x/']['get'] op_post = schema['paths']['/x/']['post'] assert get_response_schema(op_get) == {'type': 'number', 'format': 'float'} assert get_response_schema(op_post) == {'format': 'uuid', 'type': 'string'} assert get_request_schema(op_post) == {'type': 'integer'} @mock.patch('drf_spectacular.settings.spectacular_settings.COMPONENT_SPLIT_REQUEST', True) @pytest.mark.parametrize('ro,wo', [(True, False), (False, True), (False, False)]) def test_nested_empty_direction_serializer_with_split(no_warnings, ro, wo): class NestedSerializer(serializers.Serializer): field = serializers.IntegerField(write_only=wo, read_only=ro) class XSerializer(serializers.Serializer): field = NestedSerializer(many=True) @extend_schema(request=XSerializer, responses=XSerializer) @api_view(['POST']) def pi(request, format=None): pass # pragma: no cover schema = generate_schema('/x', view_function=pi) operation = schema['paths']['/x']['post'] if wo: assert get_request_schema(operation) == {'$ref': '#/components/schemas/XRequest'} assert operation['responses']['200'] == {'description': 'No response body'} elif ro: assert 'requestBody' not in operation assert get_response_schema(operation) == {'$ref': '#/components/schemas/X'} else: assert get_request_schema(operation) == {'$ref': '#/components/schemas/XRequest'} assert get_response_schema(operation) == {'$ref': '#/components/schemas/X'} @mock.patch('drf_spectacular.settings.spectacular_settings.COMPONENT_SPLIT_REQUEST', True) @pytest.mark.parametrize('ro,wo', [(True, False), (False, True), (False, False)]) def test_empty_direction_list_serializer_with_split(no_warnings, ro, wo): class XSerializer(serializers.Serializer): field = serializers.IntegerField(write_only=wo, read_only=ro) @extend_schema(request=XSerializer(many=True), responses=XSerializer(many=True)) @api_view(['POST']) def pi(request, format=None): pass # pragma: no cover schema = generate_schema('/x', view_function=pi) operation = schema['paths']['/x']['post'] if wo: assert get_request_schema(operation)['items'] == {'$ref': '#/components/schemas/XRequest'} assert operation['responses']['200'] == {'description': 'No response body'} elif ro: assert 'requestBody' not in operation assert get_response_schema(operation)['items'] == {'$ref': '#/components/schemas/X'} else: assert get_request_schema(operation)['items'] == {'$ref': '#/components/schemas/XRequest'} assert get_response_schema(operation)['items'] == {'$ref': '#/components/schemas/X'} @mock.patch('drf_spectacular.settings.spectacular_settings.SCHEMA_PATH_PREFIX_INSERT', '/service/backend') def test_schema_path_prefix_insert(no_warnings): @extend_schema(responses=typing.Any) @api_view(['GET']) def view_func(request, format=None): pass # pragma: no cover schema = generate_schema('v1/x/', view_function=view_func) assert '/service/backend/v1/x/' in schema['paths'] @mock.patch('drf_spectacular.settings.spectacular_settings.ENFORCE_NON_BLANK_FIELDS', True) def test_enforce_non_blank_fields(no_warnings): class XSerializer(serializers.Serializer): ro = serializers.CharField(read_only=True) wo = serializers.CharField(write_only=True) rw = serializers.CharField() @extend_schema(request=XSerializer, responses=XSerializer) @api_view(['POST']) def view_func(request, format=None): pass # pragma: no cover schema = generate_schema('/x/', view_function=view_func) assert schema['components']['schemas']['X']['properties'] == { 'ro': {'type': 'string', 'readOnly': True}, 'wo': {'type': 'string', 'writeOnly': True, 'minLength': 1}, 'rw': {'type': 'string', 'minLength': 1} } def test_extend_schema_serializer_isolation(no_warnings): @extend_schema_serializer(component_name='ABC') class OneSerializer(serializers.Serializer): pass @extend_schema_serializer(component_name='XYZ') class TwoSerializer(OneSerializer): pass assert OneSerializer._spectacular_annotation == {'component_name': 'ABC'} assert TwoSerializer._spectacular_annotation == {'component_name': 'XYZ'} def test_extend_schema_field_isolation(no_warnings): @extend_schema_field(field=OpenApiTypes.FLOAT) class OneField(serializers.IntegerField): pass @extend_schema_field(field=OpenApiTypes.DOUBLE) class TwoField(OneField): pass assert OneField._spectacular_annotation['field'] == OpenApiTypes.FLOAT assert TwoField._spectacular_annotation['field'] == OpenApiTypes.DOUBLE def test_catch_all_status_code_responses(no_warnings): @extend_schema(responses={ '2XX': SimpleSerializer, '401': inline_serializer('Error1', fields={'detail': serializers.CharField()}), '4XX': inline_serializer('Error2', fields={'detail': serializers.CharField()}), }) @api_view(['GET']) def view_func(request, format=None): pass # pragma: no cover schema = generate_schema('/x/', view_function=view_func) assert list(schema['paths']['/x/']['get']['responses'].keys()) == ['2XX', '401', '4XX'] @mock.patch('drf_spectacular.settings.spectacular_settings.RENDERER_WHITELIST', [renderers.MultiPartRenderer]) @mock.patch('drf_spectacular.settings.spectacular_settings.PARSER_WHITELIST', [parsers.MultiPartParser]) def test_renderer_parser_whitelist(no_warnings): class XSerializer(serializers.Serializer): field = serializers.CharField() class XViewset(viewsets.ModelViewSet): serializer_class = XSerializer queryset = SimpleModel.objects.none() renderer_classes = [renderers.MultiPartRenderer, renderers.JSONRenderer] parser_classes = [parsers.MultiPartParser, parsers.JSONParser] schema = generate_schema('/x', XViewset) request_types = list(schema['paths']['/x/']['post']['requestBody']['content'].keys()) response_types = list(schema['paths']['/x/']['post']['responses']['201']['content'].keys()) assert response_types == request_types == ['multipart/form-data'] def test_empty_auth_override(no_warnings): @extend_schema(responses=SimpleSerializer, auth=[]) @api_view(['GET']) def view_func(request, format=None): pass # pragma: no cover schema = generate_schema('/x/', view_function=view_func) assert 'security' not in schema['paths']['/x/']['get'] def test_external_docs(no_warnings): @extend_schema(responses=SimpleSerializer, external_docs="https://example.com") @api_view(['GET']) def view_func(request, format=None): pass # pragma: no cover schema = generate_schema('/x/', view_function=view_func) assert schema['paths']['/x/']['get']['externalDocs'] == {'url': 'https://example.com'} def test_basic_parameters_with_many(no_warnings): @extend_schema( request=OpenApiTypes.ANY, responses=OpenApiTypes.ANY, parameters=[OpenApiParameter(name='test', type=int, many=True)] ) @api_view(['POST']) def view_func(request, format=None): pass # pragma: no cover schema = generate_schema('/x/', view_function=view_func) assert schema['paths']['/x/']['post']['parameters'][0]['schema'] == { 'type': 'array', 'items': {'type': 'integer'} } def test_parameter_with_pattern(no_warnings): @extend_schema( request=OpenApiTypes.ANY, responses=OpenApiTypes.ANY, parameters=[OpenApiParameter(name='test', type=OpenApiTypes.REGEX, pattern='^[0-9]{3}$')] ) @api_view(['POST']) def view_func(request, format=None): pass # pragma: no cover schema = generate_schema('/x/', view_function=view_func) assert schema['paths']['/x/']['post']['parameters'][0]['schema'] == { 'pattern': '^[0-9]{3}$', 'type': 'string', 'format': 'regex' } def test_mock_request_in_serializer_context(no_warnings): # split test into 2 serializers as get_fields is used through a cached property # and thus the assert may not be executed for the annotated case. class AnnotatedSerializer(serializers.Serializer): field = serializers.CharField() def get_fields(self): assert self.context and 'request' in self.context return super().get_fields() class RegularSerializer(serializers.Serializer): field = serializers.IntegerField() def get_fields(self): assert self.context and 'request' in self.context return super().get_fields() @extend_schema_view(retrieve=extend_schema(responses=AnnotatedSerializer)) class XViewset(viewsets.ModelViewSet): serializer_class = RegularSerializer queryset = SimpleModel.objects.all() generate_schema('/x', XViewset) def test_drf_authtoken_schema_override_bug(no_warnings): from rest_framework.authtoken.views import ObtainAuthToken generate_schema('/token/', view=ObtainAuthToken) def test_safestring_serialization(no_warnings): from django.utils.safestring import mark_safe @extend_schema( responses=SimpleSerializer, summary=mark_safe('

Woah!

'), description=mark_safe('

Woah!

that\'sbold'), ) @api_view(['GET']) def view_func(request, format=None): pass # pragma: no cover schema = generate_schema('/x/', view_function=view_func) assert b"

Woah!

" in OpenApiJsonRenderer().render(schema) assert b"

Woah!

" in OpenApiYamlRenderer().render(schema) def test_many_parameter_item_enum(no_warnings): @extend_schema( parameters=[OpenApiParameter( 'status', type=int, many=True, style="form", explode=False, enum=[1, 2, 3] )], responses=SimpleSerializer, ) @api_view(['GET']) def view_func(request, format=None): pass # pragma: no cover schema = generate_schema('/x/', view_function=view_func) assert schema['paths']['/x/']['get']['parameters'][0] == { 'in': 'query', 'name': 'status', 'schema': {'type': 'array', 'items': {'type': 'integer', 'enum': [1, 2, 3]}}, 'explode': False, 'style': 'form' } @mock.patch('drf_spectacular.settings.spectacular_settings.DEFAULT_QUERY_MANAGER', '_default_manager') def test_custom_default_manager(no_warnings): class RelatedModelForCustomManager(models.Model): foo = models.Manager() class ModelWithCustomManagerRelation(models.Model): related_field = models.ForeignKey( RelatedModelForCustomManager, on_delete=models.PROTECT, editable=False ) class XSerializer(serializers.ModelSerializer): class Meta: fields = '__all__' model = ModelWithCustomManagerRelation class XViewset(viewsets.ModelViewSet): serializer_class = XSerializer queryset = ModelWithCustomManagerRelation.objects.none() router = routers.SimpleRouter() router.register('x/', XViewset) # cross-check that the test works assert not hasattr(RelatedModelForCustomManager, 'objects') schema = generate_schema(None, patterns=router.urls) assert schema['paths']['/x/{related_field}/']['get']['parameters'][0] == { 'in': 'path', 'name': 'related_field', 'schema': {'type': 'integer'}, 'required': True } def test_primary_key_related_field_default_value(no_warnings): class XSerializer(serializers.Serializer): field = serializers.PrimaryKeyRelatedField( queryset=SimpleModel.objects.none(), many=True, default=[] ) class XViewset(viewsets.ModelViewSet): serializer_class = XSerializer queryset = SimpleModel.objects.all() schema = generate_schema('/x', XViewset) assert schema['components']['schemas']['X']['properties'] == { 'field': { 'type': 'array', # this nested default is wrong but a consequence of DRF's init system 'items': {'type': 'integer', 'default': []}, 'default': [] } } def test_slug_related_field_to_model_property(no_warnings): class M10(models.Model): @property def property_field(self) -> float: return 42 # pragma: no cover class M11(models.Model): field = models.ForeignKey(M10, on_delete=models.CASCADE) class XSerializer(serializers.ModelSerializer): # How the field is defined in a Serializer field = serializers.SlugRelatedField(slug_field="property_field", read_only=True) class Meta: fields = '__all__' model = M11 class XViewset(viewsets.ModelViewSet): serializer_class = XSerializer queryset = M11.objects.all() schema = generate_schema('/x', XViewset) assert schema['components']['schemas']['X']['properties'] == { 'id': {'type': 'integer', 'readOnly': True}, 'field': {'format': 'double', 'readOnly': True, 'type': 'number'} } def test_serializer_foreign_key_default_value_handling(no_warnings): class M12(models.Model): field = models.CharField(unique=True) class M13(models.Model): field_related = models.ForeignKey(M12, on_delete=models.CASCADE, default=1) class XSerializer(serializers.ModelSerializer): field_related = serializers.PrimaryKeyRelatedField( queryset=M13.objects.all(), default=1, ) field_related_slug = serializers.SlugRelatedField( source='field_related', slug_field='field', queryset=M12.objects.all(), default='foo', ) class Meta: fields = '__all__' model = M13 class XViewset(viewsets.ModelViewSet): serializer_class = XSerializer queryset = M13.objects.none() schema = generate_schema('/x', XViewset) assert schema['components']['schemas']['X']['properties'] == { 'id': {'type': 'integer', 'readOnly': True}, 'field_related': {'type': 'integer', 'default': 1}, 'field_related_slug': {'type': 'string', 'default': 'foo'}, } def test_serializer_method_docstring_precedence(no_warnings): class XSerializer(serializers.Serializer): field_method1 = serializers.SerializerMethodField() field_method2 = serializers.SerializerMethodField(help_text='help_text 2') field_method3 = serializers.SerializerMethodField() def get_field_method1(self) -> str: """ docstring 1 """ return '' # pragma: no cover def get_field_method2(self) -> int: """ docstring 2 """ return 1 # pragma: no cover @extend_schema_field(OpenApiTypes.DATETIME) def get_field_method3(self): """ docstring 3 """ pass # pragma: no cover class XViewset(viewsets.ModelViewSet): serializer_class = XSerializer queryset = SimpleModel.objects.all() schema = generate_schema('/x', XViewset) assert schema['components']['schemas']['X']['properties'] == { 'field_method1': {'type': 'string', 'description': 'docstring 1', 'readOnly': True}, 'field_method2': {'type': 'integer', 'description': 'help_text 2', 'readOnly': True}, 'field_method3': {'type': 'string', 'description': 'docstring 3', 'format': 'date-time', 'readOnly': True}, } @mock.patch('drf_spectacular.settings.spectacular_settings.ENUM_GENERATE_CHOICE_DESCRIPTION', False) def test_disable_enum_description_generation(no_warnings): class XSerializer(serializers.Serializer): foo = serializers.ChoiceField(choices=(('A', 'a'), ('B', 'b'))) bar = serializers.ChoiceField( help_text='bar description', choices=(('A', 'a'), ('B', 'b'), ('C', 'c')) ) class XView(generics.RetrieveAPIView): serializer_class = XSerializer schema = generate_schema('/x', view=XView) assert schema['components']['schemas'] == { 'BarEnum': {'enum': ['A', 'B', 'C'], 'type': 'string'}, 'FooEnum': {'enum': ['A', 'B'], 'type': 'string'}, 'X': { 'type': 'object', 'properties': { 'foo': {'$ref': '#/components/schemas/FooEnum'}, 'bar': {'allOf': [{'$ref': '#/components/schemas/BarEnum'}], 'description': 'bar description'} }, 'required': ['bar', 'foo'] } } def test_openapi_request_wrapper(no_warnings): class XSerializer(serializers.Serializer): field = serializers.MultipleChoiceField(choices=[1, 2, 3, 4]) @extend_schema( request={ 'application/x-www-form-urlencoded': XSerializer, 'multipart/form-data': OpenApiRequest( request=XSerializer, encoding={"field": {"style": "form", "explode": True}}, examples=[OpenApiExample('Ex1', "field=1&field=3")] ) }, responses=XSerializer ) @api_view(['POST']) def view_func(request, format=None): pass # pragma: no cover schema = generate_schema('/x/', view_function=view_func) assert schema['paths']['/x/']['post']['requestBody']['content'] == { 'application/x-www-form-urlencoded': {'schema': {'$ref': '#/components/schemas/X'}}, 'multipart/form-data': { 'schema': {'$ref': '#/components/schemas/X'}, 'examples': {'Ex1': {'value': 'field=1&field=3'}}, 'encoding': {'field': {'style': 'form', 'explode': True}} } } def test_exclude_then_include_subclassed_view(no_warnings): @extend_schema(exclude=True) class X1ViewSet(viewsets.ReadOnlyModelViewSet): serializer_class = SimpleSerializer queryset = SimpleModel.objects.none() @extend_schema(exclude=False) class X2ViewSet(X1ViewSet): pass class X3ViewSet(X2ViewSet): pass router = routers.SimpleRouter() router.register('x1', X1ViewSet) router.register('x2', X2ViewSet) router.register('x3', X3ViewSet) schema = generate_schema(None, patterns=router.urls) assert '/x1/' not in schema['paths'] assert '/x2/' in schema['paths'] assert '/x3/' in schema['paths'] def test_disable_viewset_list_handling_as_one_off(no_warnings): class X1ViewSet(viewsets.ReadOnlyModelViewSet): serializer_class = SimpleSerializer queryset = SimpleModel.objects.none() @extend_schema(responses=forced_singular_serializer(SimpleSerializer)) def list(self): pass # pragma: no cover class X2ViewSet(viewsets.ReadOnlyModelViewSet): serializer_class = SimpleSerializer queryset = SimpleModel.objects.none() schema1 = generate_schema('/x', X1ViewSet) schema2 = generate_schema('/x', X2ViewSet) # both list and retrieve are single-object schema_list = get_response_schema(schema1['paths']['/x/']['get']) schema_retrieve = get_response_schema(schema1['paths']['/x/{id}/']['get']) assert schema_list == schema_retrieve == {'$ref': '#/components/schemas/Simple'} # this patch does not bleed into other usages of the same serializer class assert get_response_schema(schema2['paths']['/x/']['get']) == { 'type': 'array', 'items': {'$ref': '#/components/schemas/Simple'} } def test_openapirequest_used_without_media_type_dict(no_warnings): @extend_schema(request=OpenApiRequest(SimpleSerializer), responses=None) @api_view(['POST']) def view_func(request, format=None): pass # pragma: no cover schema = generate_schema('/x/', view_function=view_func) assert get_request_schema(schema['paths']['/x/']['post']) == { '$ref': '#/components/schemas/Simple' } drf-spectacular-0.27.0/tests/test_specification_extensions.py000066400000000000000000000155331453572150400245460ustar00rootroot00000000000000from unittest import mock import pytest from rest_framework import serializers from rest_framework.decorators import api_view, authentication_classes from rest_framework.views import APIView from drf_spectacular.contrib.django_oauth_toolkit import DjangoOAuthToolkitScheme from drf_spectacular.utils import OpenApiParameter, extend_schema, extend_schema_serializer from tests import generate_schema, get_request_schema, get_response_schema @mock.patch('drf_spectacular.settings.spectacular_settings.EXTENSIONS_INFO', { 'x-logo': { 'altText': 'Django REST Framework Logo', 'backgroundColor': "#ffffff", 'url': 'https://www.django-rest-framework.org/img/logo.png', }, }) @mock.patch('drf_spectacular.settings.spectacular_settings.EXTENSIONS_ROOT', { 'x-tagGroups': [{'name': 'Foo', 'tags': ['bar', 'baz']}] }) def test_root_info_spec_extensions(no_warnings): # https://redoc.ly/docs/api-reference-docs/specification-extensions/x-logo/ class XAPIView(APIView): pass schema = generate_schema('x', view=XAPIView) assert schema['info']['x-logo'] == { 'altText': 'Django REST Framework Logo', 'backgroundColor': "#ffffff", 'url': 'https://www.django-rest-framework.org/img/logo.png', } assert schema['x-tagGroups'] == [{'name': 'Foo', 'tags': ['bar', 'baz']}] def test_operation_spec_extensions(no_warnings): # https://mrin9.github.io/RapiDoc/api.html#vendor-extensions # https://mrin9.github.io/RapiDoc/examples/badges.html#overview @extend_schema( request=None, responses=None, extensions={ 'x-badges': [ {'color': 'red', 'label': 'Beta'}, {'color': 'orange', 'label': 'Slow'}, ], }, ) @api_view(['GET']) def view_func(request, format=None): pass # pragma: no cover schema = generate_schema('x', view_function=view_func) assert schema['paths']['/x']['get']['x-badges'] == [ {'color': 'red', 'label': 'Beta'}, {'color': 'orange', 'label': 'Slow'}, ] def test_operation_spec_extensions2(no_warnings): # https://mrin9.github.io/RapiDoc/api.html#vendor-extensions # https://mrin9.github.io/RapiDoc/examples/code-samples.html#get-/code-samples # https://redoc.ly/docs/api-reference-docs/specification-extensions/x-code-samples/ @extend_schema( request=None, responses=None, extensions={ 'x-code-samples': [ {'lang': 'js', 'label': 'JavaScript', 'source': 'console.log("Hello World");'}, {'lang': 'python', 'label': 'Python', 'source': 'print("Hello World")'}, ], }, ) @api_view(['GET']) def view_func(request, format=None): pass # pragma: no cover schema = generate_schema('x', view_function=view_func) assert schema['paths']['/x']['get']['x-code-samples'] == [ {'lang': 'js', 'label': 'JavaScript', 'source': 'console.log("Hello World");'}, {'lang': 'python', 'label': 'Python', 'source': 'print("Hello World")'}, ] def test_operation_spec_extensions3(no_warnings): # https://openapi-generator.tech/docs/swagger-codegen-migration#body-parameter-name # https://github.com/OpenAPITools/openapi-generator/issues/729 class UserSerializer(serializers.Serializer): name = serializers.CharField() age = serializers.IntegerField(min_value=0) @extend_schema( request=UserSerializer, responses=UserSerializer, extensions={'x-codegen-request-body-name': 'body'}, ) @api_view(['PATCH']) def view_func(request, format=None): pass # pragma: no cover schema = generate_schema('x', view_function=view_func) operation = schema['paths']['/x']['patch'] assert get_request_schema(operation)['$ref'] == '#/components/schemas/PatchedUser' assert operation['x-codegen-request-body-name'] == 'body' def test_serializer_component_spec_extensions(no_warnings): # https://docs.apimatic.io/specification-extensions/swagger-codegen-extensions/#dynamic-response-extension @extend_schema_serializer(extensions={'x-is-dynamic': True}) class UserSerializer(serializers.Serializer): name = serializers.CharField() age = serializers.IntegerField(min_value=0) @extend_schema(request=UserSerializer, responses=UserSerializer) @api_view(['GET']) def view_func(request, format=None): pass # pragma: no cover schema = generate_schema('x', view_function=view_func) operation = schema['paths']['/x']['get'] component = schema['components']['schemas']['User'] assert get_response_schema(operation)['$ref'] == '#/components/schemas/User' assert component['x-is-dynamic'] is True @pytest.mark.contrib('oauth2_provider') def test_security_spec_extensions(no_warnings): # https://mrin9.github.io/RapiDoc/api.html#vendor-extensions # https://mrin9.github.io/RapiDoc/examples/oauth-vendor-extension.html#overview # https://redoc.ly/docs/api-reference-docs/specification-extensions/x-default-clientid/ from oauth2_provider.contrib.rest_framework import OAuth2Authentication class CustomOAuth2Authentication(OAuth2Authentication): pass class CustomOAuthToolkitScheme(DjangoOAuthToolkitScheme): target_class = CustomOAuth2Authentication def get_security_definition(self, auto_schema): return { **super().get_security_definition(auto_schema), 'x-client-id': 'my-client-id', 'x-client-secret': 'my-client-secret', } @extend_schema(request=None, responses=None) @api_view(['GET']) @authentication_classes([CustomOAuth2Authentication]) def view_func(request, format=None): pass # pragma: no cover schema = generate_schema('x', view_function=view_func) oauth2 = schema['components']['securitySchemes']['oauth2'] assert oauth2['x-client-id'] == 'my-client-id' assert oauth2['x-client-secret'] == 'my-client-secret' def test_parameter_spec_extensions(no_warnings): # this is a bad example as it is already part of 3.0.3, but others would work accordingly # https://github.com/Redocly/redoc/blob/master/docs/redoc-vendor-extensions.md#parameter-object @extend_schema( responses=None, parameters=[ OpenApiParameter( name='q', type=str, location=OpenApiParameter.QUERY, extensions={'x-examples': ['foo', 'bar']} # don't use this for examples! ) ] ) @api_view(['GET']) def view_func(request, format=None): pass # pragma: no cover schema = generate_schema('x', view_function=view_func) assert schema['paths']['/x']['get']['parameters'][0] == { 'in': 'query', 'name': 'q', 'schema': {'type': 'string'}, 'x-examples': ['foo', 'bar'] } drf-spectacular-0.27.0/tests/test_split.py000066400000000000000000000036021453572150400205740ustar00rootroot00000000000000from unittest import mock from django.db import models from rest_framework import mixins, parsers, serializers, viewsets from tests import assert_schema, generate_schema class PNM1(models.Model): field = models.IntegerField() class PNM2(models.Model): field_relation = models.ForeignKey(PNM1, on_delete=models.CASCADE) field_non_blank = models.CharField(max_length=5) class XSerializer(serializers.ModelSerializer): class Meta: model = PNM1 fields = '__all__' class YSerializer(serializers.ModelSerializer): field_relation = XSerializer() field_relation_partial = XSerializer(source='field_relation', partial=True) class Meta: model = PNM2 fields = '__all__' class XViewset(mixins.UpdateModelMixin, viewsets.GenericViewSet): parser_classes = [parsers.JSONParser] serializer_class = YSerializer queryset = PNM2.objects.all() @mock.patch('drf_spectacular.settings.spectacular_settings.COMPONENT_SPLIT_REQUEST', False) def test_nested_partial_on_split_request_false(no_warnings, django_transforms): # without split request, PatchedY and Y have the same properties (minus required). # PATCH only modifies outermost serializer, nested serializers must stay unaffected. assert_schema( generate_schema('x', XViewset), 'tests/test_split_request_false.yml', transforms=django_transforms ) @mock.patch('drf_spectacular.settings.spectacular_settings.COMPONENT_SPLIT_REQUEST', True) def test_nested_partial_on_split_request_true(no_warnings, django_transforms): # with split request, behaves like above, however response schemas are always unpatched. # nested request serializers are only affected by their manual partial flag and not due to PATCH. assert_schema( generate_schema('x', XViewset), 'tests/test_split_request_true.yml', transforms=django_transforms, ) drf-spectacular-0.27.0/tests/test_split_request_false.yml000066400000000000000000000050551453572150400236730ustar00rootroot00000000000000openapi: 3.0.3 info: title: '' version: 0.0.0 paths: /x/{id}/: put: operationId: x_update parameters: - in: path name: id schema: type: integer description: A unique integer value identifying this pn m2. required: true tags: - x requestBody: content: application/json: schema: $ref: '#/components/schemas/Y' required: true security: - cookieAuth: [] - basicAuth: [] - {} responses: '200': content: application/json: schema: $ref: '#/components/schemas/Y' description: '' patch: operationId: x_partial_update parameters: - in: path name: id schema: type: integer description: A unique integer value identifying this pn m2. required: true tags: - x requestBody: content: application/json: schema: $ref: '#/components/schemas/PatchedY' security: - cookieAuth: [] - basicAuth: [] - {} responses: '200': content: application/json: schema: $ref: '#/components/schemas/Y' description: '' components: schemas: PatchedX: type: object properties: id: type: integer readOnly: true field: type: integer PatchedY: type: object properties: id: type: integer readOnly: true field_relation: $ref: '#/components/schemas/X' field_relation_partial: $ref: '#/components/schemas/PatchedX' field_non_blank: type: string maxLength: 5 X: type: object properties: id: type: integer readOnly: true field: type: integer required: - field - id Y: type: object properties: id: type: integer readOnly: true field_relation: $ref: '#/components/schemas/X' field_relation_partial: $ref: '#/components/schemas/PatchedX' field_non_blank: type: string maxLength: 5 required: - field_non_blank - field_relation - field_relation_partial - id securitySchemes: basicAuth: type: http scheme: basic cookieAuth: type: apiKey in: cookie name: sessionid drf-spectacular-0.27.0/tests/test_split_request_true.yml000066400000000000000000000057631453572150400235660ustar00rootroot00000000000000openapi: 3.0.3 info: title: '' version: 0.0.0 paths: /x/{id}/: put: operationId: x_update parameters: - in: path name: id schema: type: integer description: A unique integer value identifying this pn m2. required: true tags: - x requestBody: content: application/json: schema: $ref: '#/components/schemas/YRequest' required: true security: - cookieAuth: [] - basicAuth: [] - {} responses: '200': content: application/json: schema: $ref: '#/components/schemas/Y' description: '' patch: operationId: x_partial_update parameters: - in: path name: id schema: type: integer description: A unique integer value identifying this pn m2. required: true tags: - x requestBody: content: application/json: schema: $ref: '#/components/schemas/PatchedYRequest' security: - cookieAuth: [] - basicAuth: [] - {} responses: '200': content: application/json: schema: $ref: '#/components/schemas/Y' description: '' components: schemas: PatchedXRequest: type: object properties: field: type: integer PatchedYRequest: type: object properties: field_relation: $ref: '#/components/schemas/XRequest' field_relation_partial: $ref: '#/components/schemas/PatchedXRequest' field_non_blank: type: string minLength: 1 maxLength: 5 X: type: object properties: id: type: integer readOnly: true field: type: integer required: - field - id XRequest: type: object properties: field: type: integer required: - field Y: type: object properties: id: type: integer readOnly: true field_relation: $ref: '#/components/schemas/X' field_relation_partial: $ref: '#/components/schemas/X' field_non_blank: type: string maxLength: 5 required: - field_non_blank - field_relation - field_relation_partial - id YRequest: type: object properties: field_relation: $ref: '#/components/schemas/XRequest' field_relation_partial: $ref: '#/components/schemas/PatchedXRequest' field_non_blank: type: string minLength: 1 maxLength: 5 required: - field_non_blank - field_relation - field_relation_partial securitySchemes: basicAuth: type: http scheme: basic cookieAuth: type: apiKey in: cookie name: sessionid drf-spectacular-0.27.0/tests/test_validators.py000066400000000000000000000440351453572150400216160ustar00rootroot00000000000000import sys from datetime import timedelta from unittest import mock import pytest from django.contrib.auth import validators as auth_validators from django.contrib.postgres import validators as postgres_validators from django.core import validators from django.urls import path from rest_framework import serializers from rest_framework.decorators import api_view from drf_spectacular.utils import extend_schema from tests import assert_schema, generate_schema @mock.patch('rest_framework.settings.api_settings.COERCE_DECIMAL_TO_STRING', False) def test_validators(): class XSerializer(serializers.Serializer): # Note that these fields intentionally use basic field types to ensure that we detect from the validator only. # The following only apply for `string` type: char_email = serializers.CharField(validators=[validators.EmailValidator()]) char_url = serializers.CharField(validators=[validators.URLValidator()]) char_regex = serializers.CharField(validators=[validators.RegexValidator(r'\w+')]) char_max_length = serializers.CharField(validators=[validators.MaxLengthValidator(200)]) char_min_length = serializers.CharField(validators=[validators.MinLengthValidator(100)]) # The following only apply for `integer` and `number` types: float_max_value = serializers.FloatField(validators=[validators.MaxValueValidator(200.0)]) float_min_value = serializers.FloatField(validators=[validators.MinValueValidator(100.0)]) float_decimal = serializers.FloatField( validators=[validators.DecimalValidator(max_digits=4, decimal_places=2)], ) integer_max_value = serializers.IntegerField(validators=[validators.MaxValueValidator(200)]) integer_min_value = serializers.IntegerField(validators=[validators.MinValueValidator(100)]) integer_decimal = serializers.FloatField( validators=[validators.DecimalValidator(max_digits=4, decimal_places=2)], ) decimal_max_value = serializers.DecimalField( max_digits=4, decimal_places=1, validators=[validators.MaxValueValidator(200)], ) decimal_min_value = serializers.DecimalField( max_digits=4, decimal_places=1, validators=[validators.MinValueValidator(100)], ) decimal_decimal = serializers.DecimalField( max_digits=4, decimal_places=1, validators=[validators.DecimalValidator(max_digits=4, decimal_places=2)], ) # The following only apply for `array` type: list_max_length = serializers.ListField(validators=[validators.MaxLengthValidator(200)]) list_min_length = serializers.ListField(validators=[validators.MinLengthValidator(100)]) # The following only apply for `object` type: dict_max_length = serializers.DictField(validators=[validators.MaxLengthValidator(200)]) dict_min_length = serializers.DictField(validators=[validators.MinLengthValidator(100)]) # Explicit test for rest_framework.fields.DurationField: age = serializers.DurationField(validators=[ validators.RegexValidator(r'^P\d+Y$'), validators.MaxLengthValidator(5), validators.MinLengthValidator(3), ]) # Tests for additional subclasses already handled by their superclass: array_max_length = serializers.ListField(validators=[postgres_validators.ArrayMaxLengthValidator(200)]) array_min_length = serializers.ListField(validators=[postgres_validators.ArrayMinLengthValidator(100)]) ascii_username = serializers.CharField(validators=[auth_validators.ASCIIUsernameValidator()]) unicode_username = serializers.CharField(validators=[auth_validators.UnicodeUsernameValidator()]) file_extension = serializers.CharField(validators=[validators.FileExtensionValidator(['.jpg', '.png'])]) integer_string = serializers.CharField(validators=[validators.integer_validator]) integer_list = serializers.CharField(validators=[validators.validate_comma_separated_integer_list]) class YSerializer(serializers.Serializer): # These validators are unsupported for the `string` type: char_max_value = serializers.CharField(validators=[validators.MaxValueValidator(200)]) char_min_value = serializers.CharField(validators=[validators.MinValueValidator(100)]) char_decimal = serializers.CharField( validators=[validators.DecimalValidator(max_digits=4, decimal_places=2)], ) # These validators are unsupported for the `integer` and `number` types: float_email = serializers.FloatField(validators=[validators.EmailValidator()]) float_url = serializers.FloatField(validators=[validators.URLValidator()]) float_regex = serializers.FloatField(validators=[validators.RegexValidator(r'\w+')]) float_max_length = serializers.FloatField(validators=[validators.MaxLengthValidator(200)]) float_min_length = serializers.FloatField(validators=[validators.MinLengthValidator(100)]) integer_email = serializers.IntegerField(validators=[validators.EmailValidator()]) integer_url = serializers.IntegerField(validators=[validators.URLValidator()]) integer_regex = serializers.IntegerField(validators=[validators.RegexValidator(r'\w+')]) integer_max_length = serializers.IntegerField(validators=[validators.MaxLengthValidator(200)]) integer_min_length = serializers.IntegerField(validators=[validators.MinLengthValidator(100)]) decimal_email = serializers.DecimalField( max_digits=4, decimal_places=1, validators=[validators.EmailValidator()], ) decimal_url = serializers.DecimalField( max_digits=4, decimal_places=1, validators=[validators.URLValidator()], ) decimal_regex = serializers.DecimalField( max_digits=4, decimal_places=1, validators=[validators.RegexValidator(r'\w+')], ) decimal_max_length = serializers.DecimalField( max_digits=4, decimal_places=1, validators=[validators.MaxLengthValidator(200)], ) decimal_min_length = serializers.DecimalField( max_digits=4, decimal_places=1, validators=[validators.MinLengthValidator(100)], ) # These validators are unsupported for the `array` type: list_email = serializers.ListField(validators=[validators.EmailValidator()]) list_url = serializers.ListField(validators=[validators.URLValidator()]) list_regex = serializers.ListField(validators=[validators.RegexValidator(r'\w+')]) list_max_value = serializers.ListField(validators=[validators.MaxValueValidator(200)]) list_min_value = serializers.ListField(validators=[validators.MinValueValidator(100)]) list_decimal = serializers.ListField( validators=[validators.DecimalValidator(max_digits=4, decimal_places=2)], ) # These validators are unsupported for the `object` type: dict_email = serializers.DictField(validators=[validators.EmailValidator()]) dict_url = serializers.DictField(validators=[validators.URLValidator()]) dict_regex = serializers.DictField(validators=[validators.RegexValidator(r'\w+')]) dict_max_value = serializers.DictField(validators=[validators.MaxValueValidator(200)]) dict_min_value = serializers.DictField(validators=[validators.MinValueValidator(100)]) dict_decimal = serializers.DictField( validators=[validators.DecimalValidator(max_digits=4, decimal_places=2)], ) # These validators are unsupported for the `boolean` type: boolean_email = serializers.BooleanField(validators=[validators.EmailValidator()]) boolean_url = serializers.BooleanField(validators=[validators.URLValidator()]) boolean_regex = serializers.BooleanField(validators=[validators.RegexValidator(r'\w+')]) boolean_max_length = serializers.BooleanField(validators=[validators.MaxLengthValidator(200)]) boolean_min_length = serializers.BooleanField(validators=[validators.MinLengthValidator(100)]) boolean_max_value = serializers.BooleanField(validators=[validators.MaxValueValidator(200)]) boolean_min_value = serializers.BooleanField(validators=[validators.MinValueValidator(100)]) boolean_decimal = serializers.BooleanField( validators=[validators.DecimalValidator(max_digits=4, decimal_places=2)], ) # Explicit test for rest_framework.fields.DurationField: duration_max_value = serializers.DurationField(validators=[validators.MaxValueValidator(200)]) duration_min_value = serializers.DurationField(validators=[validators.MinValueValidator(100)]) duration_decimal = serializers.DurationField( validators=[validators.DecimalValidator(max_digits=4, decimal_places=2)], ) @extend_schema(request=XSerializer, responses=XSerializer) @api_view(['POST']) def view_func_x(request, format=None): pass # pragma: no cover @extend_schema(request=YSerializer, responses=YSerializer) @api_view(['POST']) def view_func_y(request, format=None): pass # pragma: no cover schema = generate_schema(None, patterns=[path('x', view_func_x), path('y', view_func_y)]) if sys.version_info < (3, 7): # In Python < 3.7, re.escape() escapes more characters than necessary. field = schema['components']['schemas']['X']['properties']['integer_list'] field['pattern'] = field['pattern'].replace(r'\,', ',') assert_schema(schema, 'tests/test_validators.yml') def test_nested_validators(): class XSerializer(serializers.Serializer): list_field = serializers.ListField( child=serializers.IntegerField( validators=[validators.MaxValueValidator(999)], ), validators=[validators.MaxLengthValidator(5)], ) dict_field = serializers.DictField( child=serializers.IntegerField( validators=[validators.MaxValueValidator(999)], ), ) @extend_schema(request=XSerializer, responses=XSerializer) @api_view(['POST']) def view_func(request, format=None): pass # pragma: no cover schema = generate_schema('x', view_function=view_func) properties = schema['components']['schemas']['X']['properties'] assert properties['list_field']['maxItems'] == 5 assert properties['list_field']['items']['maximum'] == 999 assert properties['dict_field']['additionalProperties']['maximum'] == 999 @pytest.mark.parametrize('instance,expected', [ ( serializers.DictField(validators=[validators.MaxLengthValidator(150), validators.MaxLengthValidator(200)]), {'type': 'object', 'additionalProperties': {}, 'maxProperties': 150}, ), ( serializers.DictField(validators=[validators.MinLengthValidator(150), validators.MinLengthValidator(100)]), {'type': 'object', 'additionalProperties': {}, 'minProperties': 150}, ), ( serializers.ListField(max_length=150, validators=[validators.MaxLengthValidator(200)]), {'type': 'array', 'items': {}, 'maxItems': 150}, ), ( serializers.ListField(min_length=150, validators=[validators.MinLengthValidator(100)]), {'type': 'array', 'items': {}, 'minItems': 150}, ), ( serializers.ListField(max_length=200, validators=[validators.MaxLengthValidator(150)]), {'type': 'array', 'items': {}, 'maxItems': 150}, ), ( serializers.ListField(min_length=100, validators=[validators.MinLengthValidator(150)]), {'type': 'array', 'items': {}, 'minItems': 150}, ), ( serializers.ListField(validators=[validators.MaxLengthValidator(150), validators.MaxLengthValidator(200)]), {'type': 'array', 'items': {}, 'maxItems': 150}, ), ( serializers.ListField(validators=[validators.MinLengthValidator(150), validators.MinLengthValidator(100)]), {'type': 'array', 'items': {}, 'minItems': 150}, ), ( serializers.CharField(max_length=150, validators=[validators.MaxLengthValidator(200)]), {'type': 'string', 'maxLength': 150}, ), ( serializers.CharField(min_length=150, validators=[validators.MinLengthValidator(100)]), {'type': 'string', 'minLength': 150}, ), ( serializers.CharField(max_length=200, validators=[validators.MaxLengthValidator(150)]), {'type': 'string', 'maxLength': 150}, ), ( serializers.CharField(min_length=100, validators=[validators.MinLengthValidator(150)]), {'type': 'string', 'minLength': 150}, ), ( serializers.CharField(validators=[validators.MaxLengthValidator(150), validators.MaxLengthValidator(200)]), {'type': 'string', 'maxLength': 150}, ), ( serializers.CharField(validators=[validators.MinLengthValidator(150), validators.MinLengthValidator(100)]), {'type': 'string', 'minLength': 150}, ), ( serializers.IntegerField(max_value=150, validators=[validators.MaxValueValidator(200)]), {'type': 'integer', 'maximum': 150}, ), ( serializers.IntegerField(min_value=150, validators=[validators.MinValueValidator(100)]), {'type': 'integer', 'minimum': 150}, ), ( serializers.IntegerField(max_value=200, validators=[validators.MaxValueValidator(150)]), {'type': 'integer', 'maximum': 150}, ), ( serializers.IntegerField(min_value=100, validators=[validators.MinValueValidator(150)]), {'type': 'integer', 'minimum': 150}, ), ( serializers.IntegerField(validators=[validators.MaxValueValidator(150), validators.MaxValueValidator(200)]), {'type': 'integer', 'maximum': 150}, ), ( serializers.IntegerField(validators=[validators.MinValueValidator(150), validators.MinValueValidator(100)]), {'type': 'integer', 'minimum': 150}, ), ( serializers.DecimalField(max_digits=3, decimal_places=1, validators=[validators.MaxValueValidator(50)]), {'type': 'number', 'format': 'double', 'maximum': 50, 'minimum': -100, 'exclusiveMinimum': True}, ), ( serializers.DecimalField(max_digits=3, decimal_places=1, validators=[validators.MinValueValidator(-50)]), {'type': 'number', 'format': 'double', 'maximum': 100, 'minimum': -50, 'exclusiveMaximum': True}, ), ( serializers.DecimalField(max_digits=3, decimal_places=1, validators=[validators.MaxValueValidator(150)]), {'type': 'number', 'format': 'double', 'maximum': 100, 'minimum': -100, 'exclusiveMinimum': True}, ), ( serializers.DecimalField(max_digits=3, decimal_places=1, validators=[validators.MinValueValidator(-150)]), {'type': 'number', 'format': 'double', 'maximum': 100, 'minimum': -100, 'exclusiveMaximum': True}, ), ( serializers.DecimalField( max_digits=4, decimal_places=1, validators=[validators.DecimalValidator(max_digits=3, decimal_places=1)], ), { 'type': 'number', 'format': 'double', 'maximum': 100, 'minimum': -100, 'exclusiveMaximum': True, 'exclusiveMinimum': True, }, ), ( serializers.DecimalField( max_digits=3, decimal_places=1, validators=[validators.DecimalValidator(max_digits=4, decimal_places=1)], ), { 'type': 'number', 'format': 'double', 'maximum': 100, 'minimum': -100, 'exclusiveMaximum': True, 'exclusiveMinimum': True, }, ), ( serializers.DecimalField( max_digits=3, decimal_places=1, validators=[validators.DecimalValidator(max_digits=2, decimal_places=1), validators.MaxValueValidator(5)], ), {'type': 'number', 'format': 'double', 'maximum': 5, 'minimum': -10, 'exclusiveMinimum': True}, ), ( serializers.DecimalField( max_digits=3, decimal_places=1, validators=[validators.DecimalValidator(max_digits=2, decimal_places=1), validators.MinValueValidator(-5)], ), {'type': 'number', 'format': 'double', 'maximum': 10, 'minimum': -5, 'exclusiveMaximum': True}, ), ]) @mock.patch('rest_framework.settings.api_settings.COERCE_DECIMAL_TO_STRING', False) def test_validation_constrained(instance, expected): class XSerializer(serializers.Serializer): field = instance @extend_schema(request=XSerializer, responses=XSerializer) @api_view(['POST']) def view_func(request, format=None): pass # pragma: no cover schema = generate_schema('x', view_function=view_func) assert schema['components']['schemas']['X']['properties']['field'] == expected def test_timedelta_in_validator(): class XSerializer(serializers.Serializer): field = serializers.DurationField( validators=[validators.MaxValueValidator(timedelta(seconds=3600))], ) @extend_schema(request=XSerializer, responses=XSerializer) @api_view(['POST']) def view_func(request, format=None): pass # pragma: no cover # `DurationField` values and `timedelta` serialize to `string` type so `maximum` is invalid. schema = generate_schema('x', view_function=view_func) assert 'maximum' not in schema['components']['schemas']['X']['properties']['field'] @pytest.mark.parametrize('pattern,expected', [ (r'\xff', r'\u00ff'), # Unify escape characters. (r'\Ato\Z', r'^to$'), # Switch to ECMA anchors. ]) def test_regex_validator_tweaks(pattern, expected): class XSerializer(serializers.Serializer): field = serializers.CharField(validators=[validators.RegexValidator(pattern)]) @extend_schema(request=XSerializer, responses=XSerializer) @api_view(['POST']) def view_func(request, format=None): pass # pragma: no cover schema = generate_schema('x', view_function=view_func) field = schema['components']['schemas']['X']['properties']['field'] assert field['pattern'] == expected drf-spectacular-0.27.0/tests/test_validators.yml000066400000000000000000000213471453572150400217700ustar00rootroot00000000000000openapi: 3.0.3 info: title: '' version: 0.0.0 paths: /x: post: operationId: x_create tags: - x requestBody: content: application/json: schema: $ref: '#/components/schemas/X' application/x-www-form-urlencoded: schema: $ref: '#/components/schemas/X' multipart/form-data: schema: $ref: '#/components/schemas/X' required: true security: - cookieAuth: [] - basicAuth: [] - {} responses: '200': content: application/json: schema: $ref: '#/components/schemas/X' description: '' /y: post: operationId: y_create tags: - y requestBody: content: application/json: schema: $ref: '#/components/schemas/Y' application/x-www-form-urlencoded: schema: $ref: '#/components/schemas/Y' multipart/form-data: schema: $ref: '#/components/schemas/Y' required: true security: - cookieAuth: [] - basicAuth: [] - {} responses: '200': content: application/json: schema: $ref: '#/components/schemas/Y' description: '' components: schemas: X: type: object properties: char_email: type: string format: email char_url: type: string format: uri char_regex: type: string pattern: \w+ char_max_length: type: string maxLength: 200 char_min_length: type: string minLength: 100 float_max_value: type: number format: double maximum: 200.0 float_min_value: type: number format: double minimum: 100.0 float_decimal: type: number format: double maximum: 100 exclusiveMaximum: true minimum: -100 exclusiveMinimum: true integer_max_value: type: integer maximum: 200 integer_min_value: type: integer minimum: 100 integer_decimal: type: number format: double maximum: 100 exclusiveMaximum: true minimum: -100 exclusiveMinimum: true decimal_max_value: type: number format: double maximum: 200 minimum: -1000 exclusiveMinimum: true decimal_min_value: type: number format: double maximum: 1000 minimum: 100 exclusiveMaximum: true decimal_decimal: type: number format: double maximum: 100 minimum: -100 exclusiveMaximum: true exclusiveMinimum: true list_max_length: type: array items: {} maxItems: 200 list_min_length: type: array items: {} minItems: 100 dict_max_length: type: object additionalProperties: {} maxProperties: 200 dict_min_length: type: object additionalProperties: {} minProperties: 100 age: type: string pattern: ^P\d+Y$ maxLength: 5 minLength: 3 array_max_length: type: array items: {} maxItems: 200 array_min_length: type: array items: {} minItems: 100 ascii_username: type: string pattern: ^[\w.@+-]+$ unicode_username: type: string pattern: ^[\w.@+-]+$ file_extension: type: string pattern: (?:\.jpg|\.png)$ integer_string: type: string pattern: ^-?\d+$ integer_list: type: string pattern: ^\d+(?:,\d+)*$ required: - age - array_max_length - array_min_length - ascii_username - char_email - char_max_length - char_min_length - char_regex - char_url - decimal_decimal - decimal_max_value - decimal_min_value - dict_max_length - dict_min_length - file_extension - float_decimal - float_max_value - float_min_value - integer_decimal - integer_list - integer_max_value - integer_min_value - integer_string - list_max_length - list_min_length - unicode_username Y: type: object properties: char_max_value: type: string char_min_value: type: string char_decimal: type: string float_email: type: number format: double float_url: type: number format: double float_regex: type: number format: double float_max_length: type: number format: double float_min_length: type: number format: double integer_email: type: integer integer_url: type: integer integer_regex: type: integer integer_max_length: type: integer integer_min_length: type: integer decimal_email: type: number format: double maximum: 1000 minimum: -1000 exclusiveMaximum: true exclusiveMinimum: true decimal_url: type: number format: double maximum: 1000 minimum: -1000 exclusiveMaximum: true exclusiveMinimum: true decimal_regex: type: number format: double maximum: 1000 minimum: -1000 exclusiveMaximum: true exclusiveMinimum: true decimal_max_length: type: number format: double maximum: 1000 minimum: -1000 exclusiveMaximum: true exclusiveMinimum: true decimal_min_length: type: number format: double maximum: 1000 minimum: -1000 exclusiveMaximum: true exclusiveMinimum: true list_email: type: array items: {} list_url: type: array items: {} list_regex: type: array items: {} list_max_value: type: array items: {} list_min_value: type: array items: {} list_decimal: type: array items: {} dict_email: type: object additionalProperties: {} dict_url: type: object additionalProperties: {} dict_regex: type: object additionalProperties: {} dict_max_value: type: object additionalProperties: {} dict_min_value: type: object additionalProperties: {} dict_decimal: type: object additionalProperties: {} boolean_email: type: boolean boolean_url: type: boolean boolean_regex: type: boolean boolean_max_length: type: boolean boolean_min_length: type: boolean boolean_max_value: type: boolean boolean_min_value: type: boolean boolean_decimal: type: boolean duration_max_value: type: string duration_min_value: type: string duration_decimal: type: string required: - boolean_decimal - boolean_email - boolean_max_length - boolean_max_value - boolean_min_length - boolean_min_value - boolean_regex - boolean_url - char_decimal - char_max_value - char_min_value - decimal_email - decimal_max_length - decimal_min_length - decimal_regex - decimal_url - dict_decimal - dict_email - dict_max_value - dict_min_value - dict_regex - dict_url - duration_decimal - duration_max_value - duration_min_value - float_email - float_max_length - float_min_length - float_regex - float_url - integer_email - integer_max_length - integer_min_length - integer_regex - integer_url - list_decimal - list_email - list_max_value - list_min_value - list_regex - list_url securitySchemes: basicAuth: type: http scheme: basic cookieAuth: type: apiKey in: cookie name: sessionid drf-spectacular-0.27.0/tests/test_versioning.py000066400000000000000000000234151453572150400216300ustar00rootroot00000000000000# type: ignore import re from unittest import mock import pytest import yaml from django.conf.urls import include from django.db import models from django.urls import path, re_path from rest_framework import generics, mixins, routers, serializers, viewsets from rest_framework.test import APIClient, APIRequestFactory from rest_framework.versioning import AcceptHeaderVersioning, NamespaceVersioning, URLPathVersioning from drf_spectacular.generators import SchemaGenerator from drf_spectacular.utils import extend_schema from drf_spectacular.validation import validate_schema from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView from tests import assert_schema from tests.models import SimpleModel class Xv1Serializer(serializers.Serializer): id = serializers.IntegerField() class Xv2Serializer(serializers.Serializer): id = serializers.UUIDField() class PathVersioningViewset(mixins.ListModelMixin, viewsets.GenericViewSet): versioning_class = URLPathVersioning queryset = SimpleModel.objects.all() @extend_schema(responses=Xv1Serializer, versions=['v1']) @extend_schema(responses=Xv2Serializer, versions=['v2']) def list(self, request, *args, **kwargs): pass # pragma: no cover @extend_schema(responses=Xv1Serializer, versions=['v1']) @extend_schema(responses=Xv2Serializer, versions=['v2']) def retrieve(self, request, *args, **kwargs): pass # pragma: no cover class NamespaceVersioningViewset(PathVersioningViewset): versioning_class = NamespaceVersioning class AcceptHeaderVersioningViewset(PathVersioningViewset): versioning_class = AcceptHeaderVersioning class PathVersioningViewset2(mixins.ListModelMixin, viewsets.GenericViewSet): versioning_class = URLPathVersioning queryset = SimpleModel.objects.all() def list(self, request, *args, **kwargs): pass # pragma: no cover def retrieve(self, request, *args, **kwargs): pass # pragma: no cover def get_serializer_class(self): if self.request.version == 'v2': return Xv2Serializer return Xv1Serializer class NamespaceVersioningViewset2(PathVersioningViewset2): versioning_class = NamespaceVersioning class AcceptHeaderVersioningViewset2(PathVersioningViewset2): versioning_class = AcceptHeaderVersioning @pytest.mark.parametrize('viewset_cls', [PathVersioningViewset, PathVersioningViewset2]) @pytest.mark.parametrize('version', ['v1', 'v2']) def test_url_path_versioning(no_warnings, viewset_cls, version): router = routers.SimpleRouter() router.register('x', viewset_cls, basename='x') generator = SchemaGenerator( patterns=[re_path(r'^(?P[v1|v2]+)/', include((router.urls, 'x')))], api_version=version, ) schema = generator.get_schema(request=None, public=True) assert_schema(schema, f'tests/test_versioning_{version}.yml') @pytest.mark.parametrize('viewset_cls', [NamespaceVersioningViewset, NamespaceVersioningViewset2]) @pytest.mark.parametrize('version', ['v1', 'v2']) def test_namespace_versioning(no_warnings, viewset_cls, version): router = routers.SimpleRouter() router.register('x', viewset_cls, basename='x') generator = SchemaGenerator( patterns=[ path('v1/', include((router.urls, 'v1'))), path('v2/', include((router.urls, 'v2'))), ], api_version=version, ) schema = generator.get_schema(request=None, public=True) assert_schema(schema, f'tests/test_versioning_{version}.yml') class LookupModel(models.Model): """ test_namespace_versioning_urlpatterns_simplification """ field = models.IntegerField() @pytest.mark.parametrize(['path_func', 'path_str', 'pattern', ], [ (path, '{id}/', '/'), (path, '{id}/', '/'), (re_path, '{id}/', r'(?P[0-9A-Fa-f-]+)/'), (re_path, '{id}/', r'(?P[^/.]+)/$'), (re_path, '{id}/', r'(?P[a-z]{2}(-[a-z]{2})?)/'), (re_path, '{field}/t/{id}/', r'^(?P[^/.]+)/t/(?P[^/.]+)/'), (re_path, '{field}/t/{id}/', r'^(?P[A-Z\(\)]+)/t/(?P[^/.]+)/'), ]) def test_namespace_versioning_urlpatterns_simplification(no_warnings, path_func, path_str, pattern): class LookupSerializer(serializers.ModelSerializer): class Meta: model = LookupModel fields = '__all__' class NamespaceVersioningAPIView(generics.RetrieveUpdateDestroyAPIView): versioning_class = NamespaceVersioning serializer_class = LookupSerializer queryset = LookupModel.objects.all() # make sure regex are valid if path_func == re_path: re.compile(pattern) patterns_v1 = [path_func(pattern, NamespaceVersioningAPIView.as_view())] generator = SchemaGenerator( patterns=[path('v1//', include((patterns_v1, 'v1')))], api_version='v1', ) schema = generator.get_schema(request=None, public=True) assert ('/v1/{some_param}/' + path_str) in schema['paths'] @pytest.mark.parametrize('viewset_cls', [AcceptHeaderVersioningViewset, AcceptHeaderVersioningViewset2]) @pytest.mark.parametrize('version', ['v1', 'v2']) @pytest.mark.parametrize('with_request', [True, False]) def test_accept_header_versioning(no_warnings, viewset_cls, version, with_request): router = routers.SimpleRouter() router.register('x', viewset_cls, basename='x') generator = SchemaGenerator( patterns=[ path('', include((router.urls, 'x'))), ], api_version=version, ) if with_request: view = SpectacularAPIView( versioning_class=AcceptHeaderVersioning, ) factory = APIRequestFactory() request = factory.get('x', content_type='application/vnd.oai.openapi+json') request = view.initialize_request(request) else: request = None schema = generator.get_schema(request=request, public=True) assert_schema(schema, f'tests/test_versioning_accept_{version}.yml') urlpatterns_namespace = [ path('x/', NamespaceVersioningViewset.as_view({'get': 'list'})), path('schema/', SpectacularAPIView.as_view( versioning_class=NamespaceVersioning ), name='schema-nv-versioned'), path('schema/ui', SpectacularSwaggerView.as_view( versioning_class=NamespaceVersioning, url_name='schema-nv-versioned' )), ] urlpatterns_path = [ path('x/', PathVersioningViewset2.as_view({'get': 'list'})), path('schema/', SpectacularAPIView.as_view( versioning_class=URLPathVersioning ), name='schema-pv-versioned'), path('schema/ui', SpectacularSwaggerView.as_view( versioning_class=URLPathVersioning, url_name='schema-pv-versioned' )), ] urlpatterns_accept_header = [ path('x/', AcceptHeaderVersioningViewset2.as_view({'get': 'list'})), path('schema/', SpectacularAPIView.as_view( versioning_class=AcceptHeaderVersioning ), name='schema-ahv-versioned'), path('schema/ui', SpectacularSwaggerView.as_view( versioning_class=AcceptHeaderVersioning, url_name='schema-ahv-versioned' )), ] urlpatterns = [ # path versioning re_path(r'^api/pv/(?P[v1|v2]+)/', include(urlpatterns_path)), # namespace versioning path('api/nv/v1/', include((urlpatterns_namespace, 'v1'))), path('api/nv/v2/', include((urlpatterns_namespace, 'v2'))), # accept header versioning path('api/ahv/', include((urlpatterns_accept_header, 'x'))), # all unversioned path('api/schema/', SpectacularAPIView.as_view()), # manually versioned schema view that is in itself unversioned path('api/schema-v2/', SpectacularAPIView.as_view(api_version='v2')), ] @pytest.mark.parametrize(['url', 'path_count'], [ ('/api/nv/v2/schema/', 8), # v2 nv + v2 pv + v2 ahv + unversioned ('/api/pv/v1/schema/', 8), # v1 nv + v1 pv + v1 ahv + unversioned ('/api/schema-v2/', 8), # v2 nv + v2 pv + v2 ahv + unversioned ('/api/schema/', 2), # unversioned schema ('/api/schema/?version=v2', 8), # versioned with query param ]) @pytest.mark.urls(__name__) def test_spectacular_view_versioning(no_warnings, url, path_count): response = APIClient().get(url) assert response.status_code == 200 schema = yaml.load(response.content, Loader=yaml.SafeLoader) validate_schema(schema) assert len(schema['paths']) == path_count @pytest.mark.parametrize('version', ['v1', 'v2']) @pytest.mark.urls(__name__) def test_spectacular_view_accept_header_versioning(no_warnings, version): response = APIClient().get( '/api/ahv/schema/', HTTP_ACCEPT=f'application/json; version={version}' ) assert response.status_code == 200, response.content schema = yaml.load(response.content, Loader=yaml.SafeLoader) validate_schema(schema) assert schema['info']['version'] == f'0.0.0 ({version})' assert len(schema['paths']) == 8 @pytest.mark.parametrize(['url', 'schema_url'], [ ('/api/nv/v1/schema/ui', b'/api/nv/v1/schema/'), ('/api/nv/v2/schema/ui', b'/api/nv/v2/schema/'), ('/api/pv/v1/schema/ui', b'/api/pv/v1/schema/'), ('/api/pv/v2/schema/ui', b'/api/pv/v2/schema/'), ]) @pytest.mark.urls(__name__) def test_spectacular_ui_view_versioning(no_warnings, url, schema_url): response = APIClient().get(url) assert schema_url in response.content @pytest.mark.urls(__name__) def test_spectacular_versioning_info_object_variations(no_warnings): # with default VERSION 0.0.0 response = APIClient().get('/api/nv/v2/schema/') assert b'version: 0.0.0 (v2)\n' in response.content response = APIClient().get('/api/schema/') assert b'version: 0.0.0\n' in response.content with mock.patch('drf_spectacular.settings.spectacular_settings.VERSION', None): response = APIClient().get('/api/nv/v2/schema/') assert b'version: v2\n' in response.content response = APIClient().get('/api/schema/') assert b"version: ''\n" in response.content drf-spectacular-0.27.0/tests/test_versioning_accept_v1.yml000066400000000000000000000023331453572150400237220ustar00rootroot00000000000000openapi: 3.0.3 info: title: '' version: 0.0.0 (v1) paths: /x/: get: operationId: x_list tags: - x security: - cookieAuth: [] - basicAuth: [] - {} responses: '200': content: application/json; version=v1: schema: type: array items: $ref: '#/components/schemas/Xv1' description: '' /x/{id}/: get: operationId: x_retrieve parameters: - in: path name: id schema: type: integer description: A unique integer value identifying this simple model. required: true tags: - x security: - cookieAuth: [] - basicAuth: [] - {} responses: '200': content: application/json; version=v1: schema: $ref: '#/components/schemas/Xv1' description: '' components: schemas: Xv1: type: object properties: id: type: integer required: - id securitySchemes: basicAuth: type: http scheme: basic cookieAuth: type: apiKey in: cookie name: sessionid drf-spectacular-0.27.0/tests/test_versioning_accept_v2.yml000066400000000000000000000023611453572150400237240ustar00rootroot00000000000000openapi: 3.0.3 info: title: '' version: 0.0.0 (v2) paths: /x/: get: operationId: x_list tags: - x security: - cookieAuth: [] - basicAuth: [] - {} responses: '200': content: application/json; version=v2: schema: type: array items: $ref: '#/components/schemas/Xv2' description: '' /x/{id}/: get: operationId: x_retrieve parameters: - in: path name: id schema: type: integer description: A unique integer value identifying this simple model. required: true tags: - x security: - cookieAuth: [] - basicAuth: [] - {} responses: '200': content: application/json; version=v2: schema: $ref: '#/components/schemas/Xv2' description: '' components: schemas: Xv2: type: object properties: id: type: string format: uuid required: - id securitySchemes: basicAuth: type: http scheme: basic cookieAuth: type: apiKey in: cookie name: sessionid drf-spectacular-0.27.0/tests/test_versioning_v1.yml000066400000000000000000000023211453572150400224000ustar00rootroot00000000000000openapi: 3.0.3 info: title: '' version: 0.0.0 (v1) paths: /v1/x/: get: operationId: v1_x_list tags: - v1 security: - cookieAuth: [] - basicAuth: [] - {} responses: '200': content: application/json: schema: type: array items: $ref: '#/components/schemas/Xv1' description: '' /v1/x/{id}/: get: operationId: v1_x_retrieve parameters: - in: path name: id schema: type: integer description: A unique integer value identifying this simple model. required: true tags: - v1 security: - cookieAuth: [] - basicAuth: [] - {} responses: '200': content: application/json: schema: $ref: '#/components/schemas/Xv1' description: '' components: schemas: Xv1: type: object properties: id: type: integer required: - id securitySchemes: basicAuth: type: http scheme: basic cookieAuth: type: apiKey in: cookie name: sessionid drf-spectacular-0.27.0/tests/test_versioning_v2.yml000066400000000000000000000023471453572150400224110ustar00rootroot00000000000000openapi: 3.0.3 info: title: '' version: 0.0.0 (v2) paths: /v2/x/: get: operationId: v2_x_list tags: - v2 security: - cookieAuth: [] - basicAuth: [] - {} responses: '200': content: application/json: schema: type: array items: $ref: '#/components/schemas/Xv2' description: '' /v2/x/{id}/: get: operationId: v2_x_retrieve parameters: - in: path name: id schema: type: integer description: A unique integer value identifying this simple model. required: true tags: - v2 security: - cookieAuth: [] - basicAuth: [] - {} responses: '200': content: application/json: schema: $ref: '#/components/schemas/Xv2' description: '' components: schemas: Xv2: type: object properties: id: type: string format: uuid required: - id securitySchemes: basicAuth: type: http scheme: basic cookieAuth: type: apiKey in: cookie name: sessionid drf-spectacular-0.27.0/tests/test_view.py000066400000000000000000000164661453572150400204270ustar00rootroot00000000000000from unittest import mock import pytest import yaml from django import __version__ as DJANGO_VERSION from django.http import HttpResponseRedirect from django.urls import path from rest_framework.decorators import api_view from rest_framework.response import Response from rest_framework.test import APIClient from drf_spectacular.types import OpenApiTypes from drf_spectacular.utils import extend_schema from drf_spectacular.validation import validate_schema from drf_spectacular.views import ( SpectacularAPIView, SpectacularRedocView, SpectacularSwaggerOauthRedirectView, SpectacularSwaggerSplitView, SpectacularSwaggerView, ) @extend_schema(responses=OpenApiTypes.FLOAT) @api_view(http_method_names=['GET']) def pi(request): return Response(3.1415) urlpatterns_v1 = [path('api/v1/pi/', pi)] urlpatterns_v1.append( path('api/v1/schema/', SpectacularAPIView.as_view(urlconf=urlpatterns_v1)) ) urlpatterns_v2 = [ path('api/v2/pi/', pi), path('api/v2/pi-fast/', pi), path('api/v2/schema/swagger-ui/', SpectacularSwaggerView.as_view(), name='swagger'), path( "api/v1/schema/swagger-ui/oauth2-redirect.html", SpectacularSwaggerOauthRedirectView.as_view(), name="swagger-oauth-redirect"), path('api/v2/schema/swagger-ui-alt/', SpectacularSwaggerSplitView.as_view(), name='swagger-alt'), path('api/v2/schema/redoc/', SpectacularRedocView.as_view(), name='redoc'), ] urlpatterns_v2.append( path('api/v2/schema/', SpectacularAPIView.as_view(urlconf=urlpatterns_v2), name='schema'), ) urlpatterns_str_import = [ path('api/schema-str1/', SpectacularAPIView.as_view(urlconf=['tests.test_view']), name='schema_str1'), path('api/schema-str2/', SpectacularAPIView.as_view(urlconf='tests.test_view'), name='schema_str2'), path('api/schema-err1/', SpectacularAPIView.as_view(urlconf=['tests.error']), name='schema_err1'), path('api/schema-err2/', SpectacularAPIView.as_view(urlconf='tests.error'), name='schema_err2'), ] urlpatterns = urlpatterns_v1 + urlpatterns_v2 + urlpatterns_str_import @pytest.mark.urls(__name__) def test_spectacular_view(no_warnings): response = APIClient().get('/api/v1/schema/') assert response.status_code == 200 assert response.content.startswith(b'openapi: 3.0.3\n') assert response.accepted_media_type == 'application/vnd.oai.openapi' if DJANGO_VERSION > '3': assert response.headers.get('CONTENT-DISPOSITION') == 'inline; filename="schema.yaml"' schema = yaml.load(response.content, Loader=yaml.SafeLoader) validate_schema(schema) assert len(schema['paths']) == 2 @pytest.mark.urls(__name__) def test_spectacular_view_custom_urlconf(no_warnings): response = APIClient().get('/api/v2/schema/') assert response.status_code == 200 schema = yaml.load(response.content, Loader=yaml.SafeLoader) validate_schema(schema) assert len(schema['paths']) == 3 response = APIClient().get('/api/v2/pi-fast/') assert response.status_code == 200 assert response.content == b'3.1415' @pytest.mark.parametrize(['accept', 'format', 'indent'], [ ('application/vnd.oai.openapi', 'yaml', None), ('application/yaml', 'yaml', None), ('application/vnd.oai.openapi+json', 'json', 4), ('application/json', 'json', 4), ('application/json; indent=8', 'json', 8), ]) @pytest.mark.urls(__name__) def test_spectacular_view_accept(accept, format, indent): response = APIClient().get('/api/v1/schema/', HTTP_ACCEPT=accept) assert response.status_code == 200 assert response.accepted_media_type == accept if format == 'json': assert response.content.startswith(b'{\n' + indent * b' ' + b'"openapi": "3.0.3"') if format == 'yaml': assert response.content.startswith(b'openapi: 3.0.3\n') @pytest.mark.urls(__name__) def test_spectacular_view_accept_unknown(no_warnings): response = APIClient().get('/api/v1/schema/', HTTP_ACCEPT='application/unknown') assert response.status_code == 406 assert response.content == ( b'detail:\n string: Could not satisfy the request Accept header.\n' b' code: not_acceptable\n' ) @pytest.mark.parametrize('ui', ['redoc', 'swagger-ui']) @pytest.mark.urls(__name__) def test_spectacular_ui_view(no_warnings, ui): from drf_spectacular.settings import spectacular_settings response = APIClient().get(f'/api/v2/schema/{ui}/') assert response.status_code == 200 assert response.content.startswith(b'') if ui == 'redoc': assert b'Redoc' in response.content assert spectacular_settings.REDOC_DIST.encode() in response.content else: assert b'Swagger' in response.content assert spectacular_settings.SWAGGER_UI_DIST.encode() in response.content assert b'"/api/v2/schema/"' in response.content @pytest.mark.urls(__name__) def test_spectacular_swagger_ui_alternate(no_warnings): # first request for the html response = APIClient().get('/api/v2/schema/swagger-ui-alt/') assert response.status_code == 200 assert response.content.startswith(b'') assert b'"/api/v2/schema/swagger-ui-alt/?script="' in response.content # second request to obtain js swagger config (CSP self) response = APIClient().get('/api/v2/schema/swagger-ui-alt/?script=') assert response.status_code == 200 assert b'"/api/v2/schema/"' in response.content @mock.patch( 'drf_spectacular.settings.spectacular_settings.SWAGGER_UI_SETTINGS', '{"deepLinking": true}' ) @pytest.mark.urls(__name__) def test_spectacular_ui_with_raw_settings(no_warnings): response = APIClient().get('/api/v2/schema/swagger-ui/') assert response.status_code == 200 assert b'const swaggerSettings = {"deepLinking": true};\n' in response.content @pytest.mark.urls(__name__) def test_spectacular_ui_param_passthrough(no_warnings): response = APIClient().get('/api/v2/schema/swagger-ui/?foo=bar&lang=jp&version=v2') assert response.status_code == 200 assert b'url: "/api/v2/schema/?lang\\u003Djp\\u0026version\\u003Dv2"' in response.content @pytest.mark.parametrize('url', ['/api/schema-str1/', '/api/schema-str2/']) @pytest.mark.urls(__name__) def test_spectacular_urlconf_module_list_import(no_warnings, url): response = APIClient().get(url) assert response.status_code == 200 assert b'/api/v1/pi/' in response.content assert b'/api/v2/pi/' in response.content @pytest.mark.parametrize('url', ['/api/schema-err1/', '/api/schema-err2/']) @pytest.mark.urls(__name__) def test_spectacular_urlconf_module_list_import_error(no_warnings, url): with pytest.raises(ModuleNotFoundError): APIClient().get(url) @pytest.mark.parametrize('get_params', ['', 'code=foobar123&state=xyz&session_state=hello-world']) @pytest.mark.urls(__name__) def test_swagger_oauth_redirect_view(get_params): # act response = APIClient().get('/api/v1/schema/swagger-ui/oauth2-redirect.html?' + get_params) # assert assert response.status_code == 302 if isinstance(response, HttpResponseRedirect): # older django versions test client directly returns the response instance assert response.url == '/static/drf_spectacular_sidecar/swagger-ui-dist/oauth2-redirect.html?' + get_params else: assert response.headers['Location'] ==\ '/static/drf_spectacular_sidecar/swagger-ui-dist/oauth2-redirect.html?' + get_params drf-spectacular-0.27.0/tests/test_warnings.py000066400000000000000000000420111453572150400212660ustar00rootroot00000000000000from typing import Union from unittest import mock import pytest from django.db import models from django.urls import path from rest_framework import mixins, serializers, views, viewsets from rest_framework.authentication import BaseAuthentication from rest_framework.decorators import action, api_view from rest_framework.schemas import AutoSchema as DRFAutoSchema from rest_framework.views import APIView from drf_spectacular.types import OpenApiTypes from drf_spectacular.utils import ( OpenApiParameter, OpenApiRequest, PolymorphicProxySerializer, extend_schema, extend_schema_view, inline_serializer, ) from tests import generate_schema from tests.models import SimpleModel, SimpleSerializer def test_serializer_name_reuse(capsys): from rest_framework import routers router = routers.SimpleRouter() def x1(): class XSerializer(serializers.Serializer): uuid = serializers.UUIDField() return XSerializer def x2(): class XSerializer(serializers.Serializer): integer = serializers.IntegerField() return XSerializer class X1Viewset(mixins.ListModelMixin, viewsets.GenericViewSet): serializer_class = x1() router.register('x1', X1Viewset, basename='x1') class X2Viewset(mixins.ListModelMixin, viewsets.GenericViewSet): serializer_class = x2() router.register('x2', X2Viewset, basename='x2') generate_schema(None, patterns=router.urls) stderr = capsys.readouterr().err assert 'Encountered 2 components with identical names "X" and different classes' in stderr def test_owned_serializer_naming_override_with_ref_name_collision(warnings): class XSerializer(serializers.Serializer): x = serializers.UUIDField() class YSerializer(serializers.Serializer): x = serializers.IntegerField() class Meta: ref_name = 'X' # already used above class XAPIView(APIView): @extend_schema(request=XSerializer, responses=YSerializer) def post(self, request): pass # pragma: no cover generate_schema('x', view=XAPIView) def test_no_queryset_warn(capsys): class X1Serializer(serializers.Serializer): uuid = serializers.UUIDField() class X1Viewset(viewsets.ReadOnlyModelViewSet): serializer_class = X1Serializer generate_schema('x1', X1Viewset) stderr = capsys.readouterr().err assert ( 'could not derive type of path parameter "id" because it ' 'is untyped and obtaining queryset from the viewset failed.' ) in stderr def test_path_param_not_in_model(capsys): class XViewset(viewsets.ReadOnlyModelViewSet): serializer_class = SimpleSerializer queryset = SimpleModel.objects.none() @action(detail=True, url_path='meta/(?P[^/.]+)', methods=['POST']) def meta_param(self, request, ephemeral, pk): pass # pragma: no cover generate_schema('x1', XViewset) stderr = capsys.readouterr().err assert 'no such field' in stderr assert 'XViewset' in stderr def test_no_authentication_scheme_registered(capsys): class XAuth(BaseAuthentication): pass # pragma: no cover class XSerializer(serializers.Serializer): uuid = serializers.UUIDField() class XViewset(mixins.ListModelMixin, viewsets.GenericViewSet): serializer_class = XSerializer authentication_classes = [XAuth] generate_schema('x', XViewset) stderr = capsys.readouterr().err assert 'no OpenApiAuthenticationExtension registered' in stderr assert 'XViewset' in stderr def test_serializer_not_found(capsys): class XViewset(mixins.ListModelMixin, viewsets.GenericViewSet): pass # pragma: no cover generate_schema('x', XViewset) assert ( 'Error [XViewset]: exception raised while getting serializer.' ) in capsys.readouterr().err def test_extend_schema_unknown_class(capsys): class DoesNotCompute: pass # pragma: no cover class X1Viewset(viewsets.GenericViewSet): @extend_schema(responses={200: DoesNotCompute}) def list(self, request): pass # pragma: no cover generate_schema('x1', X1Viewset) assert 'Expected either a serializer' in capsys.readouterr().err def test_extend_schema_unknown_class2(capsys): class DoesNotCompute: pass # pragma: no cover class X1Viewset(viewsets.GenericViewSet): @extend_schema(responses=DoesNotCompute) def list(self, request): pass # pragma: no cover generate_schema('x1', X1Viewset) assert 'Expected either a serializer' in capsys.readouterr().err def test_no_serializer_class_on_apiview(capsys): class XView(views.APIView): def get(self, request): pass # pragma: no cover generate_schema('x', view=XView) assert 'Error [XView]: unable to guess serializer.' in capsys.readouterr().err def test_unable_to_follow_field_source_through_intermediate_property_warning(capsys): class FailingFieldSourceTraversalModel1(models.Model): @property def x(self): # missing type hint emits warning return # pragma: no cover class XSerializer(serializers.ModelSerializer): x = serializers.ReadOnlyField(source='x.y') class Meta: model = FailingFieldSourceTraversalModel1 fields = '__all__' class XAPIView(APIView): @extend_schema(responses=XSerializer) def get(self, request): pass # pragma: no cover generate_schema('x', view=XAPIView) assert ( '[XAPIView > XSerializer]: could not follow field source through intermediate property' ) in capsys.readouterr().err def test_unable_to_derive_function_type_warning(capsys): class FailingFieldSourceTraversalModel2(models.Model): @property def x(self): # missing type hint emits warning return # pragma: no cover class XSerializer(serializers.ModelSerializer): x = serializers.ReadOnlyField() y = serializers.SerializerMethodField() def get_y(self): return # pragma: no cover class Meta: model = FailingFieldSourceTraversalModel2 fields = '__all__' class XAPIView(APIView): @extend_schema(responses=XSerializer) def get(self, request): pass # pragma: no cover generate_schema('x', view=XAPIView) stderr = capsys.readouterr().err assert '[XAPIView > XSerializer]: unable to resolve type hint for function "x"' in stderr assert '[XAPIView > XSerializer]: unable to resolve type hint for function "get_y"' in stderr def test_unable_to_traverse_union_type_hint(capsys): class Foo: foo_value: int = 1 class Bar: pass class FailingFieldSourceTraversalModel3(models.Model): @property def foo_or_bar(self) -> Union[Foo, Bar]: # type: ignore pass # pragma: no cover class XSerializer(serializers.ModelSerializer): foo_value = serializers.ReadOnlyField(source='foo_or_bar.foo_value') class Meta: model = FailingFieldSourceTraversalModel3 fields = '__all__' class XAPIView(APIView): @extend_schema(responses=XSerializer) def get(self, request): pass # pragma: no cover generate_schema('foo_value', view=XAPIView) stderr = capsys.readouterr().err assert 'could not traverse Union type' in stderr assert 'Foo' in stderr assert 'Bar' in stderr def test_operation_id_collision_resolution(capsys): @extend_schema(responses=OpenApiTypes.FLOAT) @api_view(['GET']) def view_func(request, format=None): pass # pragma: no cover urlpatterns = [ path('pi/', view_func), path('pi/', view_func), ] schema = generate_schema(None, patterns=urlpatterns) assert schema['paths']['/pi/']['get']['operationId'] == 'pi_retrieve' assert schema['paths']['/pi/{foo}']['get']['operationId'] == 'pi_retrieve_2' assert 'operationId "pi_retrieve" has collisions' in capsys.readouterr().err @mock.patch('rest_framework.settings.api_settings.DEFAULT_SCHEMA_CLASS', DRFAutoSchema) def test_compatible_auto_schema_class_on_view(no_warnings): @extend_schema(responses=OpenApiTypes.FLOAT) @api_view(['GET']) def view_func(request): pass # pragma: no cover with pytest.raises(AssertionError) as excinfo: generate_schema('/x/', view_function=view_func) assert "Incompatible AutoSchema" in str(excinfo.value) def test_extend_schema_view_on_missing_view_method(capsys): @extend_schema_view( post=extend_schema(tags=['tag']) ) class XAPIView(APIView): def get(self, request): pass # pragma: no cover generate_schema('x', view=XAPIView) stderr = capsys.readouterr().err assert '@extend_schema_view argument "post" was not found on view' in stderr def test_polymorphic_proxy_subserializer_missing_type_field(capsys): class IncompleteSerializer(serializers.Serializer): field = serializers.IntegerField() # missing field named type class XAPIView(APIView): @extend_schema( responses=PolymorphicProxySerializer( component_name='Broken', serializers=[IncompleteSerializer], resource_type_field_name='type', ) ) def get(self, request): pass # pragma: no cover generate_schema('x', view=XAPIView) stderr = capsys.readouterr().err assert 'sub-serializer Incomplete of Broken must contain' in stderr @pytest.mark.parametrize('resource_type_field_name', ['field', None]) def test_polymorphic_proxy_serializer_misconfig(capsys, resource_type_field_name): class XSerializer(serializers.Serializer): field = serializers.IntegerField() # missing field named type class XAPIView(APIView): @extend_schema( responses=PolymorphicProxySerializer( component_name='Broken', serializers=[XSerializer(many=True)], resource_type_field_name=resource_type_field_name, ) ) def get(self, request): pass # pragma: no cover generate_schema('x', view=XAPIView) stderr = capsys.readouterr().err assert 'following usage pattern is necessary: PolymorphicProxySerializer' in stderr def test_warning_operation_id_on_extend_schema_view(capsys): from drf_spectacular.drainage import GENERATOR_STATS @extend_schema(operation_id='Invalid', responses=int) class XAPIView(APIView): def get(self, request): pass # pragma: no cover generate_schema('x', view=XAPIView) stderr = capsys.readouterr().err # check basic emittance of error message to stdout assert 'using @extend_schema on viewset class XAPIView with parameters' in stderr # check that delayed error message was persisted on view class assert getattr(XAPIView, '_spectacular_annotation', {}).get('errors') # check that msg survived pre-generation warning/error cache reset assert 'using @extend_schema on viewset' in list(GENERATOR_STATS._error_cache.keys())[0] def test_warning_request_body_not_resolvable(capsys): class Dummy: pass @extend_schema(request=Dummy) @api_view(['POST']) def view_func(request, format=None): pass # pragma: no cover generate_schema('x', view_function=view_func) stderr = capsys.readouterr().err assert 'could not resolve request body for POST /x' in stderr def test_response_header_warnings(capsys): @extend_schema( request=OpenApiTypes.ANY, responses=OpenApiTypes.ANY, parameters=[OpenApiParameter(name='test', response=True)] ) @api_view(['POST']) def view_func(request, format=None): pass # pragma: no cover generate_schema('x', view_function=view_func) stderr = capsys.readouterr().err assert 'incompatible location type ignored' in stderr def test_unknown_base_field_warning(capsys): class UnknownField(serializers.Field): pass class XSerializer(serializers.Serializer): field = UnknownField() @extend_schema(request=XSerializer, responses=XSerializer) @api_view(['POST']) def view_func(request, format=None): pass # pragma: no cover generate_schema('x', view_function=view_func) stderr = capsys.readouterr().err assert 'could not resolve serializer field' in stderr def test_warning_read_only_field_on_non_model_serializer(capsys): class XSerializer(serializers.Serializer): field = serializers.ReadOnlyField() class XViewSet(viewsets.ModelViewSet): serializer_class = XSerializer queryset = SimpleModel.objects.all() # test validity of serializer construction serializer = XSerializer(instance={'field': 1}) serializer.data generate_schema('x', XViewSet) stderr = capsys.readouterr().err assert 'Could not derive type for ReadOnlyField "field"' in stderr def test_warning_missing_lookup_field_on_model_serializer(capsys): class XViewSet(viewsets.ModelViewSet): serializer_class = SimpleSerializer queryset = SimpleModel.objects.all() lookup_field = 'non_existent_field' generate_schema('x', XViewSet) stderr = capsys.readouterr().err assert ( 'could not derive type of path parameter "non_existent_field" because model ' '"tests.models.SimpleModel" contained no such field.' ) in stderr @mock.patch( 'drf_spectacular.settings.spectacular_settings.PATH_CONVERTER_OVERRIDES', {'int': object} ) def test_invalid_path_converter_override(capsys): @extend_schema(responses=OpenApiTypes.FLOAT) @api_view(['GET']) def pi(request, foo): pass # pragma: no cover urlpatterns = [path('/a//', pi)] generate_schema(None, patterns=urlpatterns) stderr = capsys.readouterr().err assert 'Unable to use path converter override for "int".' in stderr def test_malformed_vendor_extensions(capsys): @extend_schema( responses=None, extensions={'foo': 'not-prefixed'} ) @api_view(['GET']) def view_func(request, format=None): pass # pragma: no cover generate_schema('x', view_function=view_func) stderr = capsys.readouterr().err assert 'invalid extension \'foo\'. vendor extensions must start with' in stderr def test_serializer_method_missing(capsys): class XSerializer(serializers.Serializer): some_field = serializers.SerializerMethodField() @extend_schema(responses=XSerializer) @api_view(['GET']) def view_func(request): pass # pragma: no cover generate_schema('x', view_function=view_func) stderr = capsys.readouterr().err assert 'SerializerMethodField "some_field" is missing required method "get_some_field"' in stderr def test_invalid_field_names(capsys): @extend_schema( responses=inline_serializer( 'Name with spaces', fields={'test': serializers.CharField()} ), ) @api_view(['GET']) def view_func(request, format=None): pass # pragma: no cover generate_schema('/x/', view_function=view_func) stderr = capsys.readouterr().err assert 'illegal characters' in stderr @pytest.mark.parametrize('type_arg,many', [ (SimpleSerializer, True), (SimpleSerializer(many=True), None), (serializers.ListSerializer(child=serializers.CharField()), None), (serializers.ListField(child=serializers.CharField()), None), ]) def test_invalid_parameter_types(capsys, type_arg, many): @extend_schema( request=OpenApiTypes.ANY, responses=OpenApiTypes.ANY, parameters=[OpenApiParameter(name='test', type=type_arg, many=many)] ) @api_view(['POST']) def view_func(request, format=None): pass # pragma: no cover generate_schema('/x/', view_function=view_func) stderr = capsys.readouterr().err assert 'parameter "test"' in stderr def test_primary_key_related_field_without_serializer_meta(capsys): class XSerializer(serializers.Serializer): field = serializers.PrimaryKeyRelatedField(read_only=True) @extend_schema(responses=XSerializer, request=XSerializer) @api_view(['GET']) def view_func(request, format=None): pass # pragma: no cover generate_schema('/x/', view_function=view_func) stderr = capsys.readouterr().err assert 'Could not derive type for under-specified PrimaryKeyRelatedField "field"' in stderr def test_request_encoding_on_invalid_content_type(capsys): class XSerializer(serializers.Serializer): field = serializers.MultipleChoiceField(choices=[1, 2, 3, 4]) @extend_schema( request={ 'application/msgpack': OpenApiRequest( request=XSerializer, encoding={"field": {"style": "form", "explode": True}}, ) }, responses=XSerializer ) @api_view(['POST']) def view_func(request, format=None): pass # pragma: no cover generate_schema('/x/', view_function=view_func) stderr = capsys.readouterr().err assert 'Encodings object on media types other than' in stderr drf-spectacular-0.27.0/tests/urls.py000066400000000000000000000000411453572150400173610ustar00rootroot00000000000000urlpatterns = [] # type: ignore drf-spectacular-0.27.0/tox.ini000066400000000000000000000105761453572150400162110ustar00rootroot00000000000000[tox] envlist = py311-lint,py311-docs, {py36,py37,py38}-django{2.2}-drf{3.10,3.11}, {py37,py38,py39}-django{3.2}-drf{3.11,3.12}, {py38,py39,py310}-django{4.0,4.1}-drf{3.13,3.14}, {py311}-django{4.1, 4.2, 5.0}-drf{3.14}, {py312}-django{4.2, 5.0}-drf{3.14}, py311-django5.0-drfmaster py311-djangomaster-drf3.14 py311-drfmaster-djangomaster py311-drfmaster-djangomaster-allowcontribfail skip_missing_interpreters = true [testenv] commands = python runtests.py {posargs:--fast --cov=drf_spectacular --cov=tests --cov-report=xml} setenv = PYTHONDONTWRITEBYTECODE=1 passenv = CI deps = django2.2: Django>=2.2,<3.0 django3.2: Django>=3.2,<4.0 django4.0: Django>=4.0,<4.1 django4.1: Django>=4.1,<4.2 django4.2: Django>=4.2,<4.3 django5.0: Django>=5.0,<5.1 drf3.10: djangorestframework>=3.10,<3.11 drf3.11: djangorestframework>=3.11,<3.12 drf3.12: djangorestframework>=3.12,<3.13 drf3.13: djangorestframework>=3.13,<3.14 drf3.14: djangorestframework>=3.14,<3.15 djangomaster: https://github.com/django/django/archive/main.tar.gz drfmaster: https://github.com/encode/django-rest-framework/archive/master.tar.gz -r requirements/testing.txt -r requirements/optionals.txt [testenv:py311-drfmaster-djangomaster-allowcontribfail] commands = python runtests.py {posargs:--fast --cov=drf_spectacular --cov=tests --cov-report=xml --allow-contrib-fail} [testenv:py311-lint] commands = python runtests.py --lintonly deps = -r requirements/base.txt -r requirements/linting.txt [testenv:py311-docs] commands = sphinx-build -WEa -b html -d {envtmpdir}/doctrees docs {envtmpdir}/html deps = -r requirements/docs.txt [coverage:report] precision = 2 exclude_lines = pragma: no cover except ImportError raise NotImplementedError if __name__ == .__main__.: if TYPE_CHECKING: [pytest] markers = contrib: marks upstream package dependency system_requirement_fulfilled: marks system library dependency. [flake8] ignore = # line break before binary operator W503, max-line-length = 120 [isort] skip = .eggs,.tox,docs,env skip_glob = venv* line_length = 100 known_first_party = drf_spectacular,tests known_third_party = django rest_framework uritemplate yaml jsonschema inflection allauth dj_rest_auth rest_framework_simplejwt rest_polymorphic rest_framework_jwt polymorphic oauth2_provider djstripe multi_line_output = 5 use_parentheses = true include_trailing_comma = true [mypy] python_version = 3.10 plugins = mypy_django_plugin.main,mypy_drf_plugin.main warn_unused_configs = True warn_redundant_casts = True warn_unused_ignores = True [mypy.plugins.django-stubs] django_settings_module = "tests.settings" [mypy-drf_spectacular.*] strict_equality = True no_implicit_optional = True disallow_untyped_decorators = True disallow_subclassing_any = True no_implicit_reexport = True ;check_untyped_defs = True ;warn_return_any = True ;disallow_incomplete_defs = True ;disallow_any_generics = True ;disallow_untyped_calls = True ;disallow_untyped_defs = True [mypy-rest_framework.compat.*] ignore_missing_imports = True [mypy-rest_framework.utils.mediatypes.*] ignore_missing_imports = True [mypy-allauth.*] ignore_missing_imports = True [mypy-dj_rest_auth.*] ignore_missing_imports = True [mypy-rest_framework_simplejwt.*] ignore_missing_imports = True [mypy-oauth2_provider.*] ignore_missing_imports = True [mypy-rest_framework_jwt.*] ignore_missing_imports = True [mypy-uritemplate.*] ignore_missing_imports = True [mypy-inflection.*] ignore_missing_imports = True [mypy-jsonschema.*] ignore_missing_imports = True [mypy-djangorestframework_camel_case.util.*] ignore_missing_imports = True [mypy-django_filters.*] ignore_missing_imports = True [mypy-polymorphic.*] ignore_missing_imports = True [mypy-rest_polymorphic.*] ignore_missing_imports = True [mypy-rest_framework_nested.*] ignore_missing_imports = True [mypy-rest_framework_recursive.*] ignore_missing_imports = True [mypy-rest_framework_dataclasses.*] ignore_missing_imports = True [mypy-rest_framework_gis.*] ignore_missing_imports = True [mypy-djangorestframework_camel_case.*] ignore_missing_imports = True [mypy-pydantic.*] ignore_missing_imports = True [mypy-exceptiongroup.*] ignore_missing_imports = True