pax_global_header00006660000000000000000000000064147371334660014530gustar00rootroot0000000000000052 comment=c96d56879cfc96f97c5c47cc9c3f15224d20d8be apispec-6.8.1/000077500000000000000000000000001473713346600131705ustar00rootroot00000000000000apispec-6.8.1/.github/000077500000000000000000000000001473713346600145305ustar00rootroot00000000000000apispec-6.8.1/.github/FUNDING.yml000066400000000000000000000000701473713346600163420ustar00rootroot00000000000000open_collective: "marshmallow" tidelift: "pypi/apispec" apispec-6.8.1/.github/dependabot.yml000066400000000000000000000003301473713346600173540ustar00rootroot00000000000000version: 2 updates: - package-ecosystem: pip directory: "/" schedule: interval: daily open-pull-requests-limit: 10 - package-ecosystem: "github-actions" directory: "/" schedule: interval: "monthly" apispec-6.8.1/.github/workflows/000077500000000000000000000000001473713346600165655ustar00rootroot00000000000000apispec-6.8.1/.github/workflows/build-release.yml000066400000000000000000000045441473713346600220340ustar00rootroot00000000000000name: build on: push: branches: ["dev", "*.x-line"] tags: ["*"] pull_request: # Run builds nightly to catch incompatibilities with new marshmallow releases schedule: - cron: "0 0 * * *" jobs: tests: name: ${{ matrix.name }} runs-on: ubuntu-latest strategy: fail-fast: false matrix: include: - { name: "3.9-ma3", python: "3.9", tox: py39-marshmallow3 } - { name: "3.13-ma3", python: "3.13", tox: py313-marshmallow3 } - { name: "3.13-madev", python: "3.13", tox: py313-marshmallowdev } steps: - uses: actions/checkout@v4.2.2 - uses: actions/setup-python@v5 with: python-version: ${{ matrix.python }} - run: pip install tox - run: tox -e${{ matrix.tox }} build: name: Build package runs-on: ubuntu-latest steps: - uses: actions/checkout@v4.2.2 - uses: actions/setup-python@v5 with: python-version: "3.13" - name: Install pypa/build run: python -m pip install build - name: Build a binary wheel and a source tarball run: python -m build - name: Install twine run: python -m pip install twine - name: Check build run: python -m twine check --strict dist/* - name: Store the distribution packages uses: actions/upload-artifact@v4 with: name: python-package-distributions path: dist/ # this duplicates pre-commit.ci, so only run it on tags # it guarantees that linting is passing prior to a release lint-pre-release: if: startsWith(github.ref, 'refs/tags') runs-on: ubuntu-latest steps: - uses: actions/checkout@v4.2.2 - uses: actions/setup-python@v5 with: python-version: "3.13" - run: python -m pip install tox - run: python -m tox -e lint publish-to-pypi: name: PyPI release if: startsWith(github.ref, 'refs/tags/') needs: [build, tests, lint-pre-release] runs-on: ubuntu-latest environment: name: pypi url: https://pypi.org/p/apispec permissions: id-token: write steps: - name: Download all the dists uses: actions/download-artifact@v4 with: name: python-package-distributions path: dist/ - name: Publish distribution to PyPI uses: pypa/gh-action-pypi-publish@release/v1 apispec-6.8.1/.gitignore000066400000000000000000000023241473713346600151610ustar00rootroot00000000000000# Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ *.egg-info/ .installed.cfg *.egg MANIFEST # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover .hypothesis/ .pytest_cache/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py db.sqlite3 # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ README.html # PyBuilder target/ # Jupyter Notebook .ipynb_checkpoints # pyenv .python-version # celery beat schedule file celerybeat-schedule # SageMath parsed files *.sage.py # Environments .env .venv env/ venv/ ENV/ env.bak/ venv.bak/ # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site # mypy .mypy_cache/ # ruff .ruff_cache/ apispec-6.8.1/.pre-commit-config.yaml000066400000000000000000000011301473713346600174440ustar00rootroot00000000000000ci: autoupdate_schedule: monthly repos: - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.8.6 hooks: - id: ruff - id: ruff-format - repo: https://github.com/python-jsonschema/check-jsonschema rev: 0.30.0 hooks: - id: check-github-workflows - id: check-readthedocs - repo: https://github.com/asottile/blacken-docs rev: 1.19.1 hooks: - id: blacken-docs additional_dependencies: [black==24.10.0] - repo: https://github.com/pre-commit/mirrors-mypy rev: v1.14.1 hooks: - id: mypy additional_dependencies: ["marshmallow>=3.24.1,<5", "types-PyYAML"] apispec-6.8.1/AUTHORS.rst000066400000000000000000000107551473713346600150570ustar00rootroot00000000000000******* Authors ******* Leads ===== - Steven Loria `@sloria `_ - Jérôme Lafréchoux `@lafrech `_ Contributors (chronological) ============================ - Josh Johnston `@Trii `_ - Vlad Frolov `@frol `_ - Josh Carp `@jmcarp `_ - Andrew Pashkin `@AndrewPashkin `_ - João Taveira Araújo `@jta `_ - Giacomo Tagliabue `@itajaja `_ - Ben Beadle `@benbeadle `_ - Martin Latrille `@martinlatrille `_ - Lucas Costa `@lucascosta `_ - Jared Deckard `@deckar01 `_ - Eric Bobbitt `@ericb `_ - Nick Phillips `@incognick `_ - Ashish Ranjan `@ranjanashish `_ - Jérôme Lafréchoux `@lafrech `_ - Anders Steinlein `@asteinlein `_ - Yuri Heupa `@YuriHeupa `_ - Matija Besednik `@matijabesednik `_ - Boris Serebrov `@serebrov `_ - Daniel Radetsky `@dradetsky `_ - Lucas Coutinho `@lucasrc `_ - `@lamiskin `_ - Florian Scheffler `@nebularazer `_ - Yoichi NAKAYAMA `@yoichi `_ - Vadim Radovel `@NightBlues `_ - Douglas Anderson `@djanderson `_ - Marat Sharafutdinov `@decaz `_ - Daniel Radetsky `@dradetsky `_ - Evgeny Seliverstov `@theirix `_ - Michael Bangert `@Bangertm `_ - Bastien Sevajol `@buxx `_ - Durmus Karatay `@ukaratay `_ - Julien Danjou `@jd `_ - Daisuke Taniwaki `@dtaniwaki `_ - `@mathewmarcus `_ - Louis-Philippe Huberdeau `@lphuberdeau `_ - Urban `@UrKr `_ - Christina Long `@cvlong `_ - Felix Yan `@felixonmars `_ - Guoli Lyu `@Guoli-Lyu `_ - Laura Beaufort `@lbeaufort `_ - Marcin Lulek `@ergo `_ - Jonathan Beezley `@jbeezley `_ - David Stapleton `@dstape `_ - Szabolcs Blága `@blagasz `_ - Andrew Johnson `@andrjohn `_ - Dave `@zedrdave `_ - Emmanuel Valette `@karec `_ - Hugo van Kemenade `@hugovk `_ - Bastien Gerard `@bagerard `_ - Ashutosh Chaudhary `@codeasashu `_ - Fedor Fominykh `@fedorfo `_ - Colin Bounouar `@Colin-b `_ - Mikko Kortelainen `@kortsi `_ - David Bishop `@teancom `_ - Andrea Ghensi `@sanzoghenzo `_ - `@timsilvers `_ - Kangwook Lee `@pbzweihander `_ - Martijn Pieters `@mjpieters `_ - Duncan Booth `@kupuguy `_ - Luke Whitehorn ``_ - François Magimel ``_ - Stefan van der Walt ``_ - ``_ - Edwin Erdmanis `@vorticity `_ - Mounier Florian `@paradoxxxzero `_ - Renato Damas `@codectl `_ - Tayler Sokalski `@tsokalski `_ - Sebastien Lovergne `@TheBigRoomXXL `_ - Luna Lovegood `@duchuyvp `_ - Tobias Kolditz `@kolditz-senec `_ - Christian Proud `@cjproud `_ - ``_ - Theron Luhn `@luhn `_ - Robert Shepley `@ShepleySound `_ apispec-6.8.1/CHANGELOG.rst000066400000000000000000001205051473713346600152140ustar00rootroot00000000000000Changelog --------- 6.8.1 (2025-01-07) ****************** Bug fixes: - Fix handling of nullable Raw fields for OAS 3.1.0 (:issue:`960`). Thanks :user:`tsokalski` for reporting and fixing. Support: - Support marshmallow 4 (:pr:`963`). 6.8.0 (2024-12-02) ****************** Features: - Allow properties on $ref objects for OpenAPI 3.1 (:pr:`958`). Thanks :user:`luhn` for the PR. Bug fixes: - Fix nullable nested schemas with metadata in OpenAPI 3.0 (:issue:`955`). Thanks :user:`luhn` for the catch and patch. 6.7.1 (2024-11-04) ****************** Bug fixes: - Fix rendering of nullable nested fields in 3.0 spec (:issue:`952`). Thanks :user:`ShepleySound` for the catch and patch. 6.7.0 (2024-10-20) ****************** Bug fixes: - Fix handling of ``fields.Dict()`` with ``values`` unset (:issue:`949`). Thanks :user:`luhn` for the catch and patch. Other changes: - Officially support Python 3.13 (:pr:`948`). - Drop support for Python 3.8 (:pr:`947`). 6.6.1 (2024-04-22) ****************** Bug fixes: - ``MarshmallowPlugin``: Fix handling of ``Nested`` fields with ``allow_none=True`` (:issue:`833`). Thanks :user:`jc-harrison` for reporting and :user:`kolditz-senec` for the PR. 6.6.0 (2024-03-15) ****************** Features: - Add IP fields to `DEFAULT_FIELD_MAPPING (:pr:`892`) to document format. Thanks :user:`cjproud` for the PR. 6.5.0 (2024-02-26) ****************** Bug fixes: - Include ``null`` as a value when using ``validate.OneOf`` or ``fields.Enum`` when ``allow_none`` is ``True`` for a field (:issue:`812`). Thanks :user:`pmdarrow` for reporting and :user:`kolditz-senec` for the PR. Other changes: - Deprecate the ``__version__`` attribute. Use feature detection, or ``importlib.metadata.version("apispec")``, instead (:issue:`878`). 6.4.0 (2024-01-09) ****************** Features: - ``MarshmallowPlugin``: Support different datetime formats for ``marshmallow.fields.DateTime`` fields (:issue:`814`). Thanks :user:`TheBigRoomXXL` for the suggestion and PR. - ``MarshmallowPlugin``: Handle resolving names of schemas with spaces in the name (:pr:`856`). Thanks :user:`duchuyvp` for the PR. - Various typing improvements (:pr:`873`). Other changes: - Support Python 3.12. - Drop support for Python 3.7, which is EOL. - Remove `[validation]` from extras, as it is no longer used. 6.3.1 (2023-12-21) ****************** Bug fixes: - Fix conversion of deprecated flag on parameters (:issue:`850`). Thanks :user:`tsokalski` for the PR. 6.3.0 (2023-03-10) ****************** Features: - Resolve schema references in parameters content (:issue:`830`). Thanks :user:`codectl` for the PR. 6.2.0 (2023-03-06) ****************** Features: - Resolve references in callbacks (:issue:`827`). Thanks :user:`codectl` for the PR. 6.1.0 (2023-03-03) ****************** Bug fixes: - Serialize min/max values in ``field2range`` (:pr:`825`). Other changes: - Test against Python 3.11 (:pr:`809`). 6.0.2 (2022-11-10) ****************** Bug fixes: - Allow passing ``openapi_version`` as string in ``marshmallow OpenAPIConverter`` (:issue:`810`). Thanks :user:`paradoxxxzero` for the PR. 6.0.1 (2022-11-05) ****************** Bug fixes: - Document ``fields.Enum`` as list of values, not string (:issue:`806`). Thanks :user:`tadams42` for reporting. 6.0.0 (2022-10-15) ****************** Features: - Support ``fields.Enum`` (:pr:`802`). - *Backwards-incompatible*: Change ``MarshmallowPlugin.map_to_openapi_type`` from a decorator to a classic function, taking a field as first argument (:pr:`804`). - *Backwards-incompatible*: Remove ``validate_spec`` from public API. Users may call their validator of choice directly (:pr:`803`). Other changes: - Drop support for marshmallow < 3.18.0 (:pr:`802`). 6.0.0b1 (2022-10-04) ******************** Features: - Add ``OpenAPIConverter.add_parameter_attribute_function`` to allow documentation of custom list fields such as webargs ``DelimitedList`` (:pr:`778`). - *Backwards-incompatible*: Remove ``OpenAPIVersion`` and use ``packaging.Version`` instead (:pr:`801`). 5.2.2 (2022-05-13) ****************** Bug fixes: - Fix schema property ordering regression in ``ApiSpec.to_yaml()`` (:issue:`768`). Thanks :user:`vorticity` for the PR. 5.2.1 (2022-05-01) ****************** Bug fixes: - Fix type hints for ``APISpec.path`` and ``BasePlugin`` methods (:pr:`765`). 5.2.0 (2022-04-29) ****************** Features: - Use ``raise from`` whenever possible (:pr:`763`). Refactoring: - Use a ``tuple`` rather than a ``namedtuple`` for "schema key" (:pr:`725`). Other changes: - Add type hints (:pr:`747`). Thanks :user:`kasium` for the PR. - Test against Python 3.10 (:pr:`724`). - Drop support for Python 3.6 (:pr:`727`). - Switch to Github Actions for CI (:pr:`751`). 5.1.1 (2021-09-27) ****************** Bug fixes: - Fix field ordering in "ordered" schema classes documentation (:issue:`714`). Other changes: - Don't build universal wheels. We don't support Python 2 anymore. (:pr:`705`) - Make the build reproducible (:pr:`669`). 5.1.0 (2021-08-10) ****************** Features: - Add ``lazy`` option to component registration methods. This allows to add components to the spec only if they are actually referenced. (:pr:`702`) - Add ``BasePlugin.header_helper`` and ``MarshmallowPlugin.header_helper`` (:pr:`703`). Bug fixes: - Ensure plugin helpers get component copies. Avoids issues if a plugin helper mutates its inputs. (:pr:`704`) 5.0.0 (2021-07-29) ****************** Features: - Rename ``doc_default`` to ``default``. Since schema metadata is namespaced in a single ``metadata`` parameter, there is no name collision with ``default`` parameter anymore (:issue:`687`). - Don't build schema component reference in ``OpenAPIConverter.resolve_nested_schema``. This is done later in ``Components`` (:pr:`700`). - ``MarshmallowPlugin``: resolve schemas in ``allOf``, ``oneOf``, ``anyOf`` and ``not`` (:pr:`701`). Thanks :user:`stefanv` for the initial work on this. Other changes: - Refactor ``Components`` methods to make them consistent. Use ``component_id`` rather than ``name``, remove ``**kwargs`` when unused. (:pr:`696`) 5.0.0b1 (2021-07-22) ******************** Features: - Resolve all component references in paths and components. All references must be passed as strings, not as a ``{$ref: '...'}}`` dict (:pr:`671`). Other changes: - Don't use deprecated ``missing`` marshmallow field attribute but use ``load_default`` instead (:pr:`692`). - Refactor references resolution. ``get_ref`` method is moved from ``APISpec`` to ``Components`` (:pr:`655`). ``APISpec.clean_parameters`` and ``APISpec.clean_parameters`` are now private methods (:pr:`695`). - Drop support for marshmallow < 3.13.0 (:pr:`692`). 4.7.1 (2021-07-06) ****************** Bug fixes: - Correct spelling of ``'null'``: remove extra quotes (:issue:`689`). Thanks :user:`mjpieters` for the PR. 4.7.0 (2021-06-28) ****************** Features: - Document ``deprecated`` property from field metadata (:pr:`686`). Thanks :user:`greyli` for the PR. - Document ``writeOnly`` and ``nullable`` properties from field metadata (:pr:`684`). Thanks :user:`greyli` for the PR. 4.6.0 (2021-06-14) ****************** Features: - Support ``Pluck`` field (:pr:`677`). Thanks :user:`mjpieters` for the PR. - Support ``TimeDelta`` field (:pr:`678`). 4.5.0 (2021-06-04) ****************** Features: - Support OpenAPI 3.1.0 (:issue:`579`). Bug fixes: - Fix ``get_fields`` to avoid crashing when a field is named ``fields`` (:issue:`673`). Thanks :user:`Reskov` for reporting. Other changes: - Don't pass field metadata as keyword arguments in the tests. This is deprecated since marshmallow 3.10. apispec is still compatible with marshmallow >=3,<3.10 but tests now require marshmallow >=3.10. (:pr:`675`) 4.4.2 (2021-05-24) ****************** Bug fixes: - Respect ``partial`` marshmallow schema parameter: don't document the field as required. (:issue:`627`). Thanks :user:`Anti-Distinctlyminty` for the PR. 4.4.1 (2021-05-07) ****************** Bug fixes: - Don't set ``additionalProperties`` if ``Meta.unknown`` is ``EXCLUDE`` (:issue:`659`). Thanks :user:`kupuguy` for the PR. 4.4.0 (2021-03-31) ****************** Features: - Populate ``additionalProperties`` from ``Meta.unknown`` (:pr:`635`). Thanks :user:`timsilvers` for the PR. - Allow ``to_yaml`` to pass kwargs to ``yaml.dump`` (:pr:`648`). - Resolve header references in responses (:pr:`650`). - Resolve example references in parameters, request bodies and responses (:pr:`651`). 4.3.0 (2021-02-10) ****************** Features: - Add `apispec.core.Components.header` to register header components (:pr:`637`). 4.2.0 (2021-02-06) ****************** Features: - Make components public attributes of ``Components`` class (:pr:`634`). 4.1.0 (2021-01-26) ****************** Features: - Resolve schemas in callbacks (:pr:`544`). Thanks :user:`kortsi` for the PR. Bug fixes: - Fix docstrings documenting kwargs type as dict (:issue:`534`). - Use ``x-minimum`` and ``x-maximum`` extensions to document ranges that are not of number type (e.g. datetime) (:issue:`614`). Other changes: - Test against Python 3.9. 4.0.0 (2020-09-30) ****************** Features: - *Backwards-incompatible*: Automatically generate references for schemas passed as strings in responses and request bodies. When using ``MarshmallowPlugin``, if a schema is passed as string, the marshmallow registry is looked up for this schema name and if none is found, the name is assumed to be a reference to a manually created schema and a reference is generated. No exception is raised anymore if the schema name can't be found in the registry. (:pr:`554`) 4.0.0b1 (2020-09-06) ******************** Features: - *Backwards-incompatible*: Ignore ``location`` field metadata. This attribute was used in webargs but it has now been dropped. A ``Schema`` can now only have a single location. This simplifies the logic in ``OpenAPIConverter`` methods, where ``default_in`` argument now becomes ``location``. (:pr:`526`) - *Backwards-incompatible*: Don't document ``int`` format as ``"int32"`` and ``float`` format as ``"float"``, as those are platform-dependent (:pr:`595`). Refactoring: - ``OpenAPIConverter.field2parameters`` and ``OpenAPIConverter.property2parameter`` are removed. ``OpenAPIConverter.field2parameter`` becomes private. (:pr:`581`) Other changes: - Drop support for marshmallow 2. Marshmallow 3.x is required. (:pr:`583`) - Drop support for Python 3.5. Python 3.6+ is required. (:pr:`582`) 3.3.2 (2020-08-29) ****************** Bug fixes: - Fix crash when field metadata contains non-string keys (:pr:`596`). Thanks :user:`sanzoghenzo` for the fix. 3.3.1 (2020-06-06) ****************** Bug fixes: - Fix ``MarshmallowPlugin`` crash when ``resolve_schema_dict`` is passed a schema as string and ``schema_name_resolver`` returns ``None`` (:issue:`566`). Thanks :user:`black3r` for reporting and thanks :user:`Bangertm` for the PR. 3.3.0 (2020-02-14) ****************** Features: - Instantiate ``Components`` before calling plugins' ``init_spec`` (:pr:`539`). Thanks :user:`Colin-b` for the PR. 3.2.0 (2019-12-22) ****************** Features: - Add ``match_info`` to ``__location_map__`` (:pr:`517`). Thanks :user:`fedorfo` for the PR. 3.1.1 (2019-12-17) ****************** Bug fixes: - Don't emit a warning when passing "default" as response status code in OASv2 (:pr:`521`). 3.1.0 (2019-11-04) ****************** Features: - Add `apispec.core.Components.example` for adding Example Objects (:pr:`515`). Thanks :user:`codeasashu` for the PR. Support: - Test against Python 3.8 (:pr:`510`). 3.0.0 (2019-09-17) ++++++++++++++++++ Features: - Add support for generating user-defined OpenAPI properties for custom field classes via an ``add_attribute_function`` method (:pr:`478` and :pr:`498`). - [apispec.ext.marshmallow]: *Backwards-incompatible* ``fields.Raw`` and ``fields.Field`` are now represented by OpenAPI `Any Type `_ (:pr:`495`). - [apispec.ext.marshmallow]: *Backwards-incompatible*: The ``schema_name_resolver`` function now receives a ``Schema`` class, a ``Schema`` instance or a string that resolves to a ``Schema`` class. This allows a custom resolver to generate different names depending on schema modifiers used in a ``Schema`` instance (:pr:`476`). Bug fixes: - [apispec.ext.marshmallow]: With marshmallow 3, the default value of a field in the documentation is the serialized value of the ``missing`` attribute, not ``missing`` itself (:pr:`490`). Refactoring: - ``clean_parameters`` and ``clean_operations`` are now ``APISpec`` methods (:pr:`489`). - [apispec.ext.marshmallow]: ``Schema`` resolver methods are extracted from ``MarshmallowPlugin`` into a ``SchemaResolver`` class member (:pr:`496`). - [apispec.ext.marshmallow]: ``OpenAPIConverter`` is now a class member of ``MarshmallowPlugin`` (:pr:`493`). - [apispec.ext.marshmallow]: ``Field`` to properties conversion logic is extracted from ``OpenAPIConverter`` into ``FieldConverterMixin`` (:pr:`478`). Other changes: - Drop support for Python 2 (:issue:`491`). Thanks :user:`hugovk` for the PR. - Drop support for marshmallow pre-releases. Only stable 2.x and 3.x versions are supported (:issue:`485`). 2.0.2 (2019-07-04) ++++++++++++++++++ Bug fixes: - Fix compatibility with marshmallow 3.0.0rc8 (:pr:`469`). Other changes: - Switch to Azure Pipelines (:pr:`468`). 2.0.1 (2019-06-26) ++++++++++++++++++ Bug fixes: - Don't mutate ``operations`` and ``parameters`` in ``APISpec.path`` to avoid issues when calling it twice with the same ``operations`` or ``parameters`` (:pr:`464`). 2.0.0 (2019-06-18) ++++++++++++++++++ Features: - Add support for path level parameters (:issue:`453`). Thanks :user:`karec` for the PR. - *Backwards-incompatible*: A ``apispec.exceptions.DuplicateParameterError`` is raised when two parameters with same name and location are passed to a path or an operation (:pr:`455`). - *Backwards-incompatible*: A ``apispec.exceptions.InvalidParameterError`` is raised when a parameter is missing required ``name`` and ``in`` attributes after helpers have been executed (:pr:`455`). Other changes: - *Backwards-incompatible*: All plugin helpers must accept extra ``**kwargs`` (:issue:`453`). - *Backwards-incompatible*: Components must be referenced by ID, not full path (:issue:`463`). 1.3.3 (2019-05-05) ++++++++++++++++++ Bug fixes: - marshmallow 3.0.0rc6 compatibility (:pr:`445`). 1.3.2 (2019-05-02) ++++++++++++++++++ Bug fixes: - Fix handling of OpenAPI v3 components content without schema in ``MarshmallowPlugin`` (:pr:`443`). 1.3.1 (2019-04-29) ++++++++++++++++++ Bug fixes: - Fix handling of ``http.HTTPStatus`` objects (:issue:`426`). Thanks :user:`DStape`. - [apispec.ext.marshmallow]: Ensure make_schema_key returns a unique key on unhashable iterables (:pr:`416`, :pr:`439`). Thanks :user:`zedrdave`. 1.3.0 (2019-04-24) ++++++++++++++++++ Features: - [apispec.ext.marshmallow]: Use class hierarchy to infer ``type`` and ``format`` properties (:issue:`433`, :issue:`250`). Thanks :user:`andrjohn` for the PR. 1.2.1 (2019-04-18) ++++++++++++++++++ Bug fixes: - Fix error in ``MarshmallowPlugin`` when passing ``exclude`` and ``dump_only`` as ``class Meta`` attributes mixing ``list`` and ``tuple`` (:pr:`431`). Thanks :user:`blagasz` for the PR. 1.2.0 (2019-04-08) ++++++++++++++++++ Features: - Strip empty sections (components, tags) from generated documentation (:pr:`421` and :pr:`425`). 1.1.2 (2019-04-07) ++++++++++++++++++ Bug fixes: - Fix behavior when using "2xx", 3xx", etc. for response keys (:issue:`422`). Thanks :user:`zachmullen` for reporting. 1.1.1 (2019-04-02) ++++++++++++++++++ Bug fixes: - Fix passing references for parameters/responses when using ``MarshmallowPlugin`` (:pr:`414`). 1.1.0 (2019-03-17) ++++++++++++++++++ Features: - Resolve ``Schema`` classes in response headers (:pr:`409`). 1.0.0 (2019-02-08) ++++++++++++++++++ Features: - Expanded support for OpenAPI Specification version 3 (:issue:`165`). - Add ``summary`` and ``description`` parameters to ``APISpec.path`` (:issue:`227`). Thanks :user:`timakro` for the suggestion. - Add `apispec.core.Components.security_scheme` for adding Security Scheme Objects (:issue:`245`). - [apispec.ext.marshmallow]: Add support for outputting field patterns from ``Regexp`` validators (:pr:`364`). Thanks :user:`DStape` for the PR. Bug fixes: - [apispec.ext.marshmallow]: Fix automatic documentation of schemas when using ``Nested(MySchema, many==True)`` (:issue:`383`). Thanks :user:`whoiswes` for reporting. Other changes: - *Backwards-incompatible*: Components properties are now passed as dictionaries rather than keyword arguments (:pr:`381`). .. code-block:: python # <1.0.0 spec.components.schema("Pet", properties={"name": {"type": "string"}}) spec.components.parameter("PetId", "path", format="int64", type="integer") spec.components.response("NotFound", description="Pet not found") # >=1.0.0 spec.components.schema("Pet", {"properties": {"name": {"type": "string"}}}) spec.components.parameter("PetId", "path", {"format": "int64", "type": "integer"}) spec.components.response("NotFound", {"description": "Pet not found"}) Deprecations/Removals: - *Backwards-incompatible*: The ``ref`` argument passed to fields is no longer used (:issue:`354`). References for nested ``Schema`` are stored automatically. - *Backwards-incompatible*: The ``extra_fields`` argument of `apispec.core.Components.schema` is removed. All properties may be passed in the ``component`` argument. .. code-block:: python # <1.0.0 spec.definition("Pet", schema=PetSchema, extra_fields={"discriminator": "name"}) # >=1.0.0 spec.components.schema("Pet", schema=PetSchema, component={"discriminator": "name"}) 1.0.0rc1 (2018-01-29) +++++++++++++++++++++ Features: - Automatically generate references to nested schemas with a computed name, e.g. ``fields.Nested(PetSchema())`` -> ``#components/schemas/Pet``. - Automatically generate references for ``requestBody`` using the above mechanism. - Ability to opt out of the above behavior by passing a ``schema_name_resolver`` function that returns ``None`` to ``api.ext.MarshmallowPlugin``. - References now respect Schema modifiers, including ``exclude`` and ``partial``. - *Backwards-incompatible*: A `apispec.exceptions.DuplicateComponentNameError` is raised when registering two components with the same name (:issue:`340`). 1.0.0b6 (2018-12-16) ++++++++++++++++++++ Features: - *Backwards-incompatible*: `basePath` is not removed from paths anymore. Paths passed to ``APISpec.path`` should not contain the application base path (:pr:`345`). - Add ``apispec.ext.marshmallow.openapi.OpenAPIConverter.resolve_schema_class`` (:pr:`346`). Thanks :user:`buxx`. 1.0.0b5 (2018-11-06) ++++++++++++++++++++ Features: - ``apispec.core.Components`` is added. Each ``APISpec`` instance has a ``Components`` object used to define components such as schemas, parameters or responses. "Components" is the OpenAPI v3 terminology for those reusable top-level objects. - ``apispec.core.Components.parameter`` and ``apispec.core.Components.response`` are added. - *Backwards-incompatible*: ``apispec.APISpec.add_path`` and ``apispec.APISpec.add_tag`` are renamed to ``apispec.APISpec.path`` and ``apispec.APISpec.tag``. - *Backwards-incompatible*: ``apispec.APISpec.definition`` is moved to the ``Components`` class and renamed to ``apispec.core.Components.schema``. :: # apispec<1.0.0b5 spec.add_tag({'name': 'Pet', 'description': 'Operations on pets'}) spec.add_path('/pets/', operations=...) spec.definition('Pet', properties=...) # apispec>=1.0.0b5 spec.tag({'name': 'Pet', 'description': 'Operations on pets'}) spec.path('/pets/', operations=...) spec.components.schema('Pet', properties=...) - Plugins can define ``parameter_helper`` and ``response_helper`` to modify parameter and response components definitions. - ``MarshmallowPlugin`` resolves schemas in parameters and responses components. - Components helpers may return ``None`` as a no-op rather than an empty `dict` (:pr:`336`). Bug fixes: - ``MarshmallowPlugin.schema_helper`` does not crash when no schema is passed (:pr:`336`). Deprecations/Removals: - The legacy ``response_helper`` feature is removed. The same can be achieved from ``operation_helper``. 1.0.0b4 (2018-10-28) ++++++++++++++++++++ - *Backwards-incompatible*: ``apispec.ext.flask``, ``apispec.ext.bottle``, and ``apispec.ext.tornado`` are moved to a separate package, `apispec-webframeworks `_. (:issue:`302`). If you use these plugins, install ``apispec-webframeworks`` and update your imports like so: :: # apispec<1.0.0b4 from apispec.ext.flask import FlaskPlugin # apispec>=1.0.0b4 from apispec_webframeworks.flask import FlaskPlugin Thanks :user:`ergo` for the suggestion and the PR. 1.0.0b3 (2018-10-08) ++++++++++++++++++++ Features: - [apispec.core]: *Backwards-incompatible*: ``openapi_version`` parameter of ``APISpec`` class does not default to `'2.0'` anymore and ``info`` parameter is merged with ``**options`` kwargs. Bug fixes: - [apispec.ext.marshmallow]: Exclude ``load_only`` fields when documenting responses (:issue:`119`). Thanks :user:`luisincrespo` for reporting. - [apispec.ext.marshmallow]: Exclude ``dump_only`` fields when documenting request body parameter schema. 1.0.0b2 (2018-09-09) ++++++++++++++++++++ - Drop deprecated plugin interface. Only plugin classes are now supported. This includes the removal of ``APISpec``'s ``register_*_helper`` methods, as well as its ``schema_name_resolver`` parameter. Also drop deprecated ``apispec.utils.validate_swagger``. (:pr:`259`) - Use ``yaml.safe_load`` instead of ``yaml.load`` when reading docstrings (:issue:`278`). Thanks :user:`lbeaufort` for the suggestion and the PR. 1.0.0b1 (2018-07-29) ++++++++++++++++++++ Features: - [apispec.core]: *Backwards-incompatible*: Remove `Path` class. Plugins' `path_helper` methods should now return a path as a string and optionally mutate the `operations` dictionary (:pr:`238`). - [apispec.core]: *Backwards-incompatible*: YAML support is optional. To install with YAML support, use ``pip install 'apispec[yaml]'``. You will need to do this if you use ``FlaskPlugin``, ``BottlePlugin``, or ``TornadoPlugin`` (:pr:`251`). - [apispec.ext.marshmallow]: Allow overriding the documentation for a field's default. This is especially useful for documenting callable defaults (:issue:`196`). 0.39.0 (2018-06-28) +++++++++++++++++++ Features: - [apispec.core]: *Backwards-incompatible*: Change plugin interface. Plugins are now child classes of ``apispec.BasePlugin``. Built-in plugins are still usable with the deprecated legacy interface. However, the new class interface is mandatory to pass parameters to plugins or to access specific methods that used to be accessed as module level functions (typically in ``apispec.ext.marshmallow.swagger``). Also, ``schema_name_resolver`` is now a parameter of ``apispec.ext.marshmallow.MarshmallowPlugin``. It can still be passed to ``APISpec`` while using the legacy interface. (:issue:`207`) - [apispec.core]: *Backwards-incompatible*: ``APISpec.openapi_version`` is now an ``apispec.utils.OpenAPIVersion`` instance. 0.38.0 (2018-06-10) +++++++++++++++++++ Features: - [apispec.core]: *Backwards-incompatible*: Rename ``apispec.utils.validate_swagger`` to ``apispec.utils.validate_spec`` and ``apispec.exceptions.SwaggerError`` to ``apispec.exceptions.OpenAPIError``. Using ``validate_swagger`` will raise a ``DeprecationWarning`` (:pr:`224`). - [apispec.core]: ``apispec.utils.validate_spec`` no longer relies on the ``check_api`` NPM module. ``prance`` and ``openapi-spec-validator`` are required for validation, and can be installed using ``pip install 'apispec[validation]'`` (:pr:`224`). - [apispec.core]: Deep update components instead of overwriting components for OpenAPI 3 (:pr:`222`). Thanks :user:`Guoli-Lyu`. Bug fixes: - [apispec.ext.marshmallow]: Fix description for parameters in OpenAPI 3 (:pr:`223`). Thanks again :user:`Guoli-Lyu`. Other changes: - Drop official support for Python 3.4. Only Python 2.7 and >=3.5 are supported. 0.37.1 (2018-05-28) +++++++++++++++++++ Features: - [apispec.ext.marshmallow]: Fix OpenAPI 3 conversion of schemas in parameters (:issue:`217`). Thanks :user:`Guoli-Lyu` for the PR. 0.37.0 (2018-05-14) +++++++++++++++++++ Features: - [apispec.ext.marshmallow]: Resolve an array of schema objects in parameters (:issue:`209`). Thanks :user:`cvlong` for reporting and implementing this. 0.36.0 (2018-05-07) +++++++++++++++++++ Features: - [apispec.ext.marshmallow]: Document ``values`` parameter of ``Dict`` field as ``additionalProperties`` (:issue:`201`). Thanks :user:`UrKr`. 0.35.0 (2018-04-10) +++++++++++++++++++ Features: - [apispec.ext.marshmallow]: Recurse over properties when resolving schemas (:issue:`186`). Thanks :user:`lphuberdeau`. - [apispec.ext.marshmallow]: Support ``writeOnly`` and ``nullable`` in OpenAPI 3 (fall back to ``x-nullable`` for OpenAPI 2) (:issue:`165`). Thanks :user:`lafrech`. Bug fixes: - [apispec.ext.marshmallow]: Always use `field.missing` instead of `field.default` when introspecting fields (:issue:`32`). Thanks :user:`lafrech`. Other changes: - [apispec.ext.marshmallow]: Refactor some of the internal functions in `apispec.ext.marshmallow.swagger` for consistent API (:issue:`199`). Thanks :user:`lafrech`. 0.34.0 (2018-04-04) +++++++++++++++++++ Features: - [apispec.core]: Maintain order in which methods are added to an endpoint (:issue:`189`). Thanks :user:`lafrech`. Other changes: - [apispec.core]: `Path` no longer inherits from `dict` (:issue:`190`). Thanks :user:`lafrech`. 0.33.0 (2018-04-01) +++++++++++++++++++ Features: - [apispec.ext.marshmallow]: Respect ``data_key`` argument on fields (in marshmallow 3). Thanks :user:`lafrech`. 0.32.0 (2018-03-24) +++++++++++++++++++ Features: - [apispec.ext.bottle]: Allow `app` to be passed to `spec.add_path` (:issue:`188`). Thanks :user:`dtaniwaki` for the PR. Bug fixes: - [apispec.ext.marshmallow]: Fix issue where "body" and "required" were getting overwritten when passing a ``Schema`` to a parameter (:issue:`168`, :issue:`184`). Thanks :user:`dlopuch` and :user:`mathewmarcus` for reporting and thanks :user:`mathewmarcus` for the PR. 0.31.0 (2018-01-30) +++++++++++++++++++ - [apispec.ext.marshmallow]: Use ``dump_to`` for name even if ``load_from`` does not match it (:issue:`178`). Thanks :user:`LeonAgmonNacht` for reporting and thanks :user:`lafrech` for the fix. 0.30.0 (2018-01-12) +++++++++++++++++++ Features: - [apispec.core]: Add ``Spec.to_yaml`` method for serializing to YAML (:issue:`161`). Thanks :user:`jd`. 0.29.0 (2018-01-04) +++++++++++++++++++ Features: - [apispec.core and apispec.ext.marshmallow]: Add limited support for OpenAPI v3. Pass `openapi_version='3.0.0'` to `Spec` to use it (:issue:`165`). Thanks :user:`Bangertm`. 0.28.0 (2017-12-09) +++++++++++++++++++ Features: - [apispec.core and apispec.ext.marshmallow]: Add `schema_name_resolver` param to `APISpec` for resolving ref names for marshmallow Schemas. This is useful when a self-referencing schema is nested within another schema (:issue:`167`). Thanks :user:`buxx` for the PR. 0.27.1 (2017-12-06) +++++++++++++++++++ Bug fixes: - [apispec.ext.flask]: Don't document view methods that aren't included in ``app.add_url_rule(..., methods=[...]))`` (:issue:`173`). Thanks :user:`ukaratay`. 0.27.0 (2017-10-30) +++++++++++++++++++ Features: - [apispec.core]: Add ``register_operation_helper``. Bug fixes: - Order of plugins does not matter (:issue:`136`). Thanks :user:`yoichi` for these changes. 0.26.0 (2017-10-23) +++++++++++++++++++ Features: - [apispec.ext.marshmallow]: Generate "enum" property with single entry when the ``validate.Equal`` validator is used (:issue:`155`). Thanks :user:`Bangertm` for the suggestion and PR. Bug fixes: - Allow OPTIONS to be documented (:issue:`162`). Thanks :user:`buxx` for the PR. - Fix regression from 0.25.3 that caused a ``KeyError`` (:issue:`163`). Thanks :user:`yoichi`. 0.25.4 (2017-10-09) +++++++++++++++++++ Bug fixes: - [apispec.ext.marshmallow]: Fix swagger location mapping for ``default_in`` param in fields2parameters (:issue:`156`). Thanks :user:`decaz`. 0.25.3 (2017-09-27) +++++++++++++++++++ Bug fixes: - [apispec.ext.marshmallow]: Correctly handle multiple fields with ``location=json`` (:issue:`75`). Thanks :user:`shaicantor` for reporting and thanks :user:`yoichi` for the patch. 0.25.2 (2017-09-05) +++++++++++++++++++ Bug fixes: - [apispec.ext.marshmallow]: Avoid AttributeError when passing non-dict items to path objects (:issue:`151`). Thanks :user:`yoichi`. 0.25.1 (2017-08-23) +++++++++++++++++++ Bug fixes: - [apispec.ext.marshmallow]: Fix ``use_instances`` when ``many=True`` is set (:issue:`148`). Thanks :user:`theirix`. 0.25.0 (2017-08-15) +++++++++++++++++++ Features: - [apispec.ext.marshmallow]: Add ``use_instances`` parameter to ``fields2paramters`` (:issue:`144`). Thanks :user:`theirix`. Other changes: - Don't swallow ``YAMLError`` when YAML parsing fails (:issue:`135`). Thanks :user:`djanderson` for the suggestion and the PR. 0.24.0 (2017-08-15) +++++++++++++++++++ Features: - [apispec.ext.marshmallow]: Add ``swagger.map_to_swagger_field`` decorator to support custom field classes (:issue:`120`). Thanks :user:`frol` for the suggestion and thanks :user:`dradetsky` for the PR. 0.23.1 (2017-08-08) +++++++++++++++++++ Bug fixes: - [apispec.ext.marshmallow]: Fix swagger location mapping for ``default_in`` param in `property2parameter` (:issue:`142`). Thanks :user:`decaz`. 0.23.0 (2017-08-03) +++++++++++++++++++ - Pass `operations` constructed by plugins to downstream marshmallow plugin (:issue:`138`). Thanks :user:`yoichi`. - [apispec.ext.marshmallow] Generate parameter specification from marshmallow Schemas (:issue:`127`). Thanks :user:`ewalker11` for the suggestion thanks :user:`yoichi` for the PR. - [apispec.ext.flask] Add support for Flask MethodViews (:issue:`85`, :issue:`125`). Thanks :user:`lafrech` and :user:`boosh` for the suggestion. Thanks :user:`djanderson` and :user:`yoichi` for the PRs. 0.22.3 (2017-07-16) +++++++++++++++++++ - Release wheel distribution. 0.22.2 (2017-07-12) +++++++++++++++++++ Bug fixes: - [apispec.ext.marshmallow]: Properly handle callable ``default`` values in output spec (:issue:`131`). Thanks :user:`NightBlues`. 0.22.1 (2017-06-25) +++++++++++++++++++ Bug fixes: - [apispec.ext.marshmallow]: Include ``default`` in output spec when ``False`` is the default for a ``Boolean`` field (:issue:`130`). Thanks :user:`nebularazer`. 0.22.0 (2017-05-30) +++++++++++++++++++ Features: - [apispec.ext.bottle] Added bottle plugin (:issue:`128`). Thanks :user:`lucasrc`. 0.21.0 (2017-04-21) +++++++++++++++++++ Features: - [apispec.ext.marshmallow] Sort list of required field names in generated spec (:issue:`124`). Thanks :user:`dradetsky`. 0.20.1 (2017-04-18) +++++++++++++++++++ Bug fixes: - [apispec.ext.tornado]: Fix compatibility with Tornado>=4.5. - [apispec.ext.tornado]: Fix adding paths for handlers with coroutine methods in Python 2 (:issue:`99`). 0.20.0 (2017-03-19) +++++++++++++++++++ Features: - [apispec.core]: Definition helper functions receive the ``definition`` keyword argument, which is the current state of the definition (:issue:`122`). Thanks :user:`martinlatrille` for the PR. Other changes: - [apispec.ext.marshmallow] *Backwards-incompatible*: Remove ``dump`` parameter from ``schema2parameters``, ``fields2parameters``, and ``field2parameter`` (:issue:`114`). Thanks :user:`lafrech` and :user:`frol` for the feedback and :user:`lafrech` for the PR. 0.19.0 (2017-03-05) +++++++++++++++++++ Features: - [apispec.core]: Add ``extra_fields`` parameter to `APISpec.definition` (:issue:`110`). Thanks :user:`lafrech` for the PR. - [apispec.ext.marshmallow]: Preserve the order of ``choices`` (:issue:`113`). Thanks :user:`frol` for the PR. Bug fixes: - [apispec.ext.marshmallow]: 'discriminator' is no longer valid as field metadata. It should be defined by passing ``extra_fields={'discriminator': '...'}`` to `APISpec.definition`. Thanks for reporting, :user:`lafrech`. - [apispec.ext.marshmallow]: Allow additional properties when translating ``Nested`` fields using ``allOf`` (:issue:`108`). Thanks :user:`lafrech` for the suggestion and the PR. - [apispec.ext.marshmallow]: Respect ``dump_only`` and ``load_only`` specified in ``class Meta`` (:issue:`84`). Thanks :user:`lafrech` for the fix. Other changes: - Drop support for Python 3.3. 0.18.0 (2017-02-19) +++++++++++++++++++ Features: - [apispec.ext.marshmallow]: Translate ``allow_none`` on ``Fields`` to ``x-nullable`` (:issue:`66`). Thanks :user:`lafrech`. 0.17.4 (2017-02-16) +++++++++++++++++++ Bug fixes: - [apispec.ext.marshmallow]: Fix corruption of ``Schema._declared_fields`` when serializing an APISpec (:issue:`107`). Thanks :user:`serebrov` for the catch and patch. 0.17.3 (2017-01-21) +++++++++++++++++++ Bug fixes: - [apispec.ext.marshmallow]: Fix behavior when passing `Schema` instances to `APISpec.definition`. The `Schema's` class will correctly be registered as a an available `ref` (:issue:`84`). Thanks :user:`lafrech` for reporting and for the PR. 0.17.2 (2017-01-03) +++++++++++++++++++ Bug fixes: - [apispec.ext.tornado]: Remove usage of ``inspect.getargspec`` for Python >= 3.3 (:issue:`102`). Thanks :user:`matijabesednik`. 0.17.1 (2016-11-19) +++++++++++++++++++ Bug fixes: - [apispec.ext.marshmallow]: Prevent unnecessary warning when generating specs for marshmallow Schema's with autogenerated fields (:issue:`95`). Thanks :user:`khorolets` reporting and for the PR. - [apispec.ext.marshmallow]: Correctly translate ``Length`` validator to `minItems` and `maxItems` for array-type fields (``Nested`` and ``List``) (:issue:`97`). Thanks :user:`YuriHeupa` for reporting and for the PR. 0.17.0 (2016-10-30) +++++++++++++++++++ Features: - [apispec.ext.marshmallow]: Add support for properties that start with `x-`. Thanks :user:`martinlatrille` for the PR. 0.16.0 (2016-10-12) +++++++++++++++++++ Features: - [apispec.core]: Allow ``description`` to be passed to ``APISpec.definition`` (:issue:`93`). Thanks :user:`martinlatrille`. 0.15.0 (2016-10-02) +++++++++++++++++++ Features: - [apispec.ext.marshmallow]: Allow ``'query'`` to be passed as a field location (:issue:`89`). Thanks :user:`lafrech`. Bug fixes: - [apispec.ext.flask]: Properly strip off ``basePath`` when ``APPLICATION_ROOT`` is set on a Flask app's config (:issue:`78`). Thanks :user:`deckar01` for reporting and :user:`asteinlein` for the PR. 0.14.0 (2016-08-14) +++++++++++++++++++ Features: - [apispec.core]: Maintain order in which paths are added to a spec (:issue:`87`). Thanks :user:`ranjanashish` for the PR. - [apispec.ext.marshmallow]: Maintain order of fields when ``ordered=True`` on Schema. Thanks again :user:`ranjanashish`. 0.13.0 (2016-07-03) +++++++++++++++++++ Features: - [apispec.ext.marshmallow]: Add support for ``Dict`` field (:issue:`80`). Thanks :user:`ericb` for the PR. - [apispec.ext.marshmallow]: ``dump_only`` fields add ``readOnly`` flag in OpenAPI spec (:issue:`79`). Thanks :user:`itajaja` for the suggestion and PR. Bug fixes: - [apispec.ext.marshmallow]: Properly exclude nested dump-only fields from parameters (:issue:`82`). Thanks :user:`incognick` for the catch and patch. Support: - Update tasks.py for compatibility with invoke>=0.13.0. 0.12.0 (2016-05-22) +++++++++++++++++++ Features: - [apispec.ext.marshmallow]: Inspect validators to set additional attributes (:issue:`66`). Thanks :user:`deckar01` for the PR. Bug fixes: - [apispec.ext.marshmallow]: Respect ``partial`` parameters on ``Schemas`` (:issue:`74`). Thanks :user:`incognick` for reporting. 0.11.1 (2016-05-02) +++++++++++++++++++ Bug fixes: - [apispec.ext.flask]: Flask plugin respects ``APPLICATION_ROOT`` from app's config (:issue:`69`). Thanks :user:`deckar01` for the catch and patch. - [apispec.ext.marshmallow]: Fix support for plural schema instances (:issue:`71`). Thanks again :user:`deckar01`. 0.11.0 (2016-04-12) +++++++++++++++++++ Features: - Support vendor extensions on paths (:issue:`65`). Thanks :user:`lucascosta` for the PR. - *Backwards-incompatible*: Remove support for old versions (<=0.15.0) of webargs. Bug fixes: - Fix error message when plugin does not have a ``setup()`` function. - [apispec.ext.marshmallow] Fix bug in introspecting self-referencing marshmallow fields, i.e. ``fields.Nested('self')`` (:issue:`55`). Thanks :user:`whoiswes` for reporting. - [apispec.ext.marshmallow] ``field2property`` no longer pops off ``location`` from a field's metadata (:issue:`67`). Support: - Lots of new docs, including a User Guide and improved extension docs. 0.10.1 (2016-04-09) +++++++++++++++++++ Note: This version is a re-upload of 0.10.0. There is no 0.10.0 release on PyPI. Features: - Add Tornado extension (:issue:`62`). Bug fixes: - Compatibility fix with marshmallow>=2.7.0 (:issue:`64`). - Fix bug that raised error for Swagger parameters that didn't include the ``in`` key (:issue:`63`). Big thanks :user:`lucascosta` for all these changes. 0.9.1 (2016-03-17) ++++++++++++++++++ Bug fixes: - Fix generation of metadata for ``Nested`` fields (:issue:`61`). Thanks :user:`martinlatrille`. 0.9.0 (2016-03-13) ++++++++++++++++++ Features: - Add ``APISpec.add_tags`` method for adding Swagger tags. Thanks :user:`martinlatrille`. Bug fixes: - Fix bug in marshmallow extension where metadata was being lost when converting marshmallow ``Schemas`` when ``many=False``. Thanks again :user:`martinlatrille`. Other changes: - Remove duplicate ``SWAGGER_VERSION`` from ``api.ext.marshmallow.swagger``. Support: - Update docs to reflect rename of Swagger to OpenAPI. 0.8.0 (2016-03-06) ++++++++++++++++++ Features: - ``apispec.ext.marshmallow.swagger.schema2jsonschema`` properly introspects ``Schema`` instances when ``many=True`` (:issue:`53`). Thanks :user:`frol` for the PR. Bug fixes: - Fix error reporting when an invalid object is passed to ``schema2jsonschema`` or ``schema2parameters`` (:issue:`52`). Thanks again :user:`frol`. 0.7.0 (2016-02-11) ++++++++++++++++++ Features: - ``APISpec.add_path`` accepts ``Path`` objects (:issue:`49`). Thanks :user:`Trii` for the suggestion and the implementation. Bug fixes: - Use correct field name in "required" array when ``load_from`` and ``dump_to`` are used (:issue:`48`). Thanks :user:`benbeadle` for the catch and patch. 0.6.0 (2016-01-04) ++++++++++++++++++ Features: - Add ``APISpec#add_parameter`` for adding common Swagger parameter objects. Thanks :user:`jta`. - The field name in a spec will be adjusted if a ``Field's`` ``load_from`` and ``dump_to`` attributes are the same. :issue:`43`. Thanks again :user:`jta`. Bug fixes: - Fix bug that caused a stack overflow when adding nested Schemas to an ``APISpec`` (:issue:`31`, :issue:`41`). Thanks :user:`alapshin` and :user:`itajaja` for reporting. Thanks :user:`itajaja` for the patch. 0.5.0 (2015-12-13) ++++++++++++++++++ - ``schema2jsonschema`` and ``schema2parameters`` can introspect a marshmallow ``Schema`` instance as well as a ``Schema`` class (:issue:`37`). Thanks :user:`frol`. - *Backwards-incompatible*: The first argument to ``schema2jsonschema`` and ``schema2parameters`` was changed from ``schema_cls`` to ``schema``. Bug fixes: - Handle conflicting signatures for plugin helpers. Thanks :user:`AndrewPashkin` for the catch and patch. 0.4.2 (2015-11-23) ++++++++++++++++++ - Skip dump-only fields when ``dump=False`` is passed to ``schema2parameters`` and ``fields2parameters``. Thanks :user:`frol`. Bug fixes: - Raise ``SwaggerError`` when ``validate_swagger`` fails. Thanks :user:`frol`. 0.4.1 (2015-10-19) ++++++++++++++++++ - Correctly pass ``dump`` parameter to ``field2parameters``. 0.4.0 (2015-10-18) ++++++++++++++++++ - Add ``dump`` parameter to ``field2property`` (:issue:`32`). 0.3.0 (2015-10-02) ++++++++++++++++++ - Rename and repackage as "apispec". - Support ``enum`` field of JSON Schema based on ``OneOf`` and ``ContainsOnly`` validators. 0.2.0 (2015-09-27) ++++++++++++++++++ - Add ``schema2parameters``, ``fields2parameters``, and ``field2parameters``. - Removed ``Fixed`` from ``swagger.FIELD_MAPPING`` for compatibility with marshmallow>=2.0.0. 0.1.0 (2015-09-13) ++++++++++++++++++ - First release. apispec-6.8.1/CONTRIBUTING.rst000066400000000000000000000066561473713346600156460ustar00rootroot00000000000000Contributing Guidelines ======================= Security Contact Information ---------------------------- To report a security vulnerability, please use the `Tidelift security contact `_. Tidelift will coordinate the fix and disclosure. Questions, Feature Requests, Bug Reports, and Feedback. . . ----------------------------------------------------------- . . .should all be reported on the `GitHub Issue Tracker`_ . .. _`GitHub Issue Tracker`: https://github.com/marshmallow-code/apispec/issues?state=open Contributing Code ----------------- Setting Up for Local Development ++++++++++++++++++++++++++++++++ 1. Fork apispec_ on GitHub. :: $ git clone https://github.com/marshmallow-code/apispec.git $ cd apispec 2. Install development requirements. **It is highly recommended that you use a virtualenv.** Use the following command to install an editable version of apispec along with its development requirements. :: # After activating your virtualenv $ pip install -e '.[dev]' 3. Install the pre-commit hooks, which will format and lint your git staged files. :: # The pre-commit CLI was installed above $ pre-commit install Git Branch Structure ++++++++++++++++++++ apispec abides by the following branching model: ``dev`` Current development branch. **New features should branch off here**. ``X.Y-line`` Maintenance branch for release ``X.Y``. **Bug fixes should be sent to the most recent release branch.** The maintainer will forward-port the fix to ``dev``. Note: exceptions may be made for bug fixes that introduce large code changes. **Always make a new branch for your work**, no matter how small. Also, **do not put unrelated changes in the same branch or pull request**. This makes it more difficult to merge your changes. Pull Requests ++++++++++++++ 1. Create a new local branch. :: # For a new feature $ git checkout -b name-of-feature dev # For a bugfix $ git checkout -b fix-something 1.2-line 2. Commit your changes. Write `good commit messages `_. :: $ git commit -m "Detailed commit message" $ git push origin name-of-feature 3. Before submitting a pull request, check the following: - If the pull request adds functionality, it is tested and the docs are updated. - You've added yourself to ``AUTHORS.rst``. 4. Submit a pull request to ``marshmallow-code:dev`` or the appropriate maintenance branch. The `CI `_ build must be passing before your pull request is merged. Running Tests +++++++++++++ To run all tests: :: $ pytest To run syntax checks: :: $ tox -e lint (Optional) To run tests in all supported Python versions in their own virtual environments (must have each interpreter installed): :: $ tox Documentation +++++++++++++ Contributions to the documentation are welcome. Documentation is written in `reStructuredText`_ (rST). A quick rST reference can be found `here `_. Builds are powered by Sphinx_. To build the docs in "watch" mode: :: $ tox -e watch-docs Changes in the `docs/` directory will automatically trigger a rebuild. .. _Sphinx: http://sphinx.pocoo.org/ .. _`reStructuredText`: https://docutils.sourceforge.io/rst.html .. _`apispec`: https://github.com/marshmallow-code/apispec apispec-6.8.1/LICENSE000066400000000000000000000020771473713346600142030ustar00rootroot00000000000000Copyright Steven Loria, Jérôme Lafréchoux, and contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. apispec-6.8.1/README.rst000066400000000000000000000202231473713346600146560ustar00rootroot00000000000000******* apispec ******* |pypi| |build-status| |docs| |marshmallow-support| |openapi| .. |pypi| image:: https://badgen.net/pypi/v/apispec :target: https://pypi.org/project/apispec/ :alt: PyPI package .. |build-status| image:: https://github.com/marshmallow-code/apispec/actions/workflows/build-release.yml/badge.svg :target: https://github.com/marshmallow-code/webargs/actions/workflows/build-release.yml :alt: Build status .. |docs| image:: https://readthedocs.org/projects/apispec/badge/ :target: https://apispec.readthedocs.io/ :alt: Documentation .. |marshmallow-support| image:: https://badgen.net/badge/marshmallow/3,4?list=1 :target: https://marshmallow.readthedocs.io/en/latest/upgrading.html :alt: marshmallow 3|4 compatible .. |openapi| image:: https://badgen.net/badge/OAS/2,3?list=1&color=cyan :target: https://github.com/OAI/OpenAPI-Specification :alt: OpenAPI Specification 2/3 compatible A pluggable API specification generator. Currently supports the `OpenAPI Specification `_ (f.k.a. the Swagger specification). Features ======== - Supports the OpenAPI Specification (versions 2 and 3) - Framework-agnostic - Built-in support for `marshmallow `_ - Utilities for parsing docstrings Installation ============ :: $ pip install -U apispec When using the marshmallow plugin, ensure a compatible marshmallow version is used: :: $ pip install -U apispec[marshmallow] Example Application =================== .. code-block:: python from apispec import APISpec from apispec.ext.marshmallow import MarshmallowPlugin from apispec_webframeworks.flask import FlaskPlugin from flask import Flask from marshmallow import Schema, fields # Create an APISpec spec = APISpec( title="Swagger Petstore", version="1.0.0", openapi_version="3.0.2", plugins=[FlaskPlugin(), MarshmallowPlugin()], ) # Optional marshmallow support class CategorySchema(Schema): id = fields.Int() name = fields.Str(required=True) class PetSchema(Schema): category = fields.List(fields.Nested(CategorySchema)) name = fields.Str() # Optional security scheme support api_key_scheme = {"type": "apiKey", "in": "header", "name": "X-API-Key"} spec.components.security_scheme("ApiKeyAuth", api_key_scheme) # Optional Flask support app = Flask(__name__) @app.route("/random") def random_pet(): """A cute furry animal endpoint. --- get: description: Get a random pet security: - ApiKeyAuth: [] responses: 200: content: application/json: schema: PetSchema """ pet = get_random_pet() return PetSchema().dump(pet) # Register the path and the entities within it with app.test_request_context(): spec.path(view=random_pet) Generated OpenAPI Spec ---------------------- .. code-block:: python import json print(json.dumps(spec.to_dict(), indent=2)) # { # "paths": { # "/random": { # "get": { # "description": "Get a random pet", # "security": [ # { # "ApiKeyAuth": [] # } # ], # "responses": { # "200": { # "content": { # "application/json": { # "schema": { # "$ref": "#/components/schemas/Pet" # } # } # } # } # } # } # } # }, # "tags": [], # "info": { # "title": "Swagger Petstore", # "version": "1.0.0" # }, # "openapi": "3.0.2", # "components": { # "parameters": {}, # "responses": {}, # "schemas": { # "Category": { # "type": "object", # "properties": { # "name": { # "type": "string" # }, # "id": { # "type": "integer", # "format": "int32" # } # }, # "required": [ # "name" # ] # }, # "Pet": { # "type": "object", # "properties": { # "name": { # "type": "string" # }, # "category": { # "type": "array", # "items": { # "$ref": "#/components/schemas/Category" # } # } # } # } # "securitySchemes": { # "ApiKeyAuth": { # "type": "apiKey", # "in": "header", # "name": "X-API-Key" # } # } # } # } # } print(spec.to_yaml()) # components: # parameters: {} # responses: {} # schemas: # Category: # properties: # id: {format: int32, type: integer} # name: {type: string} # required: [name] # type: object # Pet: # properties: # category: # items: {$ref: '#/components/schemas/Category'} # type: array # name: {type: string} # type: object # securitySchemes: # ApiKeyAuth: # in: header # name: X-API-KEY # type: apiKey # info: {title: Swagger Petstore, version: 1.0.0} # openapi: 3.0.2 # paths: # /random: # get: # description: Get a random pet # responses: # 200: # content: # application/json: # schema: {$ref: '#/components/schemas/Pet'} # security: # - ApiKeyAuth: [] # tags: [] Documentation ============= Documentation is available at https://apispec.readthedocs.io/ . Ecosystem ========= A list of apispec-related libraries can be found at the GitHub wiki here: https://github.com/marshmallow-code/apispec/wiki/Ecosystem Support apispec =============== apispec is maintained by a group of `volunteers `_. If you'd like to support the future of the project, please consider contributing to our Open Collective: .. image:: https://opencollective.com/marshmallow/donate/button.png :target: https://opencollective.com/marshmallow :width: 200 :alt: Donate to our collective Professional Support ==================== Professionally-supported apispec is available through the `Tidelift Subscription `_. Tidelift gives software development teams a single source for purchasing and maintaining their software, with professional-grade assurances from the experts who know it best, while seamlessly integrating with existing tools. [`Get professional support`_] .. _`Get professional support`: https://tidelift.com/subscription/pkg/pypi-apispec?utm_source=pypi-apispec&utm_medium=referral&utm_campaign=readme .. image:: https://user-images.githubusercontent.com/2379650/45126032-50b69880-b13f-11e8-9c2c-abd16c433495.png :target: https://tidelift.com/subscription/pkg/pypi-apispec?utm_source=pypi-apispec&utm_medium=referral&utm_campaign=readme :alt: Get supported apispec with Tidelift Security Contact Information ============================ To report a security vulnerability, please use the `Tidelift security contact `_. Tidelift will coordinate the fix and disclosure. Project Links ============= - Docs: https://apispec.readthedocs.io/ - Changelog: https://apispec.readthedocs.io/en/latest/changelog.html - Contributing Guidelines: https://apispec.readthedocs.io/en/latest/contributing.html - PyPI: https://pypi.python.org/pypi/apispec - Issues: https://github.com/marshmallow-code/apispec/issues License ======= MIT licensed. See the bundled `LICENSE `_ file for more details. apispec-6.8.1/RELEASING.md000066400000000000000000000004231473713346600150220ustar00rootroot00000000000000# Releasing 1. Bump version in `pyproject.toml` and update the changelog with today's date. 2. Commit: `git commit -m "Bump version and update changelog"` 3. Tag the commit: `git tag x.y.z` 4. Push: `git push --tags origin dev`. CI will take care of the PyPI release. apispec-6.8.1/SECURITY.md000066400000000000000000000003001473713346600147520ustar00rootroot00000000000000# Security Contact Information To report a security vulnerability, please use the [Tidelift security contact](https://tidelift.com/security). Tidelift will coordinate the fix and disclosure. apispec-6.8.1/docs/000077500000000000000000000000001473713346600141205ustar00rootroot00000000000000apispec-6.8.1/docs/Makefile000066400000000000000000000151721473713346600155660ustar00rootroot00000000000000# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = _build # User-friendly check for sphinx-build ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) endif # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . # the i18n builder cannot share the environment and doctrees with the others I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" @echo " text to make text files" @echo " man to make manual pages" @echo " texinfo to make Texinfo files" @echo " info to make Texinfo files and run them through makeinfo" @echo " gettext to make PO message catalogs" @echo " changes to make an overview of all changed/added/deprecated items" @echo " xml to make Docutils-native XML files" @echo " pseudoxml to make pseudoxml-XML files for display purposes" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" clean: rm -rf $(BUILDDIR)/* html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/complexity.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/complexity.qhc" devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/complexity" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/complexity" @echo "# devhelp" epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." latexpdf: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." $(MAKE) -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." latexpdfja: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through platex and dvipdfmx..." $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." texinfo: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." @echo "Run \`make' in that directory to run these through makeinfo" \ "(use \`make info' here to do that automatically)." info: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo "Running Texinfo files through makeinfo..." make -C $(BUILDDIR)/texinfo info @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." gettext: $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale @echo @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." xml: $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml @echo @echo "Build finished. The XML files are in $(BUILDDIR)/xml." pseudoxml: $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml @echo @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." apispec-6.8.1/docs/api_core.rst000066400000000000000000000005001473713346600164260ustar00rootroot00000000000000Core API ======== apispec ------- .. automodule:: apispec :members: apispec.core ------------ .. automodule:: apispec.core :members: Components apispec.exceptions ------------------ .. automodule:: apispec.exceptions :members: apispec.utils ------------- .. automodule:: apispec.utils :members: apispec-6.8.1/docs/api_ext.rst000066400000000000000000000012661473713346600163100ustar00rootroot00000000000000Built-in Plugins ================ apispec.ext.marshmallow ----------------------- .. automodule:: apispec.ext.marshmallow :members: apispec.ext.marshmallow.schema_resolver +++++++++++++++++++++++++++++++++++++++ .. automodule:: apispec.ext.marshmallow.schema_resolver :members: apispec.ext.marshmallow.openapi +++++++++++++++++++++++++++++++ .. automodule:: apispec.ext.marshmallow.openapi :members: apispec.ext.marshmallow.field_converter +++++++++++++++++++++++++++++++++++++++ .. automodule:: apispec.ext.marshmallow.field_converter :members: apispec.ext.marshmallow.common ++++++++++++++++++++++++++++++ .. automodule:: apispec.ext.marshmallow.common :members: apispec-6.8.1/docs/authors.rst000066400000000000000000000000341473713346600163340ustar00rootroot00000000000000.. include:: ../AUTHORS.rst apispec-6.8.1/docs/changelog.rst000066400000000000000000000002101473713346600165720ustar00rootroot00000000000000.. seealso:: Need help upgrading to a newer version? Check out the :doc:`upgrading guide `. .. include:: ../CHANGELOG.rst apispec-6.8.1/docs/conf.py000077500000000000000000000015131473713346600154220ustar00rootroot00000000000000import importlib import sphinx_rtd_theme extensions = [ "sphinx.ext.autodoc", "sphinx.ext.intersphinx", "sphinx.ext.viewcode", "sphinx.ext.todo", "sphinx_issues", ] primary_domain = "py" default_role = "py:obj" intersphinx_mapping = { "python": ("https://python.readthedocs.io/en/latest/", None), "marshmallow": ("https://marshmallow.readthedocs.io/en/latest/", None), "webargs": ("https://webargs.readthedocs.io/en/latest/", None), } issues_github_path = "marshmallow-code/apispec" source_suffix = ".rst" master_doc = "index" project = "apispec" copyright = "Steven Loria, Jérôme Lafréchoux, and contributors" version = release = importlib.metadata.version("apispec") exclude_patterns = ["_build"] # THEME html_theme = "sphinx_rtd_theme" html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] apispec-6.8.1/docs/contributing.rst000066400000000000000000000000411473713346600173540ustar00rootroot00000000000000.. include:: ../CONTRIBUTING.rst apispec-6.8.1/docs/ecosystem.rst000066400000000000000000000002321473713346600166620ustar00rootroot00000000000000Ecosystem ========= A list of apispec-related projects can be found at the GitHub wiki here: https://github.com/marshmallow-code/apispec/wiki/Ecosystem apispec-6.8.1/docs/index.rst000066400000000000000000000132211473713346600157600ustar00rootroot00000000000000******* apispec ******* Release v\ |version| (:doc:`Changelog `) A pluggable API specification generator. Currently supports the `OpenAPI Specification `_ (f.k.a. the Swagger specification). Features ======== - Supports the OpenAPI Specification (versions 2 and 3) - Framework-agnostic - Built-in support for `marshmallow `_ - Utilities for parsing docstrings Example Application =================== .. code-block:: python import uuid from apispec import APISpec from apispec.ext.marshmallow import MarshmallowPlugin from apispec_webframeworks.flask import FlaskPlugin from flask import Flask from marshmallow import Schema, fields # Create an APISpec spec = APISpec( title="Swagger Petstore", version="1.0.0", openapi_version="3.0.2", plugins=[FlaskPlugin(), MarshmallowPlugin()], ) # Optional marshmallow support class CategorySchema(Schema): id = fields.Int() name = fields.Str(required=True) class PetSchema(Schema): categories = fields.List(fields.Nested(CategorySchema)) name = fields.Str() # Optional security scheme support api_key_scheme = {"type": "apiKey", "in": "header", "name": "X-API-Key"} spec.components.security_scheme("ApiKeyAuth", api_key_scheme) # Optional Flask support app = Flask(__name__) @app.route("/random") def random_pet(): """A cute furry animal endpoint. --- get: description: Get a random pet security: - ApiKeyAuth: [] responses: 200: description: Return a pet content: application/json: schema: PetSchema """ # Hardcoded example data pet_data = { "name": "sample_pet_" + str(uuid.uuid1()), "categories": [{"id": 1, "name": "sample_category"}], } return PetSchema().dump(pet_data) # Register the path and the entities within it with app.test_request_context(): spec.path(view=random_pet) Generated OpenAPI Spec ---------------------- .. code-block:: python import json print(json.dumps(spec.to_dict(), indent=2)) # { # "info": { # "title": "Swagger Petstore", # "version": "1.0.0" # }, # "openapi": "3.0.2", # "components": { # "schemas": { # "Category": { # "type": "object", # "properties": { # "id": { # "type": "integer", # "format": "int32" # }, # "name": { # "type": "string" # } # }, # "required": [ # "name" # ] # }, # "Pet": { # "type": "object", # "properties": { # "categories": { # "type": "array", # "items": { # "$ref": "#/components/schemas/Category" # } # }, # "name": { # "type": "string" # } # } # }, # } # }, # "securitySchemes": { # "ApiKeyAuth": { # "type": "apiKey", # "in": "header", # "name": "X-API-Key" # } # }, # "paths": { # "/random": { # "get": { # "description": "Get a random pet", # "security": [ # { # "ApiKeyAuth": [] # } # ], # "responses": { # "200": { # "description": "Return a pet", # "content": { # "application/json": { # "schema": { # "$ref": "#/components/schemas/Pet" # } # } # } # } # } # } # } # }, # } print(spec.to_yaml()) # info: # title: Swagger Petstore # version: 1.0.0 # openapi: 3.0.2 # components: # schemas: # Category: # properties: # id: # format: int32 # type: integer # name: # type: string # required: # - name # type: object # Pet: # properties: # categories: # items: # $ref: '#/components/schemas/Category' # type: array # name: # type: string # type: object # securitySchemes: # ApiKeyAuth: # in: header # name: X-API-KEY # type: apiKey # paths: # /random: # get: # description: Get a random pet # responses: # '200': # content: # application/json: # schema: # $ref: '#/components/schemas/Pet' # description: Return a pet # security: # - ApiKeyAuth: [] User Guide ========== .. toctree:: :maxdepth: 2 install quickstart using_plugins writing_plugins special_topics API Reference ============= .. toctree:: :maxdepth: 2 api_core api_ext Project Links ============= - `apispec @ GitHub `_ - `Issue Tracker `_ Project Info ============ .. toctree:: :maxdepth: 1 changelog upgrading ecosystem authors contributing license apispec-6.8.1/docs/install.rst000066400000000000000000000007121473713346600163200ustar00rootroot00000000000000Install ======= From the PyPI ------------- To install the latest version from the PyPI: :: pip install -U apispec To install with validation support: :: pip install -U 'apispec[validation]' To install with YAML support: :: pip install -U 'apispec[yaml]' Get the Bleeding Edge Version ----------------------------- To install the latest development version: :: pip install -U git+https://github.com/marshmallow-code/apispec@dev apispec-6.8.1/docs/license.rst000066400000000000000000000000701473713346600162710ustar00rootroot00000000000000******* License ******* .. literalinclude:: ../LICENSE apispec-6.8.1/docs/make.bat000066400000000000000000000145031473713346600155300ustar00rootroot00000000000000@ECHO OFF REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=sphinx-build ) set BUILDDIR=_build set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . set I18NSPHINXOPTS=%SPHINXOPTS% . if NOT "%PAPER%" == "" ( set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% ) if "%1" == "" goto help if "%1" == "help" ( :help echo.Please use `make ^` where ^ is one of echo. html to make standalone HTML files echo. dirhtml to make HTML files named index.html in directories echo. singlehtml to make a single large HTML file echo. pickle to make pickle files echo. json to make JSON files echo. htmlhelp to make HTML files and a HTML help project echo. qthelp to make HTML files and a qthelp project echo. devhelp to make HTML files and a Devhelp project echo. epub to make an epub echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter echo. text to make text files echo. man to make manual pages echo. texinfo to make Texinfo files echo. gettext to make PO message catalogs echo. changes to make an overview over all changed/added/deprecated items echo. xml to make Docutils-native XML files echo. pseudoxml to make pseudoxml-XML files for display purposes echo. linkcheck to check all external links for integrity echo. doctest to run all doctests embedded in the documentation if enabled goto end ) if "%1" == "clean" ( for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i del /q /s %BUILDDIR%\* goto end ) %SPHINXBUILD% 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 ) if "%1" == "html" ( %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/html. goto end ) if "%1" == "dirhtml" ( %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. goto end ) if "%1" == "singlehtml" ( %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. goto end ) if "%1" == "pickle" ( %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the pickle files. goto end ) if "%1" == "json" ( %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the JSON files. goto end ) if "%1" == "htmlhelp" ( %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run HTML Help Workshop with the ^ .hhp project file in %BUILDDIR%/htmlhelp. goto end ) if "%1" == "qthelp" ( %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run "qcollectiongenerator" with the ^ .qhcp project file in %BUILDDIR%/qthelp, like this: echo.^> qcollectiongenerator %BUILDDIR%\qthelp\complexity.qhcp echo.To view the help file: echo.^> assistant -collectionFile %BUILDDIR%\qthelp\complexity.ghc goto end ) if "%1" == "devhelp" ( %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp if errorlevel 1 exit /b 1 echo. echo.Build finished. goto end ) if "%1" == "epub" ( %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub if errorlevel 1 exit /b 1 echo. echo.Build finished. The epub file is in %BUILDDIR%/epub. goto end ) if "%1" == "latex" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex if errorlevel 1 exit /b 1 echo. echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. goto end ) if "%1" == "latexpdf" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex cd %BUILDDIR%/latex make all-pdf cd %BUILDDIR%/.. echo. echo.Build finished; the PDF files are in %BUILDDIR%/latex. goto end ) if "%1" == "latexpdfja" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex cd %BUILDDIR%/latex make all-pdf-ja cd %BUILDDIR%/.. echo. echo.Build finished; the PDF files are in %BUILDDIR%/latex. goto end ) if "%1" == "text" ( %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text if errorlevel 1 exit /b 1 echo. echo.Build finished. The text files are in %BUILDDIR%/text. goto end ) if "%1" == "man" ( %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man if errorlevel 1 exit /b 1 echo. echo.Build finished. The manual pages are in %BUILDDIR%/man. goto end ) if "%1" == "texinfo" ( %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo if errorlevel 1 exit /b 1 echo. echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. goto end ) if "%1" == "gettext" ( %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale if errorlevel 1 exit /b 1 echo. echo.Build finished. The message catalogs are in %BUILDDIR%/locale. goto end ) if "%1" == "changes" ( %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes if errorlevel 1 exit /b 1 echo. echo.The overview file is in %BUILDDIR%/changes. goto end ) if "%1" == "linkcheck" ( %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck if errorlevel 1 exit /b 1 echo. echo.Link check complete; look for any errors in the above output ^ or in %BUILDDIR%/linkcheck/output.txt. goto end ) if "%1" == "doctest" ( %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest if errorlevel 1 exit /b 1 echo. echo.Testing of doctests in the sources finished, look at the ^ results in %BUILDDIR%/doctest/output.txt. goto end ) if "%1" == "xml" ( %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml if errorlevel 1 exit /b 1 echo. echo.Build finished. The XML files are in %BUILDDIR%/xml. goto end ) if "%1" == "pseudoxml" ( %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml if errorlevel 1 exit /b 1 echo. echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. goto end ) :end apispec-6.8.1/docs/quickstart.rst000066400000000000000000000057641473713346600170600ustar00rootroot00000000000000Quickstart ========== Basic Usage ----------- First, create an `APISpec ` object, passing basic information about your API. .. code-block:: python from apispec import APISpec spec = APISpec( title="Gisty", version="1.0.0", openapi_version="3.0.2", info=dict(description="A minimal gist API"), ) Add schemas to your spec using `spec.components.schema `. .. code-block:: python spec.components.schema( "Gist", { "properties": { "id": {"type": "integer", "format": "int64"}, "name": {"type": "string"}, } }, ) Add paths to your spec using `path `. .. code-block:: python spec.path( path="/gist/{gist_id}", operations=dict( get=dict( responses={"200": {"content": {"application/json": {"schema": "Gist"}}}} ) ), ) The API is chainable, allowing you to combine multiple method calls in one statement: .. code-block:: python spec.path(...).path(...).tag(...) spec.components.schema(...).parameter(...) To output your OpenAPI spec, invoke the `to_dict ` method. .. code-block:: python from pprint import pprint pprint(spec.to_dict()) # {'components': {'parameters': {}, # 'responses': {}, # 'schemas': {'Gist': {'properties': {'id': {'format': 'int64', # 'type': 'integer'}, # 'name': {'type': 'string'}}}}}, # 'info': {'description': 'A minimal gist API', # 'title': 'Gisty', # 'version': '1.0.0'}, # 'openapi': '3.0.2', # 'paths': {'/gist/{gist_id}': # {'get': {'responses': {'200': {'content': {'application/json': {'schema': {'$ref': '#/definitions/Gist'}}}}}}}}, # 'tags': []} Use `to_yaml ` to export your spec to YAML. .. code-block:: python print(spec.to_yaml()) # components: # parameters: {} # responses: {} # schemas: # Gist: # properties: # id: {format: int64, type: integer} # name: {type: string} # info: {description: A minimal gist API, title: Gisty, version: 1.0.0} # openapi: 3.0.2 # paths: # /gist/{gist_id}: # get: # responses: # '200': # content: # application/json: # schema: {$ref: '#/definitions/Gist'} # tags: [] .. seealso:: For a full reference of the `APISpec ` class, see the :doc:`Core API Reference `. Next Steps ---------- We've learned how to programmatically construct an OpenAPI spec, but defining our entities was verbose. In the next section, we'll learn how to let plugins do the dirty work: :doc:`Using Plugins `. apispec-6.8.1/docs/special_topics.rst000066400000000000000000000120771473713346600176620ustar00rootroot00000000000000Special Topics ============== Solutions to specific problems are documented here. Adding Additional Fields To Schema Objects ------------------------------------------ To add additional fields (e.g. ``"discriminator"``) to Schema objects generated from `spec.components.schema ` , pass them to the ``component`` parameter. If your'e using ``MarshmallowPlugin``, the ``component`` properties will get merged with the autogenerated properties. .. code-block:: python properties = { "id": {"type": "integer", "format": "int64"}, "name": {"type": "string", "example": "doggie"}, } spec.components.schema("Pet", component={"discriminator": "petType"}, schema=PetSchema) .. note:: Be careful about the input that you pass to ``component``. ``apispec`` will not guarantee that the passed fields are valid against the OpenAPI spec. Rendering to YAML or JSON ------------------------- YAML ++++ .. code-block:: python spec.to_yaml() .. note:: `to_yaml ` requires `PyYAML` to be installed. You can install apispec with YAML support using: :: pip install 'apispec[yaml]' JSON ++++ .. code-block:: python import json json.dumps(spec.to_dict()) Documenting Top-level Components -------------------------------- The ``APISpec`` object contains helpers to add top-level components: .. list-table:: :header-rows: 1 * - Component type - Helper method - OpenAPI version * - Schema (f.k.a. "definition" in OAS v2) - `spec.components.schema ` - 2, 3 * - Parameter - `spec.components.parameter ` - 2, 3 * - Response - `spec.components.response ` - 2, 3 * - Header - `spec.components.response ` - 3 * - Example - `spec.components.response ` - 3 * - Security scheme - `spec.components.response ` - 2, 3 Most component registration methods provide a ``lazy`` keyword argument, allowing to define a component but only publish it in the generated documentation if it is actually referenced. To add other top-level objects, pass them to the ``APISpec`` as keyword arguments. Here is an example that includes a `Server Object `_. .. code-block:: python import yaml from apispec import APISpec from apispec.ext.marshmallow import MarshmallowPlugin from apispec.utils import validate_spec OPENAPI_SPEC = """ openapi: 3.0.2 info: description: Server API document title: Server API version: 1.0.0 servers: - url: http://localhost:{port}/ description: The development API server variables: port: enum: - '3000' - '8888' default: '3000' """ settings = yaml.safe_load(OPENAPI_SPEC) # retrieve title, version, and openapi version title = settings["info"].pop("title") spec_version = settings["info"].pop("version") openapi_version = settings.pop("openapi") spec = APISpec( title=title, version=spec_version, openapi_version=openapi_version, plugins=(MarshmallowPlugin(),), **settings ) validate_spec(spec) Documenting Security Schemes ---------------------------- Use `spec.components.security_scheme ` to document `Security Scheme Objects `_. .. code-block:: python from pprint import pprint from apispec import APISpec spec = APISpec(title="Swagger Petstore", version="1.0.0", openapi_version="3.0.2") api_key_scheme = {"type": "apiKey", "in": "header", "name": "X-API-Key"} jwt_scheme = {"type": "http", "scheme": "bearer", "bearerFormat": "JWT"} spec.components.security_scheme("api_key", api_key_scheme) spec.components.security_scheme("jwt", jwt_scheme) pprint(spec.to_dict()["components"]["securitySchemes"], indent=2) # { 'api_key': {'in': 'header', 'name': 'X-API-Key', 'type': 'apiKey'}, # 'jwt': {'bearerFormat': 'JWT', 'scheme': 'bearer', 'type': 'http'}} Referencing Top-level Components -------------------------------- On OpenAPI, top-level component are meant to be referenced using a ``$ref``, as in ``{$ref: '#/components/schemas/Pet'}`` (OpenAPI v3) or ``{$ref: '#/definitions/Pet'}`` (OpenAPI v2). APISpec automatically resolves references in paths and in components themselves when a string is provided while a dict is expected. Passing a fully-resolved reference is not supported. In other words, rather than passing ``{"schema": {$ref: '#/components/schemas/Pet'}}``, the user must pass ``{"schema": "Pet"}``. APISpec assumes a schema reference named ``"Pet"`` has been defined and builds the reference using the components location corresponding to the OpenAPI version. apispec-6.8.1/docs/upgrading.rst000066400000000000000000000131351473713346600166350ustar00rootroot00000000000000Upgrading to Newer Releases =========================== This section documents migration paths to new releases. Upgrading to 5.0.0 ------------------ Upgrading to 4.0.0 ------------------ location is ignored in field metadata ************************************* ``location`` parameter is ignored in ``Field`` metadata. A ``Schema`` can only have a single location. A ``Schema`` with fields from different locations must be split into multiple ``Schema``s. Upgrading to 3.0.0 ------------------ Upgrading to 2.0.0 ------------------ plugin helpers must accept extra `**kwargs` ******************************************* Since custom plugins helpers may define extra kwargs and those kwargs are passed to all plugin helpers by :meth:`APISpec.path `, all plugins should accept unknown kwargs. The example plugin below defines an additional `func` argument and accepts extra `**kwargs`. .. code-block:: python :emphasize-lines: 2 class MyPlugin(BasePlugin): def path_helper(self, path, func, **kwargs): """Path helper that parses docstrings for operations. Adds a ``func`` parameter to `apispec.APISpec.path`. """ operations.update(load_operations_from_docstring(func.__doc__)) Components must be referenced by ID, not full path ************************************************** While apispec 1.x would let the user reference components by path or ID, apispec 2.x only accepts references by ID. .. code-block:: python # apispec<2.0.0 spec.path( path="/gist/{gist_id}", operations=dict( get=dict( responses={ "200": { "content": { "application/json": {"schema": {"$ref": "#/definitions/Gist"}} } } } ) ), ) # apispec>=2.0.0 spec.path( path="/gist/{gist_id}", operations=dict( get=dict( responses={"200": {"content": {"application/json": {"schema": "Gist"}}}} ) ), ) References by ID are accepted by both apispec 1.x ad 2.x and are a better choice because they delegate the creation of the full component path to apispec. This allows more flexibility as apispec creates the component path according to the OpenAPI version. Upgrading to 1.0.0 ------------------ ``openapi_version`` Is Required ******************************* ``openapi_version`` no longer defaults to ``"2.0"``. It is now a required argument. .. code-block:: python :emphasize-lines: 4 spec = APISpec( title="Swagger Petstore", version="1.0.0", openapi_version="2.0", # or "3.0.2" plugins=[MarshmallowPlugin()], ) Web Framework Plugins Packaged Separately ***************************************** ``apispec.ext.flask``, ``apispec.ext.bottle``, and ``apispec.ext.tornado`` have been moved to a a separate package, `apispec-webframeworks `_. If you use these plugins, install ``apispec-webframeworks`` with ``pip``: :: $ pip install apispec-webframeworks Then, update your imports: .. code-block:: python # apispec<1.0.0 from apispec.ext.flask import FlaskPlugin # apispec>=1.0.0 from apispec_webframeworks.flask import FlaskPlugin YAML Support Is Optional ************************ YAML functionality is now optional. To install with YAML support: :: $ pip install 'apispec[yaml]' You will need to do this if you use ``apispec-webframeworks`` or call `APISpec.to_yaml ` in your code. Registering Entities ******************** Methods for registering OAS entities are changed to the noun form for internal consistency and for consistency with OAS v3 terminology. .. code-block:: python # apispec<1.0.0 spec.add_tag({"name": "Pet", "description": "Operations on pets"}) spec.add_path("/pets/", operations={...}) spec.definition("Pet", properties={...}) spec.add_parameter("PetID", "path", {...}) # apispec>=1.0.0 spec.tag({"name": "Pet", "description": "Operations on pets"}) spec.path("/pets/", operations={...}) spec.components.schema("Pet", {"properties": {...}}) spec.components.parameter("PetID", "path", {...}) Adding Additional Fields to Schemas *********************************** The ``extra_fields`` parameter to ``schema`` is removed. It is no longer necessary. Pass all fields in to the component ``dict``. .. code-block:: python # <1.0.0 spec.definition("Pet", schema=PetSchema, extra_fields={"discriminator": "name"}) # >=1.0.0 spec.components.schema("Pet", schema=PetSchema, component={"discriminator": "name"}) Nested Schemas Are Referenced ***************************** When using the `MarshmallowPlugin `, nested `Schema ` classes are referenced (with ``"$ref"``) in the output spec. By default, the name in the spec will be the class name with the "Schema" suffix removed, e.g. ``fields.Nested(PetSchema())`` -> ``"#components/schemas/Pet"``. The `ref` argument to `fields.Nested `_ is no longer respected. .. code-block:: python # apispec<1.0.0 class PetSchema(Schema): owner = fields.Nested( HumanSchema, # `ref` has no effect in 1.0.0. Remove. ref="#components/schemas/Human", ) # apispec>=1.0.0 class PetSchema(Schema): owner = fields.Nested(HumanSchema) .. seealso:: This behavior is customizable. See :ref:`marshmallow_nested_schemas`. apispec-6.8.1/docs/using_plugins.rst000066400000000000000000000317051473713346600175460ustar00rootroot00000000000000Using Plugins ============= What is an apispec "plugin"? ---------------------------- An apispec *plugin* is an object that provides helper methods for generating OpenAPI entities from objects in your application. A plugin may modify the behavior of `APISpec ` methods so that they can take your application's objects as input. Enabling Plugins ---------------- To enable a plugin, pass an instance to the constructor of `APISpec `. .. code-block:: python :emphasize-lines: 9 from apispec import APISpec from apispec.ext.marshmallow import MarshmallowPlugin spec = APISpec( title="Gisty", version="1.0.0", openapi_version="3.0.2", info=dict(description="A minimal gist API"), plugins=[MarshmallowPlugin()], ) Example: Flask and Marshmallow Plugins -------------------------------------- The bundled marshmallow plugin (`apispec.ext.marshmallow.MarshmallowPlugin`) provides helpers for generating OpenAPI schema and parameter objects from `marshmallow `_ schemas and fields. The `apispec-webframeworks `_ package includes a Flask plugin with helpers for generating path objects from view functions. Let's recreate the spec from the :doc:`Quickstart guide ` using these two plugins. First, ensure that ``apispec-webframeworks`` is installed: :: $ pip install apispec-webframeworks Also, ensure that a compatible ``marshmallow`` version is used: :: $ pip install -U apispec[marshmallow] We can now use the marshmallow and Flask plugins. .. code-block:: python from apispec import APISpec from apispec.ext.marshmallow import MarshmallowPlugin from apispec_webframeworks.flask import FlaskPlugin spec = APISpec( title="Gisty", version="1.0.0", openapi_version="3.0.2", info=dict(description="A minimal gist API"), plugins=[FlaskPlugin(), MarshmallowPlugin()], ) Our application will have a marshmallow `Schema ` for gists. .. code-block:: python from marshmallow import Schema, fields class GistParameter(Schema): gist_id = fields.Int() class GistSchema(Schema): id = fields.Int() content = fields.Str() The marshmallow plugin allows us to pass this `Schema` to `spec.components.schema `. .. code-block:: python spec.components.schema("Gist", schema=GistSchema) The schema is now added to the spec. .. code-block:: python from pprint import pprint pprint(spec.to_dict()) # {'components': {'parameters': {}, 'responses': {}, 'schemas': {}}, # 'info': {'description': 'A minimal gist API', # 'title': 'Gisty', # 'version': '1.0.0'}, # 'openapi': '3.0.2', # 'paths': {}, # 'tags': []} Our application will have a Flask route for the gist detail endpoint. We'll add some YAML in the docstring to add response information. .. code-block:: python from flask import Flask app = Flask(__name__) # NOTE: Plugins may inspect docstrings to gather more information for the spec @app.route("/gists/") def gist_detail(gist_id): """Gist detail view. --- get: parameters: - in: path schema: GistParameter responses: 200: content: application/json: schema: GistSchema """ return "details about gist {}".format(gist_id) The Flask plugin allows us to pass this view to `spec.path `. .. code-block:: python # Since path inspects the view and its route, # we need to be in a Flask request context with app.test_request_context(): spec.path(view=gist_detail) Our OpenAPI spec now looks like this: .. code-block:: python pprint(spec.to_dict()) # {'components': {'parameters': {}, # 'responses': {}, # 'schemas': {'Gist': {'properties': {'content': {'type': 'string'}, # 'id': {'format': 'int32', # 'type': 'integer'}}, # 'type': 'object'}}}, # 'info': {'description': 'A minimal gist API', # 'title': 'Gisty', # 'version': '1.0.0'}, # 'openapi': '3.0.2', # 'paths': {'/gists/{gist_id}': {'get': {'parameters': [{'in': 'path', # 'name': 'gist_id', # 'required': True, # 'schema': {'format': 'int32', # 'type': 'integer'}}], # 'responses': {200: {'content': {'application/json': {'schema': {'$ref': '#/components/schemas/Gist'}}}}}}}}, # 'tags': []} If your API uses `method-based dispatching `_, the process is similar. Note that the method no longer needs to be included in the docstring. .. code-block:: python from flask.views import MethodView class GistApi(MethodView): def get(self): """Gist view --- description: Get a gist responses: 200: content: application/json: schema: GistSchema """ pass def post(self): pass method_view = GistApi.as_view("gist") app.add_url_rule("/gist", view_func=method_view) with app.test_request_context(): spec.path(view=method_view) pprint(dict(spec.to_dict()["paths"]["/gist"])) # {'get': {'description': 'get a gist', # 'responses': {200: {'content': {'application/json': {'schema': {'$ref': '#/components/schemas/Gist'}}}}}}, # 'post': {}} Marshmallow Plugin ------------------ .. _marshmallow_nested_schemas: Nested Schemas ************** By default, Marshmallow `Nested` fields are represented by a `JSON Reference object `_. If the schema has been added to the spec via `spec.components.schema `, the user-supplied name will be used in the reference. Otherwise apispec will add the nested schema to the spec using an automatically resolved name for the nested schema. The default `resolver ` function will resolve a name based on the schema's class `__name__`, dropping a trailing "Schema" so that `class PetSchema(Schema)` resolves to "Pet". To change the behavior of the name resolution simply pass a function accepting a `Schema` class, `Schema` instance or a string that resolves to a `Schema` class and returning a string to the plugin's constructor. To easily work with these argument types the marshmallow plugin provides `resolve_schema_cls ` and `resolve_schema_instance ` functions. If the `schema_name_resolver` function returns a value that evaluates to `False` in a boolean context the nested schema will not be added to the spec and instead defined in-line. .. note:: A `schema_name_resolver` function must return a string name when working with circular-referencing schemas in order to avoid infinite recursion. Schema Modifiers **************** apispec will respect schema modifiers such as ``exclude`` and ``partial`` in the generated schema definition. If a schema is initialized with modifiers, apispec will treat each combination of modifiers as a unique schema definition. Custom DateTime formats *********************** apispec supports all four basic formats of `marshmallow.fields.DateTime`: ``"rfc"`` (for RFC822), ``"iso"`` (for ISO8601), ``"timestamp"``, ``"timestamp_ms"`` (for a POSIX timestamp). If you are using a custom DateTime format you should pass a regex string to the ``pattern`` parameter in your field ``metadata`` so that it is included as documentation. .. code-block:: python class SchemaWithCustomDate(Schema): french_date = ma.DateTime( format="%d-%m%Y %H:%M:%S", metadata={"pattern": r"^\d{2}-\d{2}-\d{4} \d{2}:\d{2}:\d{2}$"}, ) Custom Fields ************* apispec maps standard marshmallow fields to OpenAPI types and formats. If your custom field subclasses a standard marshmallow `Field` class then it will inherit the default mapping. If you want to override the OpenAPI type and format for custom fields, use the `map_to_openapi_type ` method. It can be invoked with either a pair of strings providing the OpenAPI type and format, or a marshmallow `Field` that has the desired target mapping. .. code-block:: python from apispec import APISpec from apispec.ext.marshmallow import MarshmallowPlugin from marshmallow.fields import Integer, Field ma_plugin = MarshmallowPlugin() spec = APISpec( title="Demo", version="0.1", openapi_version="3.0.0", plugins=(ma_plugin,) ) # Inherits Integer mapping of ('integer', None) class CustomInteger(Integer): pass # Override Integer mapping class Int32(Integer): pass ma_plugin.map_to_openapi_type(Int32, "string", "int32") # Map to ('integer', None) like Integer class IntegerLike(Field): pass ma_plugin.map_to_openapi_type(IntegerLike, Integer) In situations where greater control of the properties generated for a custom field is desired, users may add custom logic to the conversion of fields to OpenAPI properties through the use of the `add_attribute_function ` method. Continuing from the example above: .. code-block:: python def my_custom_field2properties(self, field, **kwargs): """Add an OpenAPI extension flag to MyCustomField instances""" ret = {} if isinstance(field, MyCustomField): if self.openapi_version.major > 2: ret["x-customString"] = True return ret ma_plugin.converter.add_attribute_function(my_custom_field2properties) The function passed to `add_attribute_function` will be bound to the converter. It must accept the converter instance as first positional argument. In some rare cases, typically with container fields such as fields derived from :class:`List `, documenting the parameters using this field require some more customization. This can be achieved using the `add_parameter_attribute_function ` method. For instance, when documenting webargs's :class:`DelimitedList ` field, one may register this function: .. code-block:: python def delimited_list2param(self, field, **kwargs): ret: dict = {} if isinstance(field, DelimitedList): if self.openapi_version.major < 3: ret["collectionFormat"] = "csv" else: ret["explode"] = False ret["style"] = "form" return ret ma_plugin.converter.add_parameter_attribute_function(delimited_list2param) Enum Fields *********** When using `marshmallow.fields.Enum` fields to (de)serialize `enum.Enum` values, we recommend passing a marshmallow field to ``by_value``. This ensures the correct ``type`` property is included in the generated OAI spec. .. code-block:: python :emphasize-lines: 23,42 from enum import Enum from apispec import APISpec from apispec.ext.marshmallow import MarshmallowPlugin from marshmallow import Schema, fields spec = APISpec( title="Gisty", version="1.0.0", openapi_version="3.0.2", info=dict(description="A minimal gist API"), plugins=[MarshmallowPlugin()], ) class GistVisibility(Enum): PRIVATE = "private" PUBLIC = "public" class GistSchema(Schema): id = fields.Int() visibility = fields.Enum(GistVisibility, by_value=fields.String()) spec.components.schema("Gist", schema=GistSchema) print(spec.to_yaml()) # info: # description: A minimal gist API # title: Gisty # version: 1.0.0 # paths: {} # openapi: 3.0.2 # components: # schemas: # Gist: # type: object # properties: # id: # type: integer # visibility: # type: string # enum: # - private # - public Next Steps ---------- You now know how to use plugins. The next section will show you how to write plugins: :doc:`Writing Plugins `. apispec-6.8.1/docs/writing_plugins.rst000066400000000000000000000117211473713346600201000ustar00rootroot00000000000000Writing Plugins =============== A plugins is a subclass of `apispec.plugin.BasePlugin`. Helper Methods -------------- Plugins provide "helper" methods that augment the behavior of `apispec.APISpec` methods. There are five types of helper methods: * Schema helpers * Parameter helpers * Response helpers * Path helpers * Operation helpers Helper functions modify `apispec.APISpec` methods. For example, path helpers modify `apispec.APISpec.path`. A plugin with a path helper function may look something like this: .. code-block:: python from apispec import BasePlugin from apispec.yaml_utils import load_operations_from_docstring class MyPlugin(BasePlugin): def path_helper(self, path, operations, func, **kwargs): """Path helper that parses docstrings for operations. Adds a ``func`` parameter to `apispec.APISpec.path`. """ operations.update(load_operations_from_docstring(func.__doc__)) All plugin helpers must accept extra `**kwargs`, allowing custom plugins to define new arguments if required. A plugin with an operation helper that adds `deprecated` flag may look like this .. code-block:: python # deprecated_plugin.py from apispec import BasePlugin from apispec.yaml_utils import load_operations_from_docstring class DeprecatedPlugin(BasePlugin): def operation_helper(self, path, operations, **kwargs): """Operation helper that add `deprecated` flag if in `kwargs`""" if kwargs.pop("deprecated", False) is True: for key, value in operations.items(): value["deprecated"] = True Using this plugin .. code-block:: python import json from apispec import APISpec from deprecated_plugin import DeprecatedPlugin spec = APISpec( title="Gisty", version="1.0.0", openapi_version="3.0.2", plugins=[DeprecatedPlugin()], ) # path will call operation_helper on operations spec.path( path="/gists/{gist_id}", operations={"get": {"responses": {"200": {"description": "standard response"}}}}, deprecated=True, ) print(json.dumps(spec.to_dict()["paths"])) # {"/gists/{gist_id}": {"get": {"responses": {"200": {"description": "standard response"}}, "deprecated": true}}} The ``init_spec`` Method ------------------------ `BasePlugin` has an `init_spec` method that `APISpec` calls on each plugin at initialization with the spec object itself as parameter. It is no-op by default, but a plugin may override it to access and store useful information on the spec object. A typical use case is conditional code depending on the OpenAPI version, which is stored as ``openapi_version`` on the `spec` object. See source code for `apispec.ext.marshmallow.MarshmallowPlugin `_ for an example. Example: Docstring-parsing Plugin --------------------------------- Here's a plugin example involving conditional processing depending on the OpenAPI version: .. code-block:: python # docplugin.py from apispec import BasePlugin from apispec.yaml_utils import load_operations_from_docstring class DocPlugin(BasePlugin): def init_spec(self, spec): super(DocPlugin, self).init_spec(spec) self.openapi_major_version = spec.openapi_version.major def operation_helper(self, operations, func, **kwargs): """Operation helper that parses docstrings for operations. Adds a ``func`` parameter to `apispec.APISpec.path`. """ doc_operations = load_operations_from_docstring(func.__doc__) # Apply conditional processing if self.openapi_major_version < 3: "...Mutating doc_operations for OpenAPI v2..." else: "...Mutating doc_operations for OpenAPI v3+..." operations.update(doc_operations) To use the plugin: .. code-block:: python from apispec import APISpec from docplugin import DocPlugin spec = APISpec( title="Gisty", version="1.0.0", openapi_version="3.0.2", plugins=[DocPlugin()] ) def gist_detail(gist_id): """Gist detail view. --- get: responses: 200: content: application/json: schema: '#/definitions/Gist' """ pass spec.path(path="/gists/{gist_id}", func=gist_detail) print(dict(spec.to_dict()["paths"])) # {'/gists/{gist_id}': {'get': {'responses': {200: {'content': {'application/json': {'schema': '#/definitions/Gist'}}}}}}} Next Steps ---------- To learn more about how to write plugins: * Consult the :doc:`Core API docs ` for `BasePlugin ` * View the source for an existing apispec plugin, e.g. `FlaskPlugin `_. * Check out some projects using apispec: https://github.com/marshmallow-code/apispec/wiki/Ecosystem apispec-6.8.1/pyproject.toml000066400000000000000000000046701473713346600161130ustar00rootroot00000000000000[project] name = "apispec" version = "6.8.1" description = "A pluggable API specification generator. Currently supports the OpenAPI Specification (f.k.a. the Swagger specification)." readme = "README.rst" license = { file = "LICENSE" } authors = [{ name = "Steven Loria", email = "sloria1@gmail.com" }] maintainers = [ { name = "Steven Loria", email = "sloria1@gmail.com" }, { name = "Jérôme Lafréchoux", email = "jerome@jolimont.fr" }, ] classifiers = [ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", ] keywords = [ "apispec", "swagger", "openapi", "specification", "oas", "documentation", "spec", "rest", "api", ] requires-python = ">=3.9" dependencies = ["packaging>=21.3"] [project.urls] Changelog = "https://apispec.readthedocs.io/en/latest/changelog.html" Funding = "https://opencollective.com/marshmallow" Issues = "https://github.com/marshmallow-code/apispec/issues" Source = "https://github.com/marshmallow-code/apispec" Tidelift = "https://tidelift.com/subscription/pkg/pypi-apispec?utm_source=pypi-apispec&utm_medium=pypi" [project.optional-dependencies] yaml = ["PyYAML>=3.10"] marshmallow = ["marshmallow>=3.18.0"] docs = [ "apispec[marshmallow]", "pyyaml==6.0.2", "sphinx==8.1.3", "sphinx-issues==5.0.0", "sphinx-rtd-theme==3.0.2", ] tests = ["apispec[yaml,marshmallow]", "openapi-spec-validator==0.7.1", "pytest"] dev = ["apispec[tests]", "tox", "pre-commit>=3.5,<5.0"] [build-system] requires = ["flit_core<4"] build-backend = "flit_core.buildapi" [tool.flit.sdist] include = [ "docs/", "tests/", "CHANGELOG.rst", "CONTRIBUTING.rst", "SECURITY.md", "tox.ini", ] exclude = ["docs/_build/"] [tool.ruff] src = ["src"] fix = true show-fixes = true output-format = "full" [tool.ruff.format] docstring-code-format = true [tool.ruff.lint] ignore = ["E203", "E266", "E501", "E731"] select = [ "B", # flake8-bugbear "E", # pycodestyle error "F", # pyflakes "I", # isort "UP", # pyupgrade "W", # pycodestyle warning ] [tool.mypy] ignore_missing_imports = true warn_unreachable = true warn_unused_ignores = true warn_redundant_casts = true no_implicit_optional = true apispec-6.8.1/readthedocs.yml000066400000000000000000000003241473713346600161770ustar00rootroot00000000000000version: 2 sphinx: configuration: docs/conf.py formats: - pdf build: os: ubuntu-22.04 tools: python: "3.11" python: install: - method: pip path: . extra_requirements: - docs apispec-6.8.1/src/000077500000000000000000000000001473713346600137575ustar00rootroot00000000000000apispec-6.8.1/src/apispec/000077500000000000000000000000001473713346600154035ustar00rootroot00000000000000apispec-6.8.1/src/apispec/__init__.py000066400000000000000000000012621473713346600175150ustar00rootroot00000000000000"""Contains main apispec classes: `APISpec` and `BasePlugin`""" import typing from .core import APISpec from .plugin import BasePlugin __all__ = ["APISpec", "BasePlugin"] def __getattr__(name: str) -> typing.Any: if name == "__version__": import importlib.metadata import warnings warnings.warn( "The '__version__' attribute is deprecated and will be removed in" " in a future version. Use feature detection or" " 'importlib.metadata.version(\"apispec\")' instead.", DeprecationWarning, stacklevel=2, ) return importlib.metadata.version("apispec") raise AttributeError(name) apispec-6.8.1/src/apispec/core.py000066400000000000000000000567021473713346600167170ustar00rootroot00000000000000"""Core apispec classes and functions.""" from __future__ import annotations import typing import warnings from collections.abc import Sequence from copy import deepcopy from packaging.version import Version from .exceptions import ( APISpecError, DuplicateComponentNameError, DuplicateParameterError, InvalidParameterError, PluginMethodNotImplementedError, ) from .utils import COMPONENT_SUBSECTIONS, build_reference, deepupdate if typing.TYPE_CHECKING: from .plugin import BasePlugin VALID_METHODS_OPENAPI_V2 = ["get", "post", "put", "patch", "delete", "head", "options"] VALID_METHODS_OPENAPI_V3 = VALID_METHODS_OPENAPI_V2 + ["trace"] VALID_METHODS = {2: VALID_METHODS_OPENAPI_V2, 3: VALID_METHODS_OPENAPI_V3} MIN_INCLUSIVE_OPENAPI_VERSION = Version("2.0") MAX_EXCLUSIVE_OPENAPI_VERSION = Version("4.0") class Components: """Stores OpenAPI components Components are top-level fields in OAS v2. They became sub-fields of "components" top-level field in OAS v3. """ def __init__( self, plugins: Sequence[BasePlugin], openapi_version: Version, ) -> None: self._plugins = plugins self.openapi_version = openapi_version self.schemas: dict[str, dict] = {} self.responses: dict[str, dict] = {} self.parameters: dict[str, dict] = {} self.headers: dict[str, dict] = {} self.examples: dict[str, dict] = {} self.security_schemes: dict[str, dict] = {} self.schemas_lazy: dict[str, dict] = {} self.responses_lazy: dict[str, dict] = {} self.parameters_lazy: dict[str, dict] = {} self.headers_lazy: dict[str, dict] = {} self.examples_lazy: dict[str, dict] = {} self._subsections = { "schema": self.schemas, "response": self.responses, "parameter": self.parameters, "header": self.headers, "example": self.examples, "security_scheme": self.security_schemes, } self._subsections_lazy = { "schema": self.schemas_lazy, "response": self.responses_lazy, "parameter": self.parameters_lazy, "header": self.headers_lazy, "example": self.examples_lazy, } def to_dict(self) -> dict[str, dict]: return { COMPONENT_SUBSECTIONS[self.openapi_version.major][k]: v for k, v in self._subsections.items() if v != {} } def _register_component( self, obj_type: str, component_id: str, component: dict, *, lazy: bool = False, ) -> None: subsection = (self._subsections if lazy is False else self._subsections_lazy)[ obj_type ] subsection[component_id] = component def _do_register_lazy_component( self, obj_type: str, component_id: str, ) -> None: component_buffer = self._subsections_lazy[obj_type] # If component was lazy registered, register it for real if component_id in component_buffer: self._subsections[obj_type][component_id] = component_buffer.pop( component_id ) def get_ref( self, obj_type: str, obj_or_component_id: dict | str, ) -> dict: """Return object or reference If obj is a dict, it is assumed to be a complete description and it is returned as is. Otherwise, it is assumed to be a reference name as string and the corresponding $ref string is returned. :param str subsection: "schema", "parameter", "response" or "security_scheme" :param dict|str obj: object in dict form or as ref_id string """ if isinstance(obj_or_component_id, dict): return obj_or_component_id # Register the component if it was lazy registered self._do_register_lazy_component(obj_type, obj_or_component_id) return build_reference( obj_type, self.openapi_version.major, obj_or_component_id ) def schema( self, component_id: str, component: dict | None = None, *, lazy: bool = False, **kwargs: typing.Any, ) -> Components: """Add a new schema to the spec. :param str component_id: identifier by which schema may be referenced :param dict component: schema definition :param bool lazy: register component only when referenced in the spec :param kwargs: plugin-specific arguments .. note:: If you are using `apispec.ext.marshmallow`, you can pass fields' metadata as additional keyword arguments. For example, to add ``enum`` and ``description`` to your field: :: status = fields.String( required=True, metadata={ "description": "Status (open or closed)", "enum": ["open", "closed"], }, ) https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#schemaObject """ if component_id in self.schemas: raise DuplicateComponentNameError( f'Another schema with name "{component_id}" is already registered.' ) ret = deepcopy(component) or {} # Execute all helpers from plugins for plugin in self._plugins: try: ret.update(plugin.schema_helper(component_id, ret, **kwargs) or {}) except PluginMethodNotImplementedError: continue self._resolve_refs_in_schema(ret) self._register_component("schema", component_id, ret, lazy=lazy) return self def response( self, component_id: str, component: dict | None = None, *, lazy: bool = False, **kwargs: typing.Any, ) -> Components: """Add a response which can be referenced. :param str component_id: ref_id to use as reference :param dict component: response fields :param bool lazy: register component only when referenced in the spec :param kwargs: plugin-specific arguments """ if component_id in self.responses: raise DuplicateComponentNameError( f'Another response with name "{component_id}" is already registered.' ) ret = deepcopy(component) or {} # Execute all helpers from plugins for plugin in self._plugins: try: ret.update(plugin.response_helper(ret, **kwargs) or {}) except PluginMethodNotImplementedError: continue self._resolve_refs_in_response(ret) self._register_component("response", component_id, ret, lazy=lazy) return self def parameter( self, component_id: str, location: str, component: dict | None = None, *, lazy: bool = False, **kwargs: typing.Any, ) -> Components: """Add a parameter which can be referenced. :param str component_id: identifier by which parameter may be referenced :param str location: location of the parameter :param dict component: parameter fields :param bool lazy: register component only when referenced in the spec :param kwargs: plugin-specific arguments """ if component_id in self.parameters: raise DuplicateComponentNameError( f'Another parameter with name "{component_id}" is already registered.' ) ret = deepcopy(component) or {} ret.setdefault("name", component_id) ret["in"] = location # if "in" is set to "path", enforce required flag to True if location == "path": ret["required"] = True # Execute all helpers from plugins for plugin in self._plugins: try: ret.update(plugin.parameter_helper(ret, **kwargs) or {}) except PluginMethodNotImplementedError: continue self._resolve_refs_in_parameter_or_header(ret) self._register_component("parameter", component_id, ret, lazy=lazy) return self def header( self, component_id: str, component: dict, *, lazy: bool = False, **kwargs: typing.Any, ) -> Components: """Add a header which can be referenced. :param str component_id: identifier by which header may be referenced :param dict component: header fields :param bool lazy: register component only when referenced in the spec :param kwargs: plugin-specific arguments https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.1.md#headerObject """ ret = deepcopy(component) or {} if component_id in self.headers: raise DuplicateComponentNameError( f'Another header with name "{component_id}" is already registered.' ) # Execute all helpers from plugins for plugin in self._plugins: try: ret.update(plugin.header_helper(ret, **kwargs) or {}) except PluginMethodNotImplementedError: continue self._resolve_refs_in_parameter_or_header(ret) self._register_component("header", component_id, ret, lazy=lazy) return self def example( self, component_id: str, component: dict, *, lazy: bool = False ) -> Components: """Add an example which can be referenced :param str component_id: identifier by which example may be referenced :param dict component: example fields :param bool lazy: register component only when referenced in the spec https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.1.md#exampleObject """ if component_id in self.examples: raise DuplicateComponentNameError( f'Another example with name "{component_id}" is already registered.' ) self._register_component("example", component_id, component, lazy=lazy) return self def security_scheme(self, component_id: str, component: dict) -> Components: """Add a security scheme which can be referenced. :param str component_id: component_id to use as reference :param dict component: security scheme fields """ if component_id in self.security_schemes: raise DuplicateComponentNameError( f'Another security scheme with name "{component_id}" is already registered.' ) self._register_component("security_scheme", component_id, component) return self def _resolve_schema(self, obj) -> None: """Replace schema reference as string with a $ref if needed Also resolve references in the schema """ if "schema" in obj: obj["schema"] = self.get_ref("schema", obj["schema"]) self._resolve_refs_in_schema(obj["schema"]) def _resolve_examples(self, obj) -> None: """Replace example reference as string with a $ref""" for name, example in obj.get("examples", {}).items(): obj["examples"][name] = self.get_ref("example", example) def _resolve_refs_in_schema(self, schema: dict) -> None: if "properties" in schema: for key in schema["properties"]: schema["properties"][key] = self.get_ref( "schema", schema["properties"][key] ) self._resolve_refs_in_schema(schema["properties"][key]) if "items" in schema: schema["items"] = self.get_ref("schema", schema["items"]) self._resolve_refs_in_schema(schema["items"]) for key in ("allOf", "oneOf", "anyOf"): if key in schema: schema[key] = [self.get_ref("schema", s) for s in schema[key]] for sch in schema[key]: self._resolve_refs_in_schema(sch) if "not" in schema: schema["not"] = self.get_ref("schema", schema["not"]) self._resolve_refs_in_schema(schema["not"]) def _resolve_refs_in_parameter_or_header(self, parameter_or_header) -> None: self._resolve_schema(parameter_or_header) self._resolve_examples(parameter_or_header) # parameter content is OpenAPI v3+ for media_type in parameter_or_header.get("content", {}).values(): self._resolve_schema(media_type) def _resolve_refs_in_request_body(self, request_body) -> None: # requestBody is OpenAPI v3+ for media_type in request_body["content"].values(): self._resolve_schema(media_type) self._resolve_examples(media_type) def _resolve_refs_in_response(self, response) -> None: if self.openapi_version.major < 3: self._resolve_schema(response) else: for media_type in response.get("content", {}).values(): self._resolve_schema(media_type) self._resolve_examples(media_type) for name, header in response.get("headers", {}).items(): response["headers"][name] = self.get_ref("header", header) self._resolve_refs_in_parameter_or_header(response["headers"][name]) # TODO: Resolve link refs when Components supports links def _resolve_refs_in_operation(self, operation) -> None: if "parameters" in operation: parameters = [] for parameter in operation["parameters"]: parameter = self.get_ref("parameter", parameter) self._resolve_refs_in_parameter_or_header(parameter) parameters.append(parameter) operation["parameters"] = parameters if "callbacks" in operation: for callback in operation["callbacks"].values(): if isinstance(callback, dict): for path in callback.values(): self.resolve_refs_in_path(path) if "requestBody" in operation: self._resolve_refs_in_request_body(operation["requestBody"]) if "responses" in operation: responses = {} for code, response in operation["responses"].items(): response = self.get_ref("response", response) self._resolve_refs_in_response(response) responses[code] = response operation["responses"] = responses def resolve_refs_in_path(self, path) -> None: if "parameters" in path: parameters = [] for parameter in path["parameters"]: parameter = self.get_ref("parameter", parameter) self._resolve_refs_in_parameter_or_header(parameter) parameters.append(parameter) path["parameters"] = parameters for method in ( "get", "put", "post", "delete", "options", "head", "patch", "trace", ): if method in path: self._resolve_refs_in_operation(path[method]) class APISpec: """Stores metadata that describes a RESTful API using the OpenAPI specification. :param str title: API title :param str version: API version :param list|tuple plugins: Plugin instances. See https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#infoObject :param str openapi_version: OpenAPI Specification version. Should be in the form '2.x' or '3.x.x' to comply with the OpenAPI standard. :param options: Optional top-level keys See https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#openapi-object """ def __init__( self, title: str, version: str, openapi_version: str, plugins: Sequence[BasePlugin] = (), **options: typing.Any, ) -> None: self.title = title self.version = version self.options = options self.plugins = plugins self.openapi_version = Version(openapi_version) if not ( MIN_INCLUSIVE_OPENAPI_VERSION <= self.openapi_version < MAX_EXCLUSIVE_OPENAPI_VERSION ): raise APISpecError(f"Not a valid OpenAPI version number: {openapi_version}") # Metadata self._tags: list[dict] = [] self._paths: dict = {} # Components self.components = Components(self.plugins, self.openapi_version) # Plugins for plugin in self.plugins: plugin.init_spec(self) def to_dict(self) -> dict[str, typing.Any]: ret: dict[str, typing.Any] = { "paths": self._paths, "info": {"title": self.title, "version": self.version}, } if self._tags: ret["tags"] = self._tags if self.openapi_version.major < 3: ret["swagger"] = str(self.openapi_version) ret.update(self.components.to_dict()) else: ret["openapi"] = str(self.openapi_version) components_dict = self.components.to_dict() if components_dict: ret["components"] = components_dict ret = deepupdate(ret, self.options) return ret def to_yaml(self, yaml_dump_kwargs: typing.Any | None = None) -> str: """Render the spec to YAML. Requires PyYAML to be installed. :param dict yaml_dump_kwargs: Additional keyword arguments to pass to `yaml.dump` """ from .yaml_utils import dict_to_yaml return dict_to_yaml(self.to_dict(), yaml_dump_kwargs) def tag(self, tag: dict) -> APISpec: """Store information about a tag. :param dict tag: the dictionary storing information about the tag. """ self._tags.append(tag) return self def path( self, path: str | None = None, *, operations: dict[str, typing.Any] | None = None, summary: str | None = None, description: str | None = None, parameters: list[dict] | None = None, **kwargs: typing.Any, ) -> APISpec: """Add a new path object to the spec. https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#path-item-object :param str|None path: URL path component :param dict|None operations: describes the http methods and options for `path` :param str summary: short summary relevant to all operations in this path :param str description: long description relevant to all operations in this path :param list|None parameters: list of parameters relevant to all operations in this path :param kwargs: parameters used by any path helpers see :meth:`register_path_helper` """ # operations and parameters must be deepcopied because they are mutated # in _clean_operations and operation helpers and path may be called twice operations = deepcopy(operations) or {} parameters = deepcopy(parameters) or [] # Execute path helpers for plugin in self.plugins: try: ret = plugin.path_helper( path=path, operations=operations, parameters=parameters, **kwargs ) except PluginMethodNotImplementedError: continue if ret is not None: path = ret if not path: raise APISpecError("Path template is not specified.") # Execute operation helpers for plugin in self.plugins: try: plugin.operation_helper(path=path, operations=operations, **kwargs) except PluginMethodNotImplementedError: continue self._clean_operations(operations) self._paths.setdefault(path, operations).update(operations) if summary is not None: self._paths[path]["summary"] = summary if description is not None: self._paths[path]["description"] = description if parameters: parameters = self._clean_parameters(parameters) self._paths[path]["parameters"] = parameters self.components.resolve_refs_in_path(self._paths[path]) return self def _clean_parameters( self, parameters: list[dict], ) -> list[dict]: """Ensure that all parameters with "in" equal to "path" are also required as required by the OpenAPI specification, as well as normalizing any references to global parameters and checking for duplicates parameters See https ://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#parameterObject. :param list parameters: List of parameters mapping """ seen = set() for parameter in [p for p in parameters if isinstance(p, dict)]: # check missing name / location missing_attrs = [attr for attr in ("name", "in") if attr not in parameter] if missing_attrs: raise InvalidParameterError( f"Missing keys {missing_attrs} for parameter" ) # OpenAPI Spec 3 and 2 don't allow for duplicated parameters # A unique parameter is defined by a combination of a name and location unique_key = (parameter["name"], parameter["in"]) if unique_key in seen: raise DuplicateParameterError( "Duplicate parameter with name {} and location {}".format( parameter["name"], parameter["in"] ) ) seen.add(unique_key) # Add "required" attribute to path parameters if parameter["in"] == "path": parameter["required"] = True return parameters def _clean_operations( self, operations: dict[str, dict], ) -> None: """Ensure that all parameters with "in" equal to "path" are also required as required by the OpenAPI specification, as well as normalizing any references to global parameters. Also checks for invalid HTTP methods. See https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#parameterObject. :param dict operations: Dict mapping status codes to operations """ operation_names = set(operations) valid_methods = set(VALID_METHODS[self.openapi_version.major]) invalid = { key for key in operation_names - valid_methods if not key.startswith("x-") } if invalid: raise APISpecError( "One or more HTTP methods are invalid: {}".format(", ".join(invalid)) ) for operation in (operations or {}).values(): if "parameters" in operation: operation["parameters"] = self._clean_parameters( operation["parameters"] ) if "responses" in operation: responses = {} for code, response in operation["responses"].items(): try: code = int(code) # handles IntEnums like http.HTTPStatus except (TypeError, ValueError): if self.openapi_version.major < 3 and code != "default": warnings.warn( "Non-integer code not allowed in OpenAPI < 3", UserWarning, stacklevel=2, ) responses[str(code)] = response operation["responses"] = responses apispec-6.8.1/src/apispec/exceptions.py000066400000000000000000000013011473713346600201310ustar00rootroot00000000000000"""Exception classes.""" class APISpecError(Exception): """Base class for all apispec-related errors.""" class PluginMethodNotImplementedError(APISpecError, NotImplementedError): """Raised when calling an unimplemented helper method in a plugin""" class DuplicateComponentNameError(APISpecError): """Raised when registering two components with the same name""" class DuplicateParameterError(APISpecError): """Raised when registering a parameter already existing in a given scope""" class InvalidParameterError(APISpecError): """Raised when parameter doesn't contains required keys""" class OpenAPIError(APISpecError): """Raised when a OpenAPI spec validation fails.""" apispec-6.8.1/src/apispec/ext/000077500000000000000000000000001473713346600162035ustar00rootroot00000000000000apispec-6.8.1/src/apispec/ext/__init__.py000066400000000000000000000000001473713346600203020ustar00rootroot00000000000000apispec-6.8.1/src/apispec/ext/marshmallow/000077500000000000000000000000001473713346600205315ustar00rootroot00000000000000apispec-6.8.1/src/apispec/ext/marshmallow/__init__.py000066400000000000000000000214451473713346600226500ustar00rootroot00000000000000"""marshmallow plugin for apispec. Allows passing a marshmallow `Schema` to `spec.components.schema `, `spec.components.parameter `, `spec.components.response ` (for response and headers schemas) and `spec.path ` (for responses and response headers). Requires marshmallow>=3.13.0. ``MarshmallowPlugin`` maps marshmallow ``Field`` classes with OpenAPI types and formats. It inspects field attributes to automatically document properties such as read/write-only, range and length constraints, etc. OpenAPI properties can also be passed as metadata to the ``Field`` instance if they can't be inferred from the field attributes (`description`,...), or to override automatic documentation (`readOnly`,...). A metadata attribute is used in the documentation either if it is a valid OpenAPI property, or if it starts with `"x-"` (vendor extension). .. warning:: ``MarshmallowPlugin`` infers the ``default`` property from the ``load_default`` attribute of the ``Field`` (unless ``load_default`` is a callable). Since default values are entered in deserialized form, the value displayed in the doc is serialized by the ``Field`` instance. This may lead to inaccurate documentation in very specific cases. The default value to display in the documentation can be specified explicitly by passing ``default`` as field metadata. :: from pprint import pprint import datetime as dt from apispec import APISpec from apispec.ext.marshmallow import MarshmallowPlugin from marshmallow import Schema, fields spec = APISpec( title="Example App", version="1.0.0", openapi_version="3.0.2", plugins=[MarshmallowPlugin()], ) class UserSchema(Schema): id = fields.Int(dump_only=True) name = fields.Str(metadata={"description": "The user's name"}) created = fields.DateTime( dump_only=True, dump_default=dt.datetime.utcnow, metadata={"default": "The current datetime"}, ) spec.components.schema("User", schema=UserSchema) pprint(spec.to_dict()["components"]["schemas"]) # {'User': {'properties': {'created': {'default': 'The current datetime', # 'format': 'date-time', # 'readOnly': True, # 'type': 'string'}, # 'id': {'readOnly': True, # 'type': 'integer'}, # 'name': {'description': "The user's name", # 'type': 'string'}}, # 'type': 'object'}} """ # pyright: reportIncompatibleMethodOverride=false from __future__ import annotations import typing import warnings from marshmallow import Schema from packaging.version import Version from apispec import APISpec, BasePlugin from .common import make_schema_key, resolve_schema_cls, resolve_schema_instance from .openapi import OpenAPIConverter from .schema_resolver import SchemaResolver def resolver(schema: type[Schema]) -> str: """Default schema name resolver function that strips 'Schema' from the end of the class name.""" resolved = resolve_schema_cls(schema) schema_cls = resolved[0] if isinstance(resolved, list) else resolved name = schema_cls.__name__ if name.endswith("Schema"): name = name[:-6] or name return name.strip() class MarshmallowPlugin(BasePlugin): """APISpec plugin for translating marshmallow schemas to OpenAPI/JSONSchema format. :param callable schema_name_resolver: Callable to generate the schema definition name. Receives the `Schema` class and returns the name to be used in refs within the generated spec. When working with circular referencing this function must must not return `None` for schemas in a circular reference chain. Example: :: from apispec.ext.marshmallow.common import resolve_schema_cls def schema_name_resolver(schema): schema_cls = resolve_schema_cls(schema) return schema_cls.__name__ """ Converter = OpenAPIConverter Resolver = SchemaResolver def __init__( self, schema_name_resolver: typing.Callable[[type[Schema]], str] | None = None, ) -> None: super().__init__() self.schema_name_resolver = schema_name_resolver or resolver self.spec: APISpec | None = None self.openapi_version: Version | None = None self.converter: OpenAPIConverter | None = None self.resolver: SchemaResolver | None = None def init_spec(self, spec: APISpec) -> None: super().init_spec(spec) self.spec = spec self.openapi_version = spec.openapi_version self.converter = self.Converter( openapi_version=spec.openapi_version, schema_name_resolver=self.schema_name_resolver, spec=spec, ) self.resolver = self.Resolver( openapi_version=spec.openapi_version, converter=self.converter ) def map_to_openapi_type(self, field_cls, *args): """Set mapping for custom field class. :param type field_cls: Field class to set mapping for. ``*args`` can be: - a pair of the form ``(type, format)`` - a core marshmallow field type (in which case we reuse that type's mapping) Examples: :: # Override Integer mapping class Int32(Integer): # ... ma_plugin.map_to_openapi_type(Int32, 'string', 'int32') # Map to ('integer', None) like Integer class IntegerLike(Integer): # ... ma_plugin.map_to_openapi_type(IntegerLike, Integer) """ assert self.converter is not None, "init_spec has not yet been called" return self.converter.map_to_openapi_type(field_cls, *args) def schema_helper(self, name, _, schema=None, **kwargs): """Definition helper that allows using a marshmallow :class:`Schema ` to provide OpenAPI metadata. :param type|Schema schema: A marshmallow Schema class or instance. """ if schema is None: return None schema_instance = resolve_schema_instance(schema) schema_key = make_schema_key(schema_instance) self.warn_if_schema_already_in_spec(schema_key) assert self.converter is not None, "init_spec has not yet been called" self.converter.refs[schema_key] = name json_schema = self.converter.schema2jsonschema(schema_instance) return json_schema def parameter_helper(self, parameter, **kwargs): """Parameter component helper that allows using a marshmallow :class:`Schema ` in parameter definition. :param dict parameter: parameter fields. May contain a marshmallow Schema class or instance. """ assert self.resolver is not None, "init_spec has not yet been called" self.resolver.resolve_schema(parameter) return parameter def response_helper(self, response, **kwargs): """Response component helper that allows using a marshmallow :class:`Schema ` in response definition. :param dict parameter: response fields. May contain a marshmallow Schema class or instance. """ assert self.resolver is not None, "init_spec has not yet been called" self.resolver.resolve_response(response) return response def header_helper(self, header: dict, **kwargs: typing.Any): """Header component helper that allows using a marshmallow :class:`Schema ` in header definition. :param dict header: header fields. May contain a marshmallow Schema class or instance. """ assert self.resolver # needed for mypy self.resolver.resolve_schema(header) return header def operation_helper( self, path: str | None = None, operations: dict | None = None, **kwargs: typing.Any, ) -> None: assert self.resolver # needed for mypy self.resolver.resolve_operations(operations) def warn_if_schema_already_in_spec(self, schema_key: tuple) -> None: """Method to warn the user if the schema has already been added to the spec. """ assert self.converter # needed for mypy if schema_key in self.converter.refs: warnings.warn( f"{schema_key[0]} has already been added to the spec. Adding it twice may " "cause references to not resolve properly.", UserWarning, stacklevel=2, ) apispec-6.8.1/src/apispec/ext/marshmallow/common.py000066400000000000000000000130171473713346600223750ustar00rootroot00000000000000"""Utilities to get schema instances/classes""" from __future__ import annotations import copy import warnings import marshmallow import marshmallow.class_registry from marshmallow import fields from apispec.core import Components MODIFIERS = ["only", "exclude", "load_only", "dump_only", "partial"] def resolve_schema_instance( schema: type[marshmallow.Schema] | marshmallow.Schema | str, ) -> marshmallow.Schema: """Return schema instance for given schema (instance or class). :param type|Schema|str schema: instance, class or class name of marshmallow.Schema :return: schema instance of given schema (instance or class) """ if isinstance(schema, type) and issubclass(schema, marshmallow.Schema): return schema() if isinstance(schema, marshmallow.Schema): return schema return marshmallow.class_registry.get_class(schema)() def resolve_schema_cls( schema: type[marshmallow.Schema] | str | marshmallow.Schema, ) -> type[marshmallow.Schema] | list[type[marshmallow.Schema]]: """Return schema class for given schema (instance or class). :param type|Schema|str: instance, class or class name of marshmallow.Schema :return: schema class of given schema (instance or class) """ if isinstance(schema, type) and issubclass(schema, marshmallow.Schema): return schema if isinstance(schema, marshmallow.Schema): return type(schema) return marshmallow.class_registry.get_class(str(schema)) def get_fields( schema: type[marshmallow.Schema] | marshmallow.Schema, *, exclude_dump_only: bool = False, ) -> dict[str, fields.Field]: """Return fields from schema. :param Schema schema: A marshmallow Schema instance or a class object :param bool exclude_dump_only: whether to filter fields in Meta.dump_only :rtype: dict, of field name field object pairs """ if isinstance(schema, marshmallow.Schema): fields = schema.fields elif isinstance(schema, type) and issubclass(schema, marshmallow.Schema): fields = copy.deepcopy(schema._declared_fields) else: raise ValueError(f"{schema!r} is neither a Schema class nor a Schema instance.") Meta = getattr(schema, "Meta", None) warn_if_fields_defined_in_meta(fields, Meta) return filter_excluded_fields(fields, Meta, exclude_dump_only=exclude_dump_only) def warn_if_fields_defined_in_meta(fields: dict[str, fields.Field], Meta): """Warns user that fields defined in Meta.fields or Meta.additional will be ignored. :param dict fields: A dictionary of fields name field object pairs :param Meta: the schema's Meta class """ if getattr(Meta, "fields", None) or getattr(Meta, "additional", None): declared_fields = set(fields.keys()) if ( set(getattr(Meta, "fields", set())) > declared_fields or set(getattr(Meta, "additional", set())) > declared_fields ): warnings.warn( "Only explicitly-declared fields will be included in the Schema Object. " "Fields defined in Meta.fields or Meta.additional are ignored.", UserWarning, stacklevel=2, ) def filter_excluded_fields( fields: dict[str, fields.Field], Meta, *, exclude_dump_only: bool ) -> dict[str, fields.Field]: """Filter fields that should be ignored in the OpenAPI spec. :param dict fields: A dictionary of fields name field object pairs :param Meta: the schema's Meta class :param bool exclude_dump_only: whether to filter dump_only fields """ exclude = list(getattr(Meta, "exclude", [])) if exclude_dump_only: exclude.extend(getattr(Meta, "dump_only", [])) filtered_fields = { key: value for key, value in fields.items() if key not in exclude and not (exclude_dump_only and value.dump_only) } return filtered_fields def make_schema_key(schema: marshmallow.Schema) -> tuple[type[marshmallow.Schema], ...]: if not isinstance(schema, marshmallow.Schema): raise TypeError("can only make a schema key based on a Schema instance.") modifiers = [] for modifier in MODIFIERS: attribute = getattr(schema, modifier) try: # Hashable (string, tuple) hash(attribute) except TypeError: # Unhashable iterable (list, set) attribute = frozenset(attribute) modifiers.append(attribute) return tuple([schema.__class__, *modifiers]) def get_unique_schema_name(components: Components, name: str, counter: int = 0) -> str: """Function to generate a unique name based on the provided name and names already in the spec. Will append a number to the name to make it unique if the name is already in the spec. :param Components components: instance of the components of the spec :param string name: the name to use as a basis for the unique name :param int counter: the counter of the number of recursions :return: the unique name """ if name not in components.schemas: return name if not counter: # first time through recursion warnings.warn( f"Multiple schemas resolved to the name {name}. The name has been modified. " "Either manually add each of the schemas with a different name or " "provide a custom schema_name_resolver.", UserWarning, stacklevel=2, ) else: # subsequent recursions name = name[: -len(str(counter))] counter += 1 return get_unique_schema_name(components, name + str(counter), counter) apispec-6.8.1/src/apispec/ext/marshmallow/field_converter.py000066400000000000000000000556101473713346600242640ustar00rootroot00000000000000"""Utilities for generating OpenAPI Specification (fka Swagger) entities from :class:`Fields `. .. warning:: This module is treated as private API. Users should not need to use this module directly. """ from __future__ import annotations import functools import operator import re import typing import warnings import marshmallow from marshmallow.orderedset import OrderedSet from packaging.version import Version # marshmallow field => (JSON Schema type, format) DEFAULT_FIELD_MAPPING: dict[type, tuple[str | None, str | None]] = { marshmallow.fields.Integer: ("integer", None), marshmallow.fields.Number: ("number", None), marshmallow.fields.Float: ("number", None), marshmallow.fields.Decimal: ("number", None), marshmallow.fields.String: ("string", None), marshmallow.fields.Boolean: ("boolean", None), marshmallow.fields.UUID: ("string", "uuid"), marshmallow.fields.DateTime: ("string", "date-time"), marshmallow.fields.Date: ("string", "date"), marshmallow.fields.Time: ("string", None), marshmallow.fields.TimeDelta: ("integer", None), marshmallow.fields.Email: ("string", "email"), marshmallow.fields.URL: ("string", "url"), marshmallow.fields.Dict: ("object", None), marshmallow.fields.Field: (None, None), marshmallow.fields.Raw: (None, None), marshmallow.fields.List: ("array", None), marshmallow.fields.IP: ("string", "ip"), marshmallow.fields.IPv4: ("string", "ipv4"), marshmallow.fields.IPv6: ("string", "ipv6"), } # Properties that may be defined in a field's metadata that will be added to the output # of field2property # https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#schemaObject _VALID_PROPERTIES = { "format", "title", "description", "default", "multipleOf", "maximum", "exclusiveMaximum", "minimum", "exclusiveMinimum", "maxLength", "minLength", "pattern", "maxItems", "minItems", "uniqueItems", "maxProperties", "minProperties", "required", "enum", "type", "items", "allOf", "oneOf", "anyOf", "not", "properties", "additionalProperties", "readOnly", "writeOnly", "xml", "externalDocs", "example", "nullable", "deprecated", } _VALID_PREFIX = "x-" class FieldConverterMixin: """Adds methods for converting marshmallow fields to an OpenAPI properties.""" field_mapping: dict[type, tuple[str | None, str | None]] = DEFAULT_FIELD_MAPPING openapi_version: Version def init_attribute_functions(self): self.attribute_functions = [ # self.field2type_and_format should run first # as other functions may rely on its output self.field2type_and_format, self.field2default, self.field2choices, self.field2read_only, self.field2write_only, self.field2range, self.field2length, self.field2pattern, self.metadata2properties, self.enum2properties, self.nested2properties, self.pluck2properties, self.list2properties, self.dict2properties, self.timedelta2properties, self.datetime2properties, self.field2nullable, ] def map_to_openapi_type(self, field_cls, *args): """Set mapping for custom field class. :param type field_cls: Field class to set mapping for. ``*args`` can be: - a pair of the form ``(type, format)`` - a core marshmallow field type (in which case we reuse that type's mapping) """ if len(args) == 1 and args[0] in self.field_mapping: openapi_type_field = self.field_mapping[args[0]] elif len(args) == 2: openapi_type_field = args else: raise TypeError("Pass core marshmallow field type or (type, fmt) pair.") self.field_mapping[field_cls] = openapi_type_field def add_attribute_function(self, func): """Method to add an attribute function to the list of attribute functions that will be called on a field to convert it from a field to an OpenAPI property. :param func func: the attribute function to add The attribute function will be bound to the `OpenAPIConverter ` instance. It will be called for each field in a schema with `self ` and a `field ` instance positional arguments and `ret ` keyword argument. Must return a dictionary of OpenAPI properties that will be shallow merged with the return values of all other attribute functions called on the field. User added attribute functions will be called after all built-in attribute functions in the order they were added. The merged results of all previously called attribute functions are accessible via the `ret` argument. """ bound_func = func.__get__(self) setattr(self, func.__name__, bound_func) self.attribute_functions.append(bound_func) def field2property(self, field: marshmallow.fields.Field) -> dict: """Return the JSON Schema property definition given a marshmallow :class:`Field `. Will include field metadata that are valid properties of OpenAPI schema objects (e.g. "description", "enum", "example"). https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#schemaObject :param Field field: A marshmallow field. :rtype: dict, a Property Object """ ret: dict = {} for attr_func in self.attribute_functions: ret.update(attr_func(field, ret=ret)) return ret def field2type_and_format( self, field: marshmallow.fields.Field, **kwargs: typing.Any ) -> dict: """Return the dictionary of OpenAPI type and format based on the field type. :param Field field: A marshmallow field. :rtype: dict """ # If this type isn't directly in the field mapping then check the # hierarchy until we find something that does. for field_class in type(field).__mro__: if field_class in self.field_mapping: type_, fmt = self.field_mapping[field_class] break else: warnings.warn( f"Field of type {type(field)} does not inherit from marshmallow.Field.", UserWarning, stacklevel=2, ) type_, fmt = "string", None ret = {} if type_: ret["type"] = type_ if fmt: ret["format"] = fmt return ret def field2default( self, field: marshmallow.fields.Field, **kwargs: typing.Any ) -> dict: """Return the dictionary containing the field's default value. Will first look for a `default` key in the field's metadata and then fall back on the field's `missing` parameter. A callable passed to the field's missing parameter will be ignored. :param Field field: A marshmallow field. :rtype: dict """ ret = {} if "default" in field.metadata: ret["default"] = field.metadata["default"] else: default = field.load_default if default is not marshmallow.missing and not callable(default): default = field._serialize(default, None, None) ret["default"] = default return ret def field2choices( self, field: marshmallow.fields.Field, **kwargs: typing.Any ) -> dict: """Return the dictionary of OpenAPI field attributes for valid choices definition. :param Field field: A marshmallow field. :rtype: dict """ attributes = {} comparable = [ validator.comparable for validator in field.validators if hasattr(validator, "comparable") ] if comparable: attributes["enum"] = comparable else: choices = [ OrderedSet(validator.choices) for validator in field.validators if hasattr(validator, "choices") ] if choices: attributes["enum"] = list(functools.reduce(operator.and_, choices)) if field.allow_none: enum = attributes.get("enum") if enum is not None and None not in enum: attributes["enum"].append(None) return attributes def field2read_only( self, field: marshmallow.fields.Field, **kwargs: typing.Any ) -> dict: """Return the dictionary of OpenAPI field attributes for a dump_only field. :param Field field: A marshmallow field. :rtype: dict """ attributes = {} if field.dump_only: attributes["readOnly"] = True return attributes def field2write_only( self, field: marshmallow.fields.Field, **kwargs: typing.Any ) -> dict: """Return the dictionary of OpenAPI field attributes for a load_only field. :param Field field: A marshmallow field. :rtype: dict """ attributes = {} if field.load_only and self.openapi_version.major >= 3: attributes["writeOnly"] = True return attributes def field2nullable(self, field: marshmallow.fields.Field, ret) -> dict: """Return the dictionary of OpenAPI field attributes for a nullable field. :param Field field: A marshmallow field. :rtype: dict """ attributes: dict = {} if field.allow_none: if self.openapi_version.major < 3: attributes["x-nullable"] = True elif self.openapi_version.minor < 1: if "$ref" in ret: attributes["anyOf"] = [ {"type": "object", "nullable": True}, {"$ref": ret.pop("$ref")}, ] elif "allOf" in ret: attributes["anyOf"] = [ *ret.pop("allOf"), {"type": "object", "nullable": True}, ] else: attributes["nullable"] = True else: if "$ref" in ret: attributes["anyOf"] = [{"$ref": ret.pop("$ref")}, {"type": "null"}] elif "allOf" in ret: attributes["anyOf"] = [*ret.pop("allOf"), {"type": "null"}] elif "type" in ret: attributes["type"] = [*make_type_list(ret.get("type")), "null"] return attributes def field2range(self, field: marshmallow.fields.Field, ret) -> dict: """Return the dictionary of OpenAPI field attributes for a set of :class:`Range ` validators. :param Field field: A marshmallow field. :rtype: dict """ validators = [ validator for validator in field.validators if ( hasattr(validator, "min") and hasattr(validator, "max") and not hasattr(validator, "equal") ) ] min_attr, max_attr = ( ("minimum", "maximum") if set(make_type_list(ret.get("type"))) & {"number", "integer"} else ("x-minimum", "x-maximum") ) # Serialize min/max values with the field to which the validator is applied return { k: field._serialize(v, None, None) for k, v in make_min_max_attributes(validators, min_attr, max_attr).items() } def field2length( self, field: marshmallow.fields.Field, **kwargs: typing.Any ) -> dict: """Return the dictionary of OpenAPI field attributes for a set of :class:`Length ` validators. :param Field field: A marshmallow field. :rtype: dict """ validators = [ validator for validator in field.validators if ( hasattr(validator, "min") and hasattr(validator, "max") and hasattr(validator, "equal") ) ] is_array = isinstance( field, (marshmallow.fields.Nested, marshmallow.fields.List) ) min_attr = "minItems" if is_array else "minLength" max_attr = "maxItems" if is_array else "maxLength" equal_list = [ validator.equal for validator in validators if validator.equal is not None ] if equal_list: return {min_attr: equal_list[0], max_attr: equal_list[0]} return make_min_max_attributes(validators, min_attr, max_attr) def field2pattern( self, field: marshmallow.fields.Field, **kwargs: typing.Any ) -> dict: """Return the dictionary of OpenAPI field attributes for a :class:`Regexp ` validator. If there is more than one such validator, only the first is used in the output spec. :param Field field: A marshmallow field. :rtype: dict """ regex_validators = ( v for v in field.validators if isinstance(getattr(v, "regex", None), re.Pattern) ) v = next(regex_validators, None) attributes = {} if v is None else {"pattern": v.regex.pattern} # type:ignore if next(regex_validators, None) is not None: warnings.warn( f"More than one regex validator defined on {type(field)} field. Only the " "first one will be used in the output spec.", UserWarning, stacklevel=2, ) return attributes def metadata2properties( self, field: marshmallow.fields.Field, **kwargs: typing.Any ) -> dict: """Return a dictionary of properties extracted from field metadata. Will include field metadata that are valid properties of `OpenAPI schema objects `_ (e.g. "description", "enum", "example"). In addition, `specification extensions `_ are supported. Prefix `x_` to the desired extension when passing the keyword argument to the field constructor. apispec will convert `x_` to `x-` to comply with OpenAPI. :param Field field: A marshmallow field. :rtype: dict """ # Dasherize metadata that starts with x_ metadata = { key.replace("_", "-") if key.startswith("x_") else key: value for key, value in field.metadata.items() if isinstance(key, str) } # Avoid validation error with "Additional properties not allowed" ret = { key: value for key, value in metadata.items() if key in _VALID_PROPERTIES or key.startswith(_VALID_PREFIX) } return ret def nested2properties(self, field: marshmallow.fields.Field, ret) -> dict: """Return a dictionary of properties from :class:`Nested dict: """Return a dictionary of properties from :class:`Pluck dict: """Return a dictionary of properties from :class:`List ` fields. Will provide an `items` property based on the field's `inner` attribute :param Field field: A marshmallow field. :rtype: dict """ ret = {} if isinstance(field, marshmallow.fields.List): ret["items"] = self.field2property(field.inner) return ret def dict2properties(self, field, **kwargs: typing.Any) -> dict: """Return a dictionary of properties from :class:`Dict ` fields. Only applicable for Marshmallow versions greater than 3. Will provide an `additionalProperties` property based on the field's `value_field` attribute :param Field field: A marshmallow field. :rtype: dict """ ret = {} if isinstance(field, marshmallow.fields.Dict): value_field = field.value_field if value_field: ret["additionalProperties"] = self.field2property(value_field) else: ret["additionalProperties"] = {} return ret def timedelta2properties(self, field, **kwargs: typing.Any) -> dict: """Return a dictionary of properties from :class:`TimeDelta ` fields. Adds a `x-unit` vendor property based on the field's `precision` attribute :param Field field: A marshmallow field. :rtype: dict """ ret = {} if isinstance(field, marshmallow.fields.TimeDelta): ret["x-unit"] = field.precision return ret def enum2properties(self, field, **kwargs: typing.Any) -> dict: """Return a dictionary of properties from :class:`Enum dict: """Return a dictionary of properties from :class:`DateTime dict: """Return a dictionary of minimum and maximum attributes based on a list of validators. If either minimum or maximum values are not present in any of the validator objects that attribute will be omitted. :param validators list: A list of `Marshmallow` validator objects. Each objct is inspected for a minimum and maximum values :param min_attr string: The OpenAPI attribute for the minimum value :param max_attr string: The OpenAPI attribute for the maximum value """ attributes = {} min_list = [validator.min for validator in validators if validator.min is not None] max_list = [validator.max for validator in validators if validator.max is not None] if min_list: attributes[min_attr] = max(min_list) if max_list: attributes[max_attr] = min(max_list) return attributes apispec-6.8.1/src/apispec/ext/marshmallow/openapi.py000066400000000000000000000265271473713346600225520ustar00rootroot00000000000000"""Utilities for generating OpenAPI Specification (fka Swagger) entities from marshmallow :class:`Schemas ` and :class:`Fields `. .. warning:: This module is treated as private API. Users should not need to use this module directly. """ from __future__ import annotations import typing import marshmallow import marshmallow.exceptions from marshmallow.utils import is_collection from packaging.version import Version from apispec import APISpec from apispec.exceptions import APISpecError from .common import ( get_fields, get_unique_schema_name, make_schema_key, resolve_schema_instance, ) from .field_converter import FieldConverterMixin __location_map__ = { "match_info": "path", "query": "query", "querystring": "query", "json": "body", "headers": "header", "cookies": "cookie", "form": "formData", "files": "formData", } class OpenAPIConverter(FieldConverterMixin): """Adds methods for generating OpenAPI specification from marshmallow schemas and fields. :param Version|str openapi_version: The OpenAPI version to use. Should be in the form '2.x' or '3.x.x' to comply with the OpenAPI standard. :param callable schema_name_resolver: Callable to generate the schema definition name. Receives the `Schema` class and returns the name to be used in refs within the generated spec. When working with circular referencing this function must must not return `None` for schemas in a circular reference chain. :param APISpec spec: An initialized spec. Nested schemas will be added to the spec """ def __init__( self, openapi_version: Version | str, schema_name_resolver, spec: APISpec, ) -> None: self.openapi_version = ( Version(openapi_version) if isinstance(openapi_version, str) else openapi_version ) self.schema_name_resolver = schema_name_resolver self.spec = spec self.init_attribute_functions() self.init_parameter_attribute_functions() # Schema references self.refs: dict = {} def init_parameter_attribute_functions(self) -> None: self.parameter_attribute_functions = [ self.field2required, self.list2param, ] def add_parameter_attribute_function(self, func) -> None: """Method to add a field parameter function to the list of field parameter functions that will be called on a field to convert it to a field parameter. :param func func: the field parameter function to add The attribute function will be bound to the `OpenAPIConverter ` instance. It will be called for each field in a schema with `self ` and a `field ` instance positional arguments and `ret ` keyword argument. May mutate `ret`. User added field parameter functions will be called after all built-in field parameter functions in the order they were added. """ bound_func = func.__get__(self) setattr(self, func.__name__, bound_func) self.parameter_attribute_functions.append(bound_func) def resolve_nested_schema(self, schema): """Return the OpenAPI representation of a marshmallow Schema. Adds the schema to the spec if it isn't already present. Typically will return a dictionary with the reference to the schema's path in the spec unless the `schema_name_resolver` returns `None`, in which case the returned dictionary will contain a JSON Schema Object representation of the schema. :param schema: schema to add to the spec """ try: schema_instance = resolve_schema_instance(schema) # If schema is a string and is not found in registry, # assume it is a schema reference except marshmallow.exceptions.RegistryError: return schema schema_key = make_schema_key(schema_instance) if schema_key not in self.refs: name = self.schema_name_resolver(schema) if not name: try: json_schema = self.schema2jsonschema(schema_instance) except RuntimeError as exc: raise APISpecError( f"Name resolver returned None for schema {schema} which is " "part of a chain of circular referencing schemas. Please" " ensure that the schema_name_resolver passed to" " MarshmallowPlugin returns a string for all circular" " referencing schemas." ) from exc if getattr(schema, "many", False): return {"type": "array", "items": json_schema} return json_schema name = get_unique_schema_name(self.spec.components, name) self.spec.components.schema(name, schema=schema) return self.get_ref_dict(schema_instance) def schema2parameters( self, schema, *, location, name: str = "body", required: bool = False, description: str | None = None, ): """Return an array of OpenAPI parameters given a given marshmallow :class:`Schema `. If `location` is "body", then return an array of a single parameter; else return an array of a parameter for each included field in the :class:`Schema `. In OpenAPI 3, only "query", "header", "path" or "cookie" are allowed for the location of parameters. "requestBody" is used when fields are in the body. https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#parameterObject """ location = __location_map__.get(location, location) # OAS 2 body parameter if location == "body": param = { "in": location, "required": required, "name": name, "schema": self.resolve_nested_schema(schema), } if description: param["description"] = description return [param] assert not getattr( schema, "many", False ), "Schemas with many=True are only supported for 'json' location (aka 'in: body')" fields = get_fields(schema, exclude_dump_only=True) return [ self._field2parameter( field_obj, name=field_obj.data_key or field_name, location=location, ) for field_name, field_obj in fields.items() ] def _field2parameter( self, field: marshmallow.fields.Field, *, name: str, location: str ) -> dict: """Return an OpenAPI parameter as a `dict`, given a marshmallow :class:`Field `. https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#parameterObject """ ret: dict = {"in": location, "name": name} prop = self.field2property(field) if self.openapi_version.major < 3: ret.update(prop) else: if "description" in prop: ret["description"] = prop.pop("description") if "deprecated" in prop: ret["deprecated"] = prop.pop("deprecated") ret["schema"] = prop for param_attr_func in self.parameter_attribute_functions: ret.update(param_attr_func(field, ret=ret)) return ret def field2required( self, field: marshmallow.fields.Field, **kwargs: typing.Any ) -> dict: """Return the dictionary of OpenAPI parameter attributes for a required field. :param Field field: A marshmallow field. :rtype: dict """ ret = {} partial = getattr(field.parent, "partial", False) ret["required"] = field.required and ( not partial or (is_collection(partial) and field.name not in partial) # type:ignore ) return ret def list2param(self, field: marshmallow.fields.Field, **kwargs: typing.Any) -> dict: """Return a dictionary of parameter properties from :class:`List ` instance. Schema may optionally provide the ``title`` and ``description`` class Meta options. https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#schemaObject :param Schema schema: A marshmallow Schema instance :rtype: dict, a JSON Schema Object """ fields = get_fields(schema) Meta = getattr(schema, "Meta", None) partial = getattr(schema, "partial", None) jsonschema = self.fields2jsonschema(fields, partial=partial) if hasattr(Meta, "title"): jsonschema["title"] = Meta.title if hasattr(Meta, "description"): jsonschema["description"] = Meta.description if hasattr(Meta, "unknown") and Meta.unknown != marshmallow.EXCLUDE: jsonschema["additionalProperties"] = Meta.unknown == marshmallow.INCLUDE return jsonschema def fields2jsonschema(self, fields, *, partial=None): """Return the JSON Schema Object given a mapping between field names and :class:`Field ` objects. :param dict fields: A dictionary of field name field object pairs :param bool|tuple partial: Whether to override a field's required flag. If `True` no fields will be set as required. If an iterable fields in the iterable will not be marked as required. :rtype: dict, a JSON Schema Object """ jsonschema = {"type": "object", "properties": {}} for field_name, field_obj in fields.items(): observed_field_name = field_obj.data_key or field_name prop = self.field2property(field_obj) jsonschema["properties"][observed_field_name] = prop if field_obj.required: if not partial or ( is_collection(partial) and field_name not in partial ): jsonschema.setdefault("required", []).append(observed_field_name) if "required" in jsonschema: jsonschema["required"].sort() return jsonschema def get_ref_dict(self, schema): """Method to create a dictionary containing a JSON reference to the schema in the spec """ schema_key = make_schema_key(schema) ref_schema = self.spec.components.get_ref("schema", self.refs[schema_key]) if getattr(schema, "many", False): return {"type": "array", "items": ref_schema} return ref_schema apispec-6.8.1/src/apispec/ext/marshmallow/schema_resolver.py000066400000000000000000000250721473713346600242720ustar00rootroot00000000000000from .common import resolve_schema_instance class SchemaResolver: """Resolve marshmallow Schemas in OpenAPI components and translate to OpenAPI `schema objects `_, `parameter objects `_ or `reference objects `_. """ def __init__(self, openapi_version, converter): self.openapi_version = openapi_version self.converter = converter def resolve_operations(self, operations, **kwargs): """Resolve marshmallow Schemas in a dict mapping operation to OpenApi `Operation Object https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#operationObject`_ """ for operation in operations.values(): if not isinstance(operation, dict): continue if "parameters" in operation: operation["parameters"] = self.resolve_parameters( operation["parameters"] ) if self.openapi_version.major >= 3: self.resolve_callback(operation.get("callbacks", {})) if "requestBody" in operation: self.resolve_schema(operation["requestBody"]) for response in operation.get("responses", {}).values(): self.resolve_response(response) def resolve_callback(self, callbacks): """Resolve marshmallow Schemas in a dict mapping callback name to OpenApi `Callback Object https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#callbackObject`_. This is done recursively, so it is possible to define callbacks in your callbacks. Example: :: # Input { "userEvent": { "https://my.example/user-callback": { "post": { "requestBody": { "content": {"application/json": {"schema": UserSchema}} } }, } } } # Output { "userEvent": { "https://my.example/user-callback": { "post": { "requestBody": { "content": { "application/json": { "schema": {"$ref": "#/components/schemas/User"} } } } }, } } } """ for callback in callbacks.values(): if isinstance(callback, dict): for path in callback.values(): self.resolve_operations(path) def resolve_parameters(self, parameters): """Resolve marshmallow Schemas in a list of OpenAPI `Parameter Objects `_. Each parameter object that contains a Schema will be translated into one or more Parameter Objects. If the value of a `schema` key is marshmallow Schema class, instance or a string that resolves to a Schema Class each field in the Schema will be expanded as a separate Parameter Object. Example: :: # Input class UserSchema(Schema): name = fields.String() id = fields.Int() [{"in": "query", "schema": "UserSchema"}] # Output [ { "in": "query", "name": "id", "required": False, "schema": {"type": "integer"}, }, { "in": "query", "name": "name", "required": False, "schema": {"type": "string"}, }, ] If the Parameter Object contains a `content` key a single Parameter Object is returned with the Schema translated into a Schema Object or Reference Object. Example: :: # Input [ { "in": "query", "name": "pet", "content": {"application/json": {"schema": "PetSchema"}}, } ] # Output [ { "in": "query", "name": "pet", "content": { "application/json": {"schema": {"$ref": "#/components/schemas/Pet"}} }, } ] :param list parameters: the list of OpenAPI parameter objects to resolve. """ resolved = [] for parameter in parameters: if ( isinstance(parameter, dict) and not isinstance(parameter.get("schema", {}), dict) and "in" in parameter ): schema_instance = resolve_schema_instance(parameter.pop("schema")) resolved += self.converter.schema2parameters( schema_instance, location=parameter.pop("in"), **parameter ) else: self.resolve_schema(parameter) resolved.append(parameter) return resolved def resolve_response(self, response): """Resolve marshmallow Schemas in OpenAPI `Response Objects `_. Schemas may appear in either a Media Type Object or a Header Object. Example: :: # Input { "content": {"application/json": {"schema": "PetSchema"}}, "description": "successful operation", "headers": {"PetHeader": {"schema": "PetHeaderSchema"}}, } # Output { "content": { "application/json": {"schema": {"$ref": "#/components/schemas/Pet"}} }, "description": "successful operation", "headers": { "PetHeader": {"schema": {"$ref": "#/components/schemas/PetHeader"}} }, } :param dict response: the response object to resolve. """ self.resolve_schema(response) if "headers" in response: for header in response["headers"].values(): self.resolve_schema(header) def resolve_schema(self, data): """Resolve marshmallow Schemas in an OpenAPI component or header - modifies the input dictionary to translate marshmallow Schemas to OpenAPI Schema Objects or Reference Objects. OpenAPIv3 Components: :: # Input { "description": "user to add to the system", "content": {"application/json": {"schema": "UserSchema"}}, } # Output { "description": "user to add to the system", "content": { "application/json": {"schema": {"$ref": "#/components/schemas/User"}} }, } :param dict|str data: either a parameter or response dictionary that may contain a schema, or a reference provided as string """ if not isinstance(data, dict): return # OAS 2 component or OAS 3 parameter or header if "schema" in data: data["schema"] = self.resolve_schema_dict(data["schema"]) # OAS 3 component except header if self.openapi_version.major >= 3: if "content" in data: for content in data["content"].values(): if "schema" in content: content["schema"] = self.resolve_schema_dict(content["schema"]) def resolve_schema_dict(self, schema): """Resolve a marshmallow Schema class, object, or a string that resolves to a Schema class or a schema reference or an OpenAPI Schema Object containing one of the above to an OpenAPI Schema Object or Reference Object. If the input is a marshmallow Schema class, object or a string that resolves to a Schema class the Schema will be translated to an OpenAPI Schema Object or Reference Object. Example: :: # Input "PetSchema" # Output {"$ref": "#/components/schemas/Pet"} If the input is a dictionary representation of an OpenAPI Schema Object recursively search for a marshmallow Schemas to resolve. For `"type": "array"`, marshmallow Schemas may appear as the value of the `items` key. For `"type": "object"` Marshmalow Schemas may appear as values in the `properties` dictionary. Examples: :: # Input {"type": "array", "items": "PetSchema"} # Output {"type": "array", "items": {"$ref": "#/components/schemas/Pet"}} # Input {"type": "object", "properties": {"pet": "PetSchcema", "user": "UserSchema"}} # Output { "type": "object", "properties": { "pet": {"$ref": "#/components/schemas/Pet"}, "user": {"$ref": "#/components/schemas/User"}, }, } :param string|Schema|dict schema: the schema to resolve. """ if isinstance(schema, dict): if schema.get("type") == "array" and "items" in schema: schema["items"] = self.resolve_schema_dict(schema["items"]) if schema.get("type") == "object" and "properties" in schema: schema["properties"] = { k: self.resolve_schema_dict(v) for k, v in schema["properties"].items() } for keyword in ("oneOf", "anyOf", "allOf"): if keyword in schema: schema[keyword] = [ self.resolve_schema_dict(s) for s in schema[keyword] ] if "not" in schema: schema["not"] = self.resolve_schema_dict(schema["not"]) return schema return self.converter.resolve_nested_schema(schema) apispec-6.8.1/src/apispec/plugin.py000066400000000000000000000072341473713346600172610ustar00rootroot00000000000000"""Base class for Plugin classes.""" from __future__ import annotations import typing from .core import APISpec from .exceptions import PluginMethodNotImplementedError class BasePlugin: """Base class for APISpec plugin classes.""" def init_spec(self, spec: APISpec) -> None: """Initialize plugin with APISpec object :param APISpec spec: APISpec object this plugin instance is attached to """ def schema_helper( self, name: str, definition: dict, **kwargs: typing.Any ) -> dict | None: """May return definition as a dict. :param str name: Identifier by which schema may be referenced :param dict definition: Schema definition :param kwargs: All additional keywords arguments sent to `APISpec.schema()` """ raise PluginMethodNotImplementedError def response_helper(self, response: dict, **kwargs: typing.Any) -> dict | None: """May return response component description as a dict. :param dict response: Response fields :param kwargs: All additional keywords arguments sent to `APISpec.response()` """ raise PluginMethodNotImplementedError def parameter_helper(self, parameter: dict, **kwargs: typing.Any) -> dict | None: """May return parameter component description as a dict. :param dict parameter: Parameter fields :param kwargs: All additional keywords arguments sent to `APISpec.parameter()` """ raise PluginMethodNotImplementedError def header_helper(self, header: dict, **kwargs: typing.Any) -> dict | None: """May return header component description as a dict. :param dict header: Header fields :param kwargs: All additional keywords arguments sent to `APISpec.header()` """ raise PluginMethodNotImplementedError def path_helper( self, path: str | None = None, operations: dict | None = None, parameters: list[dict] | None = None, **kwargs: typing.Any, ) -> str | None: """May return a path as string and mutate operations dict and parameters list. :param str path: Path to the resource :param dict operations: A `dict` mapping HTTP methods to operation object. See https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#operationObject :param list parameters: A `list` of parameters objects or references for the path. See https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#parameterObject and https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#referenceObject :param kwargs: All additional keywords arguments sent to `APISpec.path()` Return value should be a string or None. If a string is returned, it is set as the path. The last path helper returning a string sets the path value. Therefore, the order of plugin registration matters. However, generally, registering several plugins that return a path does not make sense. """ raise PluginMethodNotImplementedError def operation_helper( self, path: str | None = None, operations: dict | None = None, **kwargs: typing.Any, ) -> None: """May mutate operations. :param str path: Path to the resource :param dict operations: A `dict` mapping HTTP methods to operation object. See https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#operationObject :param kwargs: All additional keywords arguments sent to `APISpec.path()` """ raise PluginMethodNotImplementedError apispec-6.8.1/src/apispec/py.typed000066400000000000000000000000001473713346600170700ustar00rootroot00000000000000apispec-6.8.1/src/apispec/utils.py000066400000000000000000000055671473713346600171320ustar00rootroot00000000000000"""Various utilities for parsing OpenAPI operations from docstrings and validating against the OpenAPI spec. """ from __future__ import annotations import re COMPONENT_SUBSECTIONS = { 2: { "schema": "definitions", "response": "responses", "parameter": "parameters", "security_scheme": "securityDefinitions", }, 3: { "schema": "schemas", "response": "responses", "parameter": "parameters", "header": "headers", "example": "examples", "security_scheme": "securitySchemes", }, } def build_reference( component_type: str, openapi_major_version: int, component_name: str ) -> dict[str, str]: """Return path to reference :param str component_type: Component type (schema, parameter, response, security_scheme) :param int openapi_major_version: OpenAPI major version (2 or 3) :param str component_name: Name of component to reference """ return { "$ref": "#/{}{}/{}".format( "components/" if openapi_major_version >= 3 else "", COMPONENT_SUBSECTIONS[openapi_major_version][component_type], component_name, ) } # from django.contrib.admindocs.utils def trim_docstring(docstring: str) -> str: """Uniformly trims leading/trailing whitespace from docstrings. Based on http://www.python.org/peps/pep-0257.html#handling-docstring-indentation """ if not docstring or not docstring.strip(): return "" # Convert tabs to spaces and split into lines lines = docstring.expandtabs().splitlines() indent = min(len(line) - len(line.lstrip()) for line in lines if line.lstrip()) trimmed = [lines[0].lstrip()] + [line[indent:].rstrip() for line in lines[1:]] return "\n".join(trimmed).strip() # from rest_framework.utils.formatting def dedent(content: str) -> str: """ Remove leading indent from a block of text. Used when generating descriptions from docstrings. Note that python's `textwrap.dedent` doesn't quite cut it, as it fails to dedent multiline docstrings that include unindented text on the initial line. """ whitespace_counts = [ len(line) - len(line.lstrip(" ")) for line in content.splitlines()[1:] if line.lstrip() ] # unindent the content if needed if whitespace_counts: whitespace_pattern = "^" + (" " * min(whitespace_counts)) content = re.sub(re.compile(whitespace_pattern, re.MULTILINE), "", content) return content.strip() # http://stackoverflow.com/a/8310229 def deepupdate(original: dict, update: dict) -> dict: """Recursively update a dict. Subdict's won't be overwritten but also updated. """ for key, value in original.items(): if key not in update: update[key] = value elif isinstance(value, dict): deepupdate(value, update[key]) return update apispec-6.8.1/src/apispec/yaml_utils.py000066400000000000000000000025641473713346600201460ustar00rootroot00000000000000"""YAML utilities""" from __future__ import annotations import typing import yaml from apispec.utils import dedent, trim_docstring def dict_to_yaml(dic: dict, yaml_dump_kwargs: typing.Any | None = None) -> str: """Serializes a dictionary to YAML.""" yaml_dump_kwargs = yaml_dump_kwargs or {} # By default, don't sort alphabetically to respect schema field ordering yaml_dump_kwargs.setdefault("sort_keys", False) return yaml.dump(dic, **yaml_dump_kwargs) def load_yaml_from_docstring(docstring: str) -> dict: """Loads YAML from docstring.""" split_lines = trim_docstring(docstring).split("\n") # Cut YAML from rest of docstring for index, line in enumerate(split_lines): line = line.strip() if line.startswith("---"): cut_from = index break else: return {} yaml_string = "\n".join(split_lines[cut_from:]) yaml_string = dedent(yaml_string) return yaml.safe_load(yaml_string) or {} PATH_KEYS = {"get", "put", "post", "delete", "options", "head", "patch"} def load_operations_from_docstring(docstring: str) -> dict: """Return a dictionary of OpenAPI operations parsed from a a docstring. """ doc_data = load_yaml_from_docstring(docstring) return { key: val for key, val in doc_data.items() if key in PATH_KEYS or key.startswith("x-") } apispec-6.8.1/tests/000077500000000000000000000000001473713346600143325ustar00rootroot00000000000000apispec-6.8.1/tests/__init__.py000066400000000000000000000000001473713346600164310ustar00rootroot00000000000000apispec-6.8.1/tests/conftest.py000066400000000000000000000014521473713346600165330ustar00rootroot00000000000000from collections import namedtuple import pytest from apispec import APISpec from apispec.ext.marshmallow import MarshmallowPlugin def make_spec(openapi_version): ma_plugin = MarshmallowPlugin() spec = APISpec( title="Validation", version="0.1", openapi_version=openapi_version, plugins=(ma_plugin,), ) return namedtuple("Spec", ("spec", "marshmallow_plugin", "openapi"))( spec, ma_plugin, ma_plugin.converter ) @pytest.fixture(params=("2.0", "3.0.0")) def spec_fixture(request): return make_spec(request.param) @pytest.fixture(params=("2.0", "3.0.0")) def spec(request): return make_spec(request.param).spec @pytest.fixture(params=("2.0", "3.0.0")) def openapi(request): spec = make_spec(request.param) return spec.openapi apispec-6.8.1/tests/schemas.py000066400000000000000000000041221473713346600163260ustar00rootroot00000000000000from marshmallow import Schema, fields class PetSchema(Schema): description = dict(id="Pet id", name="Pet name", password="Password") id = fields.Int(dump_only=True, metadata={"description": description["id"]}) name = fields.Str( required=True, metadata={ "description": description["name"], "deprecated": False, "allowEmptyValue": False, }, ) password = fields.Str( load_only=True, metadata={"description": description["password"]} ) class SampleSchema(Schema): runs = fields.Nested("RunSchema", many=True) count = fields.Int() class RunSchema(Schema): sample = fields.Nested(SampleSchema) class AnalysisSchema(Schema): sample = fields.Nested(SampleSchema) class AnalysisWithListSchema(Schema): samples = fields.List(fields.Nested(SampleSchema)) class PatternedObjectSchema(Schema): count = fields.Int(dump_only=True, metadata={"x-count": 1}) count2 = fields.Int(dump_only=True, metadata={"x_count2": 2}) class SelfReferencingSchema(Schema): id = fields.Int() single = fields.Nested(lambda: SelfReferencingSchema()) multiple = fields.Nested(lambda: SelfReferencingSchema(many=True)) class OrderedSchema(Schema): field1 = fields.Int() field2 = fields.Int() field3 = fields.Int() field4 = fields.Int() field5 = fields.Int() class Meta: ordered = True class DefaultValuesSchema(Schema): number_auto_default = fields.Int(load_default=12) number_manual_default = fields.Int(load_default=12, metadata={"default": 42}) string_callable_default = fields.Str(load_default=lambda: "Callable") string_manual_default = fields.Str( load_default=lambda: "Callable", metadata={"default": "Manual"} ) numbers = fields.List(fields.Int, load_default=list) class CategorySchema(Schema): id = fields.Int() name = fields.Str(required=True) breed = fields.Str(dump_only=True) class CustomList(fields.List): pass class CustomStringField(fields.String): pass class CustomIntegerField(fields.Integer): pass apispec-6.8.1/tests/test_core.py000066400000000000000000001450661473713346600167070ustar00rootroot00000000000000import copy from http import HTTPStatus import pytest import yaml from apispec import APISpec, BasePlugin from apispec.exceptions import ( APISpecError, DuplicateComponentNameError, DuplicateParameterError, InvalidParameterError, ) from .utils import ( build_ref, get_examples, get_headers, get_parameters, get_paths, get_responses, get_schemas, get_security_schemes, ) description = "This is a sample Petstore server. You can find out more " 'about Swagger at http://swagger.wordnik.com ' "or on irc.freenode.net, #swagger. For this sample, you can use the api " 'key "special-key" to test the authorization filters' class RefsSchemaTestMixin: REFS_SCHEMA = { "properties": { "nested": "NestedSchema", "deep_nested": {"properties": {"nested": "NestedSchema"}}, "nested_list": {"items": "DeepNestedSchema"}, "deep_nested_list": { "items": {"properties": {"nested": "DeepNestedSchema"}} }, "allof": { "allOf": [ "AllOfSchema", {"properties": {"nested": "AllOfSchema"}}, ] }, "oneof": { "oneOf": [ "OneOfSchema", {"properties": {"nested": "OneOfSchema"}}, ] }, "anyof": { "anyOf": [ "AnyOfSchema", {"properties": {"nested": "AnyOfSchema"}}, ] }, "not": "NotSchema", "deep_not": {"properties": {"nested": "DeepNotSchema"}}, } } @staticmethod def assert_schema_refs(spec, schema): props = schema["properties"] assert props["nested"] == build_ref(spec, "schema", "NestedSchema") assert props["deep_nested"]["properties"]["nested"] == build_ref( spec, "schema", "NestedSchema" ) assert props["nested_list"]["items"] == build_ref( spec, "schema", "DeepNestedSchema" ) assert props["deep_nested_list"]["items"]["properties"]["nested"] == build_ref( spec, "schema", "DeepNestedSchema" ) assert props["allof"]["allOf"][0] == build_ref(spec, "schema", "AllOfSchema") assert props["allof"]["allOf"][1]["properties"]["nested"] == build_ref( spec, "schema", "AllOfSchema" ) assert props["oneof"]["oneOf"][0] == build_ref(spec, "schema", "OneOfSchema") assert props["oneof"]["oneOf"][1]["properties"]["nested"] == build_ref( spec, "schema", "OneOfSchema" ) assert props["anyof"]["anyOf"][0] == build_ref(spec, "schema", "AnyOfSchema") assert props["anyof"]["anyOf"][1]["properties"]["nested"] == build_ref( spec, "schema", "AnyOfSchema" ) assert props["not"] == build_ref(spec, "schema", "NotSchema") assert props["deep_not"]["properties"]["nested"] == build_ref( spec, "schema", "DeepNotSchema" ) @pytest.fixture(params=("2.0", "3.0.0")) def spec(request): openapi_version = request.param if openapi_version == "2.0": security_kwargs = {"security": [{"apiKey": []}]} else: security_kwargs = { "components": { "securitySchemes": { "bearerAuth": dict(type="http", scheme="bearer", bearerFormat="JWT") }, "schemas": { "ErrorResponse": { "type": "object", "properties": { "ok": { "type": "boolean", "description": "status indicator", "example": False, } }, "required": ["ok"], } }, } } return APISpec( title="Swagger Petstore", version="1.0.0", openapi_version=openapi_version, info={"description": description}, **security_kwargs, ) class TestAPISpecInit: def test_raises_wrong_apispec_version(self): message = "Not a valid OpenAPI version number:" with pytest.raises(APISpecError, match=message): APISpec( "Swagger Petstore", version="1.0.0", openapi_version="4.0", # 4.0 is not supported info={"description": description}, security=[{"apiKey": []}], ) class TestMetadata: def test_openapi_metadata(self, spec): metadata = spec.to_dict() assert metadata["info"]["title"] == "Swagger Petstore" assert metadata["info"]["version"] == "1.0.0" assert metadata["info"]["description"] == description if spec.openapi_version.major < 3: assert metadata["swagger"] == str(spec.openapi_version) assert metadata["security"] == [{"apiKey": []}] else: assert metadata["openapi"] == str(spec.openapi_version) security_schemes = { "bearerAuth": dict(type="http", scheme="bearer", bearerFormat="JWT") } assert metadata["components"]["securitySchemes"] == security_schemes assert metadata["components"]["schemas"].get("ErrorResponse", False) assert metadata["info"]["title"] == "Swagger Petstore" assert metadata["info"]["version"] == "1.0.0" assert metadata["info"]["description"] == description @pytest.mark.parametrize("spec", ("3.0.0",), indirect=True) def test_openapi_metadata_merge_v3(self, spec): properties = { "ok": { "type": "boolean", "description": "property description", "example": True, } } spec.components.schema( "definition", {"properties": properties, "description": "description"} ) metadata = spec.to_dict() assert metadata["components"]["schemas"].get("ErrorResponse", False) assert metadata["components"]["schemas"].get("definition", False) class TestTags: tag = { "name": "MyTag", "description": "This tag gathers all API endpoints which are mine.", } def test_tag(self, spec): spec.tag(self.tag) tags_json = spec.to_dict()["tags"] assert self.tag in tags_json def test_tag_is_chainable(self, spec): spec.tag({"name": "tag1"}).tag({"name": "tag2"}) assert spec.to_dict()["tags"] == [{"name": "tag1"}, {"name": "tag2"}] class TestComponents(RefsSchemaTestMixin): properties = { "id": {"type": "integer", "format": "int64"}, "name": {"type": "string", "example": "doggie"}, } def test_schema(self, spec): spec.components.schema("Pet", {"properties": self.properties}) schemas = get_schemas(spec) assert "Pet" in schemas assert schemas["Pet"]["properties"] == self.properties def test_schema_is_chainable(self, spec): spec.components.schema("Pet", {"properties": {}}).schema( "Plant", {"properties": {}} ) schemas = get_schemas(spec) assert "Pet" in schemas assert "Plant" in schemas def test_schema_description(self, spec): model_description = "An animal which lives with humans." spec.components.schema( "Pet", {"properties": self.properties, "description": model_description} ) schemas = get_schemas(spec) assert schemas["Pet"]["description"] == model_description def test_schema_stores_enum(self, spec): enum = ["name", "photoUrls"] spec.components.schema("Pet", {"properties": self.properties, "enum": enum}) schemas = get_schemas(spec) assert schemas["Pet"]["enum"] == enum def test_schema_discriminator(self, spec): spec.components.schema( "Pet", {"properties": self.properties, "discriminator": "name"} ) schemas = get_schemas(spec) assert schemas["Pet"]["discriminator"] == "name" def test_schema_duplicate_name(self, spec): spec.components.schema("Pet", {"properties": self.properties}) with pytest.raises( DuplicateComponentNameError, match='Another schema with name "Pet" is already registered.', ): spec.components.schema("Pet", properties=self.properties) def test_response(self, spec): response = {"description": "Pet not found"} spec.components.response("NotFound", response) responses = get_responses(spec) assert responses["NotFound"] == response def test_response_is_chainable(self, spec): spec.components.response("resp1").response("resp2") responses = get_responses(spec) assert "resp1" in responses assert "resp2" in responses def test_response_duplicate_name(self, spec): spec.components.response("test_response") with pytest.raises( DuplicateComponentNameError, match='Another response with name "test_response" is already registered.', ): spec.components.response("test_response") def test_parameter(self, spec): # Note: this is an OpenAPI v2 parameter header # but is does the job for the test even for OpenAPI v3 parameter = {"format": "int64", "type": "integer"} spec.components.parameter("PetId", "path", parameter) params = get_parameters(spec) assert params["PetId"] == { "format": "int64", "type": "integer", "in": "path", "name": "PetId", "required": True, } def test_parameter_is_chainable(self, spec): spec.components.parameter("param1", "path").parameter("param2", "path") params = get_parameters(spec) assert "param1" in params assert "param2" in params def test_parameter_duplicate_name(self, spec): spec.components.parameter("test_parameter", "path") with pytest.raises( DuplicateComponentNameError, match='Another parameter with name "test_parameter" is already registered.', ): spec.components.parameter("test_parameter", "path") # Referenced headers are only supported in OAS 3.x @pytest.mark.parametrize("spec", ("3.0.0",), indirect=True) def test_header(self, spec): header = {"schema": {"type": "string"}} spec.components.header("test_header", header.copy()) headers = get_headers(spec) assert headers["test_header"] == header # Referenced headers are only supported in OAS 3.x @pytest.mark.parametrize("spec", ("3.0.0",), indirect=True) def test_header_is_chainable(self, spec): header = {"schema": {"type": "string"}} spec.components.header("header1", header).header("header2", header) headers = get_headers(spec) assert "header1" in headers assert "header2" in headers # Referenced headers are only supported in OAS 3.x @pytest.mark.parametrize("spec", ("3.0.0",), indirect=True) def test_header_duplicate_name(self, spec): spec.components.header("test_header", {"schema": {"type": "string"}}) with pytest.raises( DuplicateComponentNameError, match='Another header with name "test_header" is already registered.', ): spec.components.header("test_header", {"schema": {"type": "integer"}}) # Referenced examples are only supported in OAS 3.x @pytest.mark.parametrize("spec", ("3.0.0",), indirect=True) def test_example(self, spec): spec.components.example("test_example", {"value": {"a": "b"}}) examples = get_examples(spec) assert examples["test_example"]["value"] == {"a": "b"} @pytest.mark.parametrize("spec", ("3.0.0",), indirect=True) def test_example_is_chainable(self, spec): spec.components.example("test_example_1", {}).example("test_example_2", {}) examples = get_examples(spec) assert "test_example_1" in examples assert "test_example_2" in examples @pytest.mark.parametrize("spec", ("3.0.0",), indirect=True) def test_example_duplicate_name(self, spec): spec.components.example("test_example", {}) with pytest.raises( DuplicateComponentNameError, match='Another example with name "test_example" is already registered.', ): spec.components.example("test_example", {}) def test_security_scheme(self, spec): sec_scheme = {"type": "apiKey", "in": "header", "name": "X-API-Key"} spec.components.security_scheme("ApiKeyAuth", sec_scheme) assert get_security_schemes(spec)["ApiKeyAuth"] == sec_scheme def test_security_scheme_is_chainable(self, spec): spec.components.security_scheme("sec_1", {}).security_scheme("sec_2", {}) security_schemes = get_security_schemes(spec) assert "sec_1" in security_schemes assert "sec_2" in security_schemes def test_security_scheme_duplicate_name(self, spec): sec_scheme_1 = {"type": "apiKey", "in": "header", "name": "X-API-Key"} sec_scheme_2 = {"type": "apiKey", "in": "header", "name": "X-API-Key-2"} spec.components.security_scheme("ApiKeyAuth", sec_scheme_1) with pytest.raises( DuplicateComponentNameError, match='Another security scheme with name "ApiKeyAuth" is already registered.', ): spec.components.security_scheme("ApiKeyAuth", sec_scheme_2) def test_to_yaml(self, spec): enum = ["name", "photoUrls"] spec.components.schema("Pet", properties=self.properties, enum=enum) assert spec.to_dict() == yaml.safe_load(spec.to_yaml()) def test_components_can_be_accessed_by_plugin_in_init_spec(self): class TestPlugin(BasePlugin): def init_spec(self, spec): spec.components.schema( "TestSchema", {"properties": {"key": {"type": "string"}}, "type": "object"}, ) spec = APISpec( "Test API", version="0.0.1", openapi_version="2.0", plugins=[TestPlugin()] ) assert get_schemas(spec) == { "TestSchema": {"properties": {"key": {"type": "string"}}, "type": "object"} } def test_components_resolve_refs_in_schema(self, spec): spec.components.schema("refs_schema", copy.deepcopy(self.REFS_SCHEMA)) self.assert_schema_refs(spec, get_schemas(spec)["refs_schema"]) def test_components_resolve_response_schema(self, spec): schema = {"schema": "PetSchema"} if spec.openapi_version.major >= 3: schema = {"content": {"application/json": schema}} spec.components.response("Response", schema) resp = get_responses(spec)["Response"] if spec.openapi_version.major < 3: schema = resp["schema"] else: schema = resp["content"]["application/json"]["schema"] assert schema == build_ref(spec, "schema", "PetSchema") # "headers" components section only exists in OAS 3 @pytest.mark.parametrize("spec", ("3.0.0",), indirect=True) def test_components_resolve_response_header(self, spec): response = {"headers": {"header_1": "Header_1"}} spec.components.response("Response", response) resp = get_responses(spec)["Response"] header_1 = resp["headers"]["header_1"] assert header_1 == build_ref(spec, "header", "Header_1") # "headers" components section only exists in OAS 3 @pytest.mark.parametrize("spec", ("3.0.0",), indirect=True) def test_components_resolve_response_header_schema(self, spec): response = {"headers": {"header_1": {"name": "Pet", "schema": "PetSchema"}}} spec.components.response("Response", response) resp = get_responses(spec)["Response"] header_1 = resp["headers"]["header_1"] assert header_1["schema"] == build_ref(spec, "schema", "PetSchema") # "headers" components section only exists in OAS 3 @pytest.mark.parametrize("spec", ("3.0.0",), indirect=True) def test_components_resolve_response_header_examples(self, spec): response = { "headers": { "header_1": {"name": "Pet", "examples": {"example_1": "Example_1"}} } } spec.components.response("Response", response) resp = get_responses(spec)["Response"] header_1 = resp["headers"]["header_1"] assert header_1["examples"]["example_1"] == build_ref( spec, "example", "Example_1" ) # "examples" components section only exists in OAS 3 @pytest.mark.parametrize("spec", ("3.0.0",), indirect=True) def test_components_resolve_response_examples(self, spec): response = { "content": {"application/json": {"examples": {"example_1": "Example_1"}}} } spec.components.response("Response", response) resp = get_responses(spec)["Response"] example_1 = resp["content"]["application/json"]["examples"]["example_1"] assert example_1 == build_ref(spec, "example", "Example_1") def test_components_resolve_refs_in_response_schema(self, spec): schema = copy.deepcopy(self.REFS_SCHEMA) if spec.openapi_version.major >= 3: response = {"content": {"application/json": {"schema": schema}}} else: response = {"schema": schema} spec.components.response("Response", response) resp = get_responses(spec)["Response"] if spec.openapi_version.major < 3: schema = resp["schema"] else: schema = resp["content"]["application/json"]["schema"] self.assert_schema_refs(spec, schema) # "headers" components section only exists in OAS 3 @pytest.mark.parametrize("spec", ("3.0.0",), indirect=True) def test_components_resolve_refs_in_response_header_schema(self, spec): header = {"schema": copy.deepcopy(self.REFS_SCHEMA)} response = {"headers": {"header": header}} spec.components.response("Response", response) resp = get_responses(spec)["Response"] self.assert_schema_refs(spec, resp["headers"]["header"]["schema"]) # "examples" components section only exists in OAS 3 @pytest.mark.parametrize("spec", ("3.0.0",), indirect=True) def test_components_resolve_parameter_examples(self, spec): parameter = { "examples": {"example_1": "Example_1"}, } spec.components.parameter("param", "path", parameter) param = get_parameters(spec)["param"] example_1 = param["examples"]["example_1"] assert example_1 == build_ref(spec, "example", "Example_1") def test_components_resolve_parameter_schemas(self, spec): parameter = {"schema": "PetSchema"} spec.components.parameter("param", "path", parameter) param = get_parameters(spec)["param"] assert param["schema"] == build_ref(spec, "schema", "PetSchema") @pytest.mark.parametrize("spec", ("3.0.0",), indirect=True) def test_components_resolve_parameter_schemas_v3(self, spec): parameter = {"content": {"application/json": {"schema": "PetSchema"}}} spec.components.parameter("param", "path", parameter) param = get_parameters(spec)["param"] schema = param["content"]["application/json"]["schema"] assert schema == build_ref(spec, "schema", "PetSchema") def test_components_resolve_refs_in_parameter_schema(self, spec): parameter = {"schema": copy.deepcopy(self.REFS_SCHEMA)} spec.components.parameter("param", "path", parameter) self.assert_schema_refs(spec, get_parameters(spec)["param"]["schema"]) # "headers" components section only exists in OAS 3 @pytest.mark.parametrize("spec", ("3.0.0",), indirect=True) def test_components_resolve_header_schema(self, spec): header = {"name": "Pet", "schema": "PetSchema"} spec.components.header("header", header) header = get_headers(spec)["header"] assert header["schema"] == build_ref(spec, "schema", "PetSchema") # "headers" components section only exists in OAS 3 @pytest.mark.parametrize("spec", ("3.0.0",), indirect=True) def test_components_resolve_header_examples(self, spec): header = {"name": "Pet", "examples": {"example_1": "Example_1"}} spec.components.header("header", header) header = get_headers(spec)["header"] assert header["examples"]["example_1"] == build_ref( spec, "example", "Example_1" ) # "headers" components section only exists in OAS 3 @pytest.mark.parametrize("spec", ("3.0.0",), indirect=True) def test_components_resolve_refs_in_header_schema(self, spec): header = {"schema": copy.deepcopy(self.REFS_SCHEMA)} spec.components.header("header", header) self.assert_schema_refs(spec, get_headers(spec)["header"]["schema"]) def test_schema_lazy(self, spec): spec.components.schema("Pet_1", {"properties": self.properties}, lazy=False) spec.components.schema("Pet_2", {"properties": self.properties}, lazy=True) schemas = get_schemas(spec) assert "Pet_1" in schemas assert "Pet_2" not in schemas spec.components.schema("PetFriend", {"oneOf": ["Pet_1", "Pet_2"]}) schemas = get_schemas(spec) assert "Pet_2" in schemas assert schemas["Pet_2"]["properties"] == self.properties def test_response_lazy(self, spec): response_1 = {"description": "Response 1"} response_2 = {"description": "Response 2"} spec.components.response("Response_1", response_1, lazy=False) spec.components.response("Response_2", response_2, lazy=True) responses = get_responses(spec) assert "Response_1" in responses assert "Response_2" not in responses spec.path("/path", operations={"get": {"responses": {"200": "Response_2"}}}) responses = get_responses(spec) assert "Response_2" in responses def test_parameter_lazy(self, spec): parameter = {"format": "int64", "type": "integer"} spec.components.parameter("Param_1", "path", parameter, lazy=False) spec.components.parameter("Param_2", "path", parameter, lazy=True) params = get_parameters(spec) assert "Param_1" in params assert "Param_2" not in params spec.path("/path", operations={"get": {"parameters": ["Param_1", "Param_2"]}}) assert "Param_2" in params # Referenced headers are only supported in OAS 3.x @pytest.mark.parametrize("spec", ("3.0.0",), indirect=True) def test_header_lazy(self, spec): header = {"schema": {"type": "string"}} spec.components.header("Header_1", header, lazy=False) spec.components.header("Header_2", header, lazy=True) headers = get_headers(spec) assert "Header_1" in headers assert "Header_2" not in headers spec.path( "/path", operations={ "get": {"responses": {"200": {"headers": {"header_2": "Header_2"}}}} }, ) assert "Header_2" in headers # Referenced examples are only supported in OAS 3.x @pytest.mark.parametrize("spec", ("3.0.0",), indirect=True) def test_example_lazy(self, spec): spec.components.example("Example_1", {"value": {"a": "b"}}, lazy=False) spec.components.example("Example_2", {"value": {"a": "b"}}, lazy=True) examples = get_examples(spec) assert "Example_1" in examples assert "Example_2" not in examples spec.path( "/path", operations={ "get": { "responses": { "200": { "content": { "application/json": { "examples": {"example_2": "Example_2"} } } } } } }, ) assert "Example_2" in examples class TestPath(RefsSchemaTestMixin): paths = { "/pet/{petId}": { "get": { "parameters": [ { "required": True, "format": "int64", "name": "petId", "in": "path", "type": "integer", "description": "ID of pet that needs to be fetched", } ], "responses": { "200": {"description": "successful operation"}, "400": {"description": "Invalid ID supplied"}, "404": {"description": "Pet not found"}, }, "produces": ["application/json", "application/xml"], "operationId": "getPetById", "summary": "Find pet by ID", "description": ( "Returns a pet when ID < 10. " "ID > 10 or nonintegers will simulate API error conditions" ), "tags": ["pet"], } } } def test_path(self, spec): route_spec = self.paths["/pet/{petId}"]["get"] spec.path( path="/pet/{petId}", operations=dict( get=dict( parameters=route_spec["parameters"], responses=route_spec["responses"], produces=route_spec["produces"], operationId=route_spec["operationId"], summary=route_spec["summary"], description=route_spec["description"], tags=route_spec["tags"], ) ), ) p = get_paths(spec)["/pet/{petId}"]["get"] assert p["parameters"] == route_spec["parameters"] assert p["responses"] == route_spec["responses"] assert p["operationId"] == route_spec["operationId"] assert p["summary"] == route_spec["summary"] assert p["description"] == route_spec["description"] assert p["tags"] == route_spec["tags"] def test_paths_maintain_order(self, spec): spec.path(path="/path1") spec.path(path="/path2") spec.path(path="/path3") spec.path(path="/path4") assert list(spec.to_dict()["paths"].keys()) == [ "/path1", "/path2", "/path3", "/path4", ] def test_path_is_chainable(self, spec): spec.path(path="/path1").path("/path2") assert list(spec.to_dict()["paths"].keys()) == ["/path1", "/path2"] def test_path_methods_maintain_order(self, spec): methods = ["get", "post", "put", "patch", "delete", "head", "options"] for method in methods: spec.path(path="/path", operations={method: {}}) assert list(spec.to_dict()["paths"]["/path"]) == methods def test_path_merges_paths(self, spec): """Test that adding a second HTTP method to an existing path performs a merge operation instead of an overwrite""" path = "/pet/{petId}" route_spec = self.paths[path]["get"] spec.path(path=path, operations=dict(get=route_spec)) spec.path( path=path, operations=dict( put=dict( parameters=route_spec["parameters"], responses=route_spec["responses"], produces=route_spec["produces"], operationId="updatePet", summary="Updates an existing Pet", description="Use this method to make changes to Pet `petId`", tags=route_spec["tags"], ) ), ) p = get_paths(spec)[path] assert "get" in p assert "put" in p @pytest.mark.parametrize("openapi_version", ("2.0", "3.0.0")) def test_path_called_twice_with_same_operations_parameters(self, openapi_version): """Test calling path twice with same operations or parameters operations and parameters being mutated by clean_operations and plugin helpers should not make path fail on second call """ class TestPlugin(BasePlugin): def path_helper(self, path, operations, parameters, **kwargs): """Mutate operations and parameters""" operations.update({"post": {"responses": {"201": "201ResponseRef"}}}) parameters.append("ParamRef_3") return path spec = APISpec( title="Swagger Petstore", version="1.0.0", openapi_version=openapi_version, plugins=[TestPlugin()], ) path = "/pet/{petId}" parameters = ["ParamRef_1"] operation = { "parameters": ["ParamRef_2"], "responses": {"200": "200ResponseRef"}, } spec.path(path=path, operations={"get": operation}, parameters=parameters) spec.path(path=path, operations={"put": operation}, parameters=parameters) operations = (get_paths(spec))[path] assert ( operations["get"] == operations["put"] == { "parameters": [build_ref(spec, "parameter", "ParamRef_2")], "responses": {"200": build_ref(spec, "response", "200ResponseRef")}, } ) assert operations["parameters"] == [ build_ref(spec, "parameter", "ParamRef_1"), build_ref(spec, "parameter", "ParamRef_3"), ] def test_path_ensures_path_parameters_required(self, spec): path = "/pet/{petId}" spec.path( path=path, operations=dict(put=dict(parameters=[{"name": "petId", "in": "path"}])), ) assert get_paths(spec)[path]["put"]["parameters"][0]["required"] is True def test_path_with_no_path_raises_error(self, spec): message = "Path template is not specified" with pytest.raises(APISpecError, match=message): spec.path() def test_path_summary_description(self, spec): summary = "Operations on a Pet" description = "Operations on a Pet identified by its ID" spec.path(path="/pet/{petId}", summary=summary, description=description) p = get_paths(spec)["/pet/{petId}"] assert p["summary"] == summary assert p["description"] == description def test_path_resolves_parameter(self, spec): route_spec = self.paths["/pet/{petId}"]["get"] spec.components.parameter("test_parameter", "path", route_spec["parameters"][0]) spec.path( path="/pet/{petId}", operations={"get": {"parameters": ["test_parameter"]}} ) p = get_paths(spec)["/pet/{petId}"]["get"] assert p["parameters"][0] == build_ref(spec, "parameter", "test_parameter") @pytest.mark.parametrize( "parameters", ([{"name": "petId"}], [{"in": "path"}]), # missing "in" # missing "name" ) def test_path_invalid_parameter(self, spec, parameters): path = "/pet/{petId}" with pytest.raises(InvalidParameterError): spec.path(path=path, operations=dict(put={}, get={}), parameters=parameters) def test_parameter_duplicate(self, spec): spec.path( path="/pet/{petId}", operations={ "get": { "parameters": [ {"name": "petId", "in": "path"}, {"name": "petId", "in": "query"}, ] } }, ) with pytest.raises(DuplicateParameterError): spec.path( path="/pet/{petId}", operations={ "get": { "parameters": [ {"name": "petId", "in": "path"}, {"name": "petId", "in": "path"}, ] } }, ) def test_global_parameters(self, spec): path = "/pet/{petId}" route_spec = self.paths["/pet/{petId}"]["get"] spec.components.parameter("test_parameter", "path", route_spec["parameters"][0]) spec.path( path=path, operations=dict(put={}, get={}), parameters=[{"name": "petId", "in": "path"}, "test_parameter"], ) assert get_paths(spec)[path]["parameters"] == [ {"name": "petId", "in": "path", "required": True}, build_ref(spec, "parameter", "test_parameter"), ] def test_global_parameter_duplicate(self, spec): path = "/pet/{petId}" spec.path( path=path, operations=dict(put={}, get={}), parameters=[ {"name": "petId", "in": "path"}, {"name": "petId", "in": "query"}, ], ) assert get_paths(spec)[path]["parameters"] == [ {"name": "petId", "in": "path", "required": True}, {"name": "petId", "in": "query"}, ] with pytest.raises(DuplicateParameterError): spec.path( path=path, operations=dict(put={}, get={}), parameters=[ {"name": "petId", "in": "path"}, {"name": "petId", "in": "path"}, "test_parameter", ], ) def test_path_resolves_response(self, spec): route_spec = self.paths["/pet/{petId}"]["get"] spec.components.response("test_response", route_spec["responses"]["200"]) spec.path( path="/pet/{petId}", operations={"get": {"responses": {"200": "test_response"}}}, ) p = get_paths(spec)["/pet/{petId}"]["get"] assert p["responses"]["200"] == build_ref(spec, "response", "test_response") def test_path_response_with_HTTPStatus_code(self, spec): code = HTTPStatus(200) spec.path( path="/pet/{petId}", operations={"get": {"responses": {code: "test_response"}}}, ) assert "200" in get_paths(spec)["/pet/{petId}"]["get"]["responses"] def test_path_response_with_status_code_range(self, spec, recwarn): status_code = "2XX" spec.path( path="/pet/{petId}", operations={"get": {"responses": {status_code: "test_response"}}}, ) if spec.openapi_version.major < 3: assert len(recwarn) == 1 assert recwarn.pop(UserWarning) assert status_code in get_paths(spec)["/pet/{petId}"]["get"]["responses"] def test_path_check_invalid_http_method(self, spec): spec.path("/pet/{petId}", operations={"get": {}}) spec.path("/pet/{petId}", operations={"x-dummy": {}}) message = "One or more HTTP methods are invalid" with pytest.raises(APISpecError, match=message): spec.path("/pet/{petId}", operations={"dummy": {}}) def test_path_resolve_response_schema(self, spec): schema = {"schema": "PetSchema"} if spec.openapi_version.major >= 3: schema = {"content": {"application/json": schema}} spec.path("/pet/{petId}", operations={"get": {"responses": {"200": schema}}}) resp = get_paths(spec)["/pet/{petId}"]["get"]["responses"]["200"] if spec.openapi_version.major < 3: schema = resp["schema"] else: schema = resp["content"]["application/json"]["schema"] assert schema == build_ref(spec, "schema", "PetSchema") # callbacks only exists in OAS 3 @pytest.mark.parametrize("spec", ("3.0.0",), indirect=True) def test_path_resolve_callbacks(self, spec): parameter = {"name": "petId", "in": "query", "schema": "PetSchema"} spec.path( "/pet/{petId}", operations={ "get": { "callbacks": { "onEvent": { "/callback/{petId}": { "post": { "parameters": [parameter], "requestBody": { "content": { "application/json": {"schema": "PetSchema"} } }, "responses": { "200": { "content": { "application/json": { "schema": "PetSchema" } } } }, } } } }, } }, ) path = get_paths(spec)["/pet/{petId}"] schema_ref = build_ref(spec, "schema", "PetSchema") callback_op = path["get"]["callbacks"]["onEvent"]["/callback/{petId}"]["post"] assert callback_op["parameters"][0]["schema"] == schema_ref assert ( callback_op["requestBody"]["content"]["application/json"]["schema"] == schema_ref ) assert ( callback_op["responses"]["200"]["content"]["application/json"]["schema"] == schema_ref ) # requestBody only exists in OAS 3 @pytest.mark.parametrize("spec", ("3.0.0",), indirect=True) def test_path_resolve_request_body(self, spec): spec.path( "/pet/{petId}", operations={ "get": { "requestBody": { "content": {"application/json": {"schema": "PetSchema"}} } } }, ) assert get_paths(spec)["/pet/{petId}"]["get"]["requestBody"]["content"][ "application/json" ]["schema"] == build_ref(spec, "schema", "PetSchema") # "headers" components section only exists in OAS 3 @pytest.mark.parametrize("spec", ("3.0.0",), indirect=True) def test_path_resolve_response_header(self, spec): response = {"headers": {"header_1": "Header_1"}} spec.path("/pet/{petId}", operations={"get": {"responses": {"200": response}}}) resp = get_paths(spec)["/pet/{petId}"]["get"]["responses"]["200"] header_1 = resp["headers"]["header_1"] assert header_1 == build_ref(spec, "header", "Header_1") # "headers" components section only exists in OAS 3 @pytest.mark.parametrize("spec", ("3.0.0",), indirect=True) def test_path_resolve_response_header_schema(self, spec): response = {"headers": {"header_1": {"name": "Pet", "schema": "PetSchema"}}} spec.path("/pet/{petId}", operations={"get": {"responses": {"200": response}}}) resp = get_paths(spec)["/pet/{petId}"]["get"]["responses"]["200"] header_1 = resp["headers"]["header_1"] assert header_1["schema"] == build_ref(spec, "schema", "PetSchema") # "headers" components section only exists in OAS 3 @pytest.mark.parametrize("spec", ("3.0.0",), indirect=True) def test_path_resolve_response_header_examples(self, spec): response = { "headers": { "header_1": {"name": "Pet", "examples": {"example_1": "Example_1"}} } } spec.path("/pet/{petId}", operations={"get": {"responses": {"200": response}}}) resp = get_paths(spec)["/pet/{petId}"]["get"]["responses"]["200"] header_1 = resp["headers"]["header_1"] assert header_1["examples"]["example_1"] == build_ref( spec, "example", "Example_1" ) # "examples" components section only exists in OAS 3 @pytest.mark.parametrize("spec", ("3.0.0",), indirect=True) def test_path_resolve_response_examples(self, spec): response = { "content": {"application/json": {"examples": {"example_1": "Example_1"}}} } spec.path("/pet/{petId}", operations={"get": {"responses": {"200": response}}}) resp = get_paths(spec)["/pet/{petId}"]["get"]["responses"]["200"] example_1 = resp["content"]["application/json"]["examples"]["example_1"] assert example_1 == build_ref(spec, "example", "Example_1") # "examples" components section only exists in OAS 3 @pytest.mark.parametrize("spec", ("3.0.0",), indirect=True) def test_path_resolve_request_body_examples(self, spec): request_body = { "content": {"application/json": {"examples": {"example_1": "Example_1"}}} } spec.path("/pet/{petId}", operations={"get": {"requestBody": request_body}}) reqbdy = get_paths(spec)["/pet/{petId}"]["get"]["requestBody"] example_1 = reqbdy["content"]["application/json"]["examples"]["example_1"] assert example_1 == build_ref(spec, "example", "Example_1") # "examples" components section only exists in OAS 3 @pytest.mark.parametrize("spec", ("3.0.0",), indirect=True) def test_path_resolve_parameter_examples(self, spec): parameter = { "name": "test", "in": "query", "examples": {"example_1": "Example_1"}, } spec.path("/pet/{petId}", operations={"get": {"parameters": [parameter]}}) param = get_paths(spec)["/pet/{petId}"]["get"]["parameters"][0] example_1 = param["examples"]["example_1"] assert example_1 == build_ref(spec, "example", "Example_1") def test_path_resolve_parameter_schemas(self, spec): parameter = {"name": "test", "in": "query", "schema": "PetSchema"} spec.path("/pet/{petId}", operations={"get": {"parameters": [parameter]}}) param = get_paths(spec)["/pet/{petId}"]["get"]["parameters"][0] assert param["schema"] == build_ref(spec, "schema", "PetSchema") def test_path_resolve_refs_in_response_schema(self, spec): if spec.openapi_version.major >= 3: schema = {"content": {"application/json": {"schema": self.REFS_SCHEMA}}} else: schema = {"schema": self.REFS_SCHEMA} spec.path("/pet/{petId}", operations={"get": {"responses": {"200": schema}}}) resp = get_paths(spec)["/pet/{petId}"]["get"]["responses"]["200"] if spec.openapi_version.major < 3: schema = resp["schema"] else: schema = resp["content"]["application/json"]["schema"] self.assert_schema_refs(spec, schema) def test_path_resolve_refs_in_parameter_schema(self, spec): schema = copy.copy({"schema": self.REFS_SCHEMA}) schema["in"] = "query" schema["name"] = "test" spec.path("/pet/{petId}", operations={"get": {"parameters": [schema]}}) schema = get_paths(spec)["/pet/{petId}"]["get"]["parameters"][0]["schema"] self.assert_schema_refs(spec, schema) # requestBody only exists in OAS 3 @pytest.mark.parametrize("spec", ("3.0.0",), indirect=True) def test_path_resolve_refs_in_request_body_schema(self, spec): schema = {"content": {"application/json": {"schema": self.REFS_SCHEMA}}} spec.path("/pet/{petId}", operations={"get": {"responses": {"200": schema}}}) resp = get_paths(spec)["/pet/{petId}"]["get"]["responses"]["200"] schema = resp["content"]["application/json"]["schema"] self.assert_schema_refs(spec, schema) class TestPlugins: @staticmethod def make_test_plugin(return_none=False): class TestPlugin(BasePlugin): """Test Plugin return_none allows to check plugin helpers returning ``None`` Inputs are mutated to allow testing only a copy is passed. """ def schema_helper(self, name, definition, **kwargs): definition.pop("dummy", None) if not return_none: return {"properties": {"name": {"type": "string"}}} def parameter_helper(self, parameter, **kwargs): parameter.pop("dummy", None) if not return_none: return {"description": "some parameter"} def response_helper(self, response, **kwargs): response.pop("dummy", None) if not return_none: return {"description": "42"} def header_helper(self, header, **kwargs): header.pop("dummy", None) if not return_none: return {"description": "some header"} def path_helper(self, path, operations, parameters, **kwargs): if not return_none: if path == "/path_1": operations.update({"get": {"responses": {"200": {}}}}) parameters.append({"name": "page", "in": "query"}) return "/path_1_modified" def operation_helper(self, path, operations, **kwargs): if path == "/path_2": operations["post"] = {"responses": {"201": {}}} return TestPlugin() @pytest.mark.parametrize("openapi_version", ("2.0", "3.0.0")) @pytest.mark.parametrize("return_none", (True, False)) def test_plugin_schema_helper_is_used(self, openapi_version, return_none): spec = APISpec( title="Swagger Petstore", version="1.0.0", openapi_version=openapi_version, plugins=(self.make_test_plugin(return_none),), ) schema = {"dummy": "dummy"} spec.components.schema("Pet", schema) definitions = get_schemas(spec) if return_none: assert definitions["Pet"] == {} else: assert definitions["Pet"] == {"properties": {"name": {"type": "string"}}} # Check original schema is not modified assert schema == {"dummy": "dummy"} @pytest.mark.parametrize("openapi_version", ("2.0", "3.0.0")) @pytest.mark.parametrize("return_none", (True, False)) def test_plugin_parameter_helper_is_used(self, openapi_version, return_none): spec = APISpec( title="Swagger Petstore", version="1.0.0", openapi_version=openapi_version, plugins=(self.make_test_plugin(return_none),), ) parameter = {"dummy": "dummy"} spec.components.parameter("Pet", "body", parameter) parameters = get_parameters(spec) if return_none: assert parameters["Pet"] == {"in": "body", "name": "Pet"} else: assert parameters["Pet"] == { "in": "body", "name": "Pet", "description": "some parameter", } # Check original parameter is not modified assert parameter == {"dummy": "dummy"} @pytest.mark.parametrize("openapi_version", ("2.0", "3.0.0")) @pytest.mark.parametrize("return_none", (True, False)) def test_plugin_response_helper_is_used(self, openapi_version, return_none): spec = APISpec( title="Swagger Petstore", version="1.0.0", openapi_version=openapi_version, plugins=(self.make_test_plugin(return_none),), ) response = {"dummy": "dummy"} spec.components.response("Pet", response) responses = get_responses(spec) if return_none: assert responses["Pet"] == {} else: assert responses["Pet"] == {"description": "42"} # Check original response is not modified assert response == {"dummy": "dummy"} @pytest.mark.parametrize("openapi_version", ("3.0.0",)) @pytest.mark.parametrize("return_none", (True, False)) def test_plugin_header_helper_is_used(self, openapi_version, return_none): spec = APISpec( title="Swagger Petstore", version="1.0.0", openapi_version=openapi_version, plugins=(self.make_test_plugin(return_none),), ) header = {"dummy": "dummy"} spec.components.header("Pet", header) headers = get_headers(spec) if return_none: assert headers["Pet"] == {} else: assert headers["Pet"] == { "description": "some header", } # Check original header is not modified assert header == {"dummy": "dummy"} @pytest.mark.parametrize("openapi_version", ("2.0", "3.0.0")) @pytest.mark.parametrize("return_none", (True, False)) def test_plugin_path_helper_is_used(self, openapi_version, return_none): spec = APISpec( title="Swagger Petstore", version="1.0.0", openapi_version=openapi_version, plugins=(self.make_test_plugin(return_none),), ) spec.path("/path_1") paths = get_paths(spec) assert len(paths) == 1 if return_none: assert paths["/path_1"] == {} else: assert paths["/path_1_modified"] == { "get": {"responses": {"200": {}}}, "parameters": [{"in": "query", "name": "page"}], } @pytest.mark.parametrize("openapi_version", ("2.0", "3.0.0")) def test_plugin_operation_helper_is_used(self, openapi_version): spec = APISpec( title="Swagger Petstore", version="1.0.0", openapi_version=openapi_version, plugins=(self.make_test_plugin(),), ) spec.path("/path_2", operations={"post": {"responses": {"200": {}}}}) paths = get_paths(spec) assert len(paths) == 1 assert paths["/path_2"] == {"post": {"responses": {"201": {}}}} class TestPluginsOrder: class OrderedPlugin(BasePlugin): def __init__(self, index, output): super().__init__() self.index = index self.output = output def path_helper(self, path, operations, **kwargs): self.output.append(f"plugin_{self.index}_path") def operation_helper(self, path, operations, **kwargs): self.output.append(f"plugin_{self.index}_operations") def test_plugins_order(self): """Test plugins execution order in APISpec.path - All path helpers are called, then all operation helpers, then all response helpers. - At each step, helpers are executed in the order the plugins are passed to APISpec. """ output = [] spec = APISpec( title="Swagger Petstore", version="1.0.0", openapi_version="3.0.0", plugins=(self.OrderedPlugin(1, output), self.OrderedPlugin(2, output)), ) spec.path("/path", operations={"get": {"responses": {200: {}}}}) assert output == [ "plugin_1_path", "plugin_2_path", "plugin_1_operations", "plugin_2_operations", ] apispec-6.8.1/tests/test_ext_marshmallow.py000066400000000000000000001473261473713346600211660ustar00rootroot00000000000000import json import pytest from marshmallow import Schema from marshmallow.fields import DateTime, Dict, Field, List, Nested, String, TimeDelta from apispec import APISpec from apispec.exceptions import APISpecError from apispec.ext.marshmallow import MarshmallowPlugin, common from .schemas import ( AnalysisSchema, AnalysisWithListSchema, DefaultValuesSchema, OrderedSchema, PatternedObjectSchema, PetSchema, RunSchema, SampleSchema, SelfReferencingSchema, ) from .utils import ( build_ref, get_headers, get_parameters, get_paths, get_responses, get_schemas, ) class TestDefinitionHelper: @pytest.mark.parametrize("schema", [PetSchema, PetSchema()]) def test_can_use_schema_as_definition(self, spec, schema): spec.components.schema("Pet", schema=schema) definitions = get_schemas(spec) props = definitions["Pet"]["properties"] assert props["id"]["type"] == "integer" assert props["name"]["type"] == "string" def test_schema_helper_without_schema(self, spec): spec.components.schema("Pet", {"properties": {"key": {"type": "integer"}}}) definitions = get_schemas(spec) assert definitions["Pet"]["properties"] == {"key": {"type": "integer"}} @pytest.mark.parametrize("schema", [AnalysisSchema, AnalysisSchema()]) def test_resolve_schema_dict_auto_reference(self, schema): def resolver(schema): schema_cls = common.resolve_schema_cls(schema) return schema_cls.__name__ spec = APISpec( title="Test auto-reference", version="0.1", openapi_version="2.0", plugins=(MarshmallowPlugin(schema_name_resolver=resolver),), ) with pytest.raises(KeyError): get_schemas(spec) spec.components.schema("analysis", schema=schema) spec.path( "/test", operations={ "get": { "responses": { "200": {"schema": build_ref(spec, "schema", "analysis")} } } }, ) definitions = get_schemas(spec) assert 3 == len(definitions) assert "analysis" in definitions assert "SampleSchema" in definitions assert "RunSchema" in definitions @pytest.mark.parametrize( "schema", [AnalysisWithListSchema, AnalysisWithListSchema()] ) def test_resolve_schema_dict_auto_reference_in_list(self, schema): def resolver(schema): schema_cls = common.resolve_schema_cls(schema) return schema_cls.__name__ spec = APISpec( title="Test auto-reference", version="0.1", openapi_version="2.0", plugins=(MarshmallowPlugin(schema_name_resolver=resolver),), ) with pytest.raises(KeyError): get_schemas(spec) spec.components.schema("analysis", schema=schema) spec.path( "/test", operations={ "get": { "responses": { "200": {"schema": build_ref(spec, "schema", "analysis")} } } }, ) definitions = get_schemas(spec) assert 3 == len(definitions) assert "analysis" in definitions assert "SampleSchema" in definitions assert "RunSchema" in definitions @pytest.mark.parametrize("schema", [AnalysisSchema, AnalysisSchema()]) def test_resolve_schema_dict_auto_reference_return_none(self, schema): def resolver(schema): return None spec = APISpec( title="Test auto-reference", version="0.1", openapi_version="2.0", plugins=(MarshmallowPlugin(schema_name_resolver=resolver),), ) with pytest.raises(KeyError): get_schemas(spec) with pytest.raises( APISpecError, match="Name resolver returned None for schema" ): spec.components.schema("analysis", schema=schema) @pytest.mark.parametrize("schema", [AnalysisSchema, AnalysisSchema()]) def test_warning_when_schema_added_twice(self, spec, schema): spec.components.schema("Analysis", schema=schema) with pytest.warns(UserWarning, match="has already been added to the spec"): spec.components.schema("DuplicateAnalysis", schema=schema) def test_schema_instances_with_different_modifiers_added(self, spec): class MultiModifierSchema(Schema): pet_unmodified = Nested(PetSchema) pet_exclude = Nested(PetSchema, exclude=("name",)) spec.components.schema("Pet", schema=PetSchema()) spec.components.schema("Pet_Exclude", schema=PetSchema(exclude=("name",))) spec.components.schema("MultiModifierSchema", schema=MultiModifierSchema) definitions = get_schemas(spec) pet_unmodified_ref = definitions["MultiModifierSchema"]["properties"][ "pet_unmodified" ] assert pet_unmodified_ref == build_ref(spec, "schema", "Pet") pet_exclude = definitions["MultiModifierSchema"]["properties"]["pet_exclude"] assert pet_exclude == build_ref(spec, "schema", "Pet_Exclude") def test_schema_instance_with_different_modifers_custom_resolver(self, recwarn): class MultiModifierSchema(Schema): pet_unmodified = Nested(PetSchema) pet_exclude = Nested(PetSchema(partial=True)) def resolver(schema): schema_instance = common.resolve_schema_instance(schema) prefix = "Partial-" if schema_instance.partial else "" schema_cls = common.resolve_schema_cls(schema) name = prefix + schema_cls.__name__ if name.endswith("Schema"): return name[:-6] or name return name spec = APISpec( title="Test Custom Resolver for Partial", version="0.1", openapi_version="2.0", plugins=(MarshmallowPlugin(schema_name_resolver=resolver),), ) spec.components.schema("NameClashSchema", schema=MultiModifierSchema) assert not recwarn def test_schema_with_clashing_names(self, spec): class Pet(PetSchema): another_field = String() class NameClashSchema(Schema): pet_1 = Nested(PetSchema) pet_2 = Nested(Pet) with pytest.warns( UserWarning, match="Multiple schemas resolved to the name Pet" ): spec.components.schema("NameClashSchema", schema=NameClashSchema) definitions = get_schemas(spec) assert "Pet" in definitions assert "Pet1" in definitions def test_resolve_nested_schema_many_true_resolver_return_none(self): def resolver(schema): return None class PetFamilySchema(Schema): pets_1 = Nested(PetSchema, many=True) pets_2 = List(Nested(PetSchema)) spec = APISpec( title="Test auto-reference", version="0.1", openapi_version="2.0", plugins=(MarshmallowPlugin(schema_name_resolver=resolver),), ) spec.components.schema("PetFamily", schema=PetFamilySchema) props = get_schemas(spec)["PetFamily"]["properties"] pets_1 = props["pets_1"] pets_2 = props["pets_2"] assert pets_1["type"] == pets_2["type"] == "array" class TestComponentParameterHelper: @pytest.mark.parametrize("schema", [PetSchema, PetSchema()]) def test_can_use_schema_in_parameter(self, spec, schema): param = {"schema": schema} spec.components.parameter("Pet", "body", param) parameter = get_parameters(spec)["Pet"] assert parameter["in"] == "body" reference = parameter["schema"] assert reference == build_ref(spec, "schema", "Pet") resolved_schema = spec.components.schemas["Pet"] assert resolved_schema["properties"]["name"]["type"] == "string" assert resolved_schema["properties"]["password"]["type"] == "string" assert resolved_schema["properties"]["id"]["type"] == "integer" @pytest.mark.parametrize("spec", ("3.0.0",), indirect=True) @pytest.mark.parametrize("schema", [PetSchema, PetSchema()]) def test_can_use_schema_in_parameter_with_content(self, spec, schema): param = {"content": {"application/json": {"schema": schema}}} spec.components.parameter("Pet", "body", param) parameter = get_parameters(spec)["Pet"] assert parameter["in"] == "body" reference = parameter["content"]["application/json"]["schema"] assert reference == build_ref(spec, "schema", "Pet") resolved_schema = spec.components.schemas["Pet"] assert resolved_schema["properties"]["name"]["type"] == "string" assert resolved_schema["properties"]["password"]["type"] == "string" assert resolved_schema["properties"]["id"]["type"] == "integer" class TestComponentResponseHelper: @pytest.mark.parametrize("schema", [PetSchema, PetSchema()]) def test_can_use_schema_in_response(self, spec, schema): if spec.openapi_version.major < 3: resp = {"schema": schema} else: resp = {"content": {"application/json": {"schema": schema}}} spec.components.response("GetPetOk", resp) response = get_responses(spec)["GetPetOk"] if spec.openapi_version.major < 3: reference = response["schema"] else: reference = response["content"]["application/json"]["schema"] assert reference == build_ref(spec, "schema", "Pet") resolved_schema = spec.components.schemas["Pet"] assert resolved_schema["properties"]["id"]["type"] == "integer" assert resolved_schema["properties"]["name"]["type"] == "string" assert resolved_schema["properties"]["password"]["type"] == "string" @pytest.mark.parametrize("schema", [PetSchema, PetSchema()]) def test_can_use_schema_in_response_header(self, spec, schema): resp = {"headers": {"PetHeader": {"schema": schema}}} spec.components.response("GetPetOk", resp) response = get_responses(spec)["GetPetOk"] reference = response["headers"]["PetHeader"]["schema"] assert reference == build_ref(spec, "schema", "Pet") resolved_schema = spec.components.schemas["Pet"] assert resolved_schema["properties"]["id"]["type"] == "integer" assert resolved_schema["properties"]["name"]["type"] == "string" assert resolved_schema["properties"]["password"]["type"] == "string" @pytest.mark.parametrize("spec", ("3.0.0",), indirect=True) def test_content_without_schema(self, spec): resp = {"content": {"application/json": {"example": {"name": "Example"}}}} spec.components.response("GetPetOk", resp) response = get_responses(spec)["GetPetOk"] assert response == resp class TestComponentHeaderHelper: @pytest.mark.parametrize("spec", ("3.0.0",), indirect=True) @pytest.mark.parametrize("schema", [PetSchema, PetSchema()]) def test_can_use_schema_in_header(self, spec, schema): param = {"schema": schema} spec.components.header("Pet", param) header = get_headers(spec)["Pet"] reference = header["schema"] assert reference == build_ref(spec, "schema", "Pet") resolved_schema = spec.components.schemas["Pet"] assert resolved_schema["properties"]["name"]["type"] == "string" assert resolved_schema["properties"]["password"]["type"] == "string" assert resolved_schema["properties"]["id"]["type"] == "integer" class TestCustomField: def test_can_use_custom_field_decorator(self, spec_fixture): class CustomNameA(Field): pass spec_fixture.marshmallow_plugin.map_to_openapi_type(CustomNameA, DateTime) class CustomNameB(Field): pass spec_fixture.marshmallow_plugin.map_to_openapi_type( CustomNameB, "integer", "int32" ) class BadCustomField(Field): pass with pytest.raises(TypeError): spec_fixture.marshmallow_plugin.map_to_openapi_type( BadCustomField, "integer" ) class CustomPetASchema(PetSchema): name = CustomNameA() class CustomPetBSchema(PetSchema): name = CustomNameB() spec_fixture.spec.components.schema("Pet", schema=PetSchema) spec_fixture.spec.components.schema("CustomPetA", schema=CustomPetASchema) spec_fixture.spec.components.schema("CustomPetB", schema=CustomPetBSchema) props_0 = get_schemas(spec_fixture.spec)["Pet"]["properties"] props_a = get_schemas(spec_fixture.spec)["CustomPetA"]["properties"] props_b = get_schemas(spec_fixture.spec)["CustomPetB"]["properties"] assert props_0["name"]["type"] == "string" assert "format" not in props_0["name"] assert props_a["name"]["type"] == "string" assert props_a["name"]["format"] == "date-time" assert props_b["name"]["type"] == "integer" assert props_b["name"]["format"] == "int32" def get_nested_schema(schema, field_name): try: return schema._declared_fields[field_name]._schema except AttributeError: return schema._declared_fields[field_name]._Nested__schema class TestOperationHelper: @pytest.fixture def make_pet_callback_spec(self, spec_fixture): def _make_pet_spec(operations): spec_fixture.spec.path( path="/pet", operations={ "post": {"callbacks": {"petEvent": {"petCallbackUrl": operations}}} }, ) return spec_fixture return _make_pet_spec @pytest.mark.parametrize( "pet_schema", (PetSchema, PetSchema(), PetSchema(many=True), "tests.schemas.PetSchema"), ) @pytest.mark.parametrize("spec_fixture", ("2.0",), indirect=True) def test_schema_v2(self, spec_fixture, pet_schema): spec_fixture.spec.path( path="/pet", operations={ "get": { "responses": { 200: { "schema": pet_schema, "description": "successful operation", "headers": {"PetHeader": {"schema": pet_schema}}, } } } }, ) get = get_paths(spec_fixture.spec)["/pet"]["get"] if isinstance(pet_schema, Schema) and pet_schema.many is True: assert get["responses"]["200"]["schema"]["type"] == "array" schema_reference = get["responses"]["200"]["schema"]["items"] assert ( get["responses"]["200"]["headers"]["PetHeader"]["schema"]["type"] == "array" ) header_reference = get["responses"]["200"]["headers"]["PetHeader"][ "schema" ]["items"] else: schema_reference = get["responses"]["200"]["schema"] header_reference = get["responses"]["200"]["headers"]["PetHeader"]["schema"] assert schema_reference == build_ref(spec_fixture.spec, "schema", "Pet") assert header_reference == build_ref(spec_fixture.spec, "schema", "Pet") assert len(spec_fixture.spec.components.schemas) == 1 resolved_schema = spec_fixture.spec.components.schemas["Pet"] assert resolved_schema == spec_fixture.openapi.schema2jsonschema(PetSchema) assert get["responses"]["200"]["description"] == "successful operation" @pytest.mark.parametrize( "pet_schema", (PetSchema, PetSchema(), PetSchema(many=True), "tests.schemas.PetSchema"), ) @pytest.mark.parametrize("spec_fixture", ("3.0.0",), indirect=True) def test_schema_v3(self, spec_fixture, pet_schema): spec_fixture.spec.path( path="/pet", operations={ "get": { "responses": { 200: { "content": {"application/json": {"schema": pet_schema}}, "description": "successful operation", "headers": {"PetHeader": {"schema": pet_schema}}, } } } }, ) get = get_paths(spec_fixture.spec)["/pet"]["get"] if isinstance(pet_schema, Schema) and pet_schema.many is True: assert ( get["responses"]["200"]["content"]["application/json"]["schema"]["type"] == "array" ) schema_reference = get["responses"]["200"]["content"]["application/json"][ "schema" ]["items"] assert ( get["responses"]["200"]["headers"]["PetHeader"]["schema"]["type"] == "array" ) header_reference = get["responses"]["200"]["headers"]["PetHeader"][ "schema" ]["items"] else: schema_reference = get["responses"]["200"]["content"]["application/json"][ "schema" ] header_reference = get["responses"]["200"]["headers"]["PetHeader"]["schema"] assert schema_reference == build_ref(spec_fixture.spec, "schema", "Pet") assert header_reference == build_ref(spec_fixture.spec, "schema", "Pet") assert len(spec_fixture.spec.components.schemas) == 1 resolved_schema = spec_fixture.spec.components.schemas["Pet"] assert resolved_schema == spec_fixture.openapi.schema2jsonschema(PetSchema) assert get["responses"]["200"]["description"] == "successful operation" @pytest.mark.parametrize( "pet_schema", (PetSchema, PetSchema(), PetSchema(many=True), "tests.schemas.PetSchema"), ) @pytest.mark.parametrize("spec_fixture", ("3.0.0",), indirect=True) def test_callback_schema_v3(self, make_pet_callback_spec, pet_schema): spec_fixture = make_pet_callback_spec( { "get": { "responses": { "200": { "content": {"application/json": {"schema": pet_schema}}, "description": "successful operation", "headers": {"PetHeader": {"schema": pet_schema}}, } } } } ) p = get_paths(spec_fixture.spec)["/pet"] c = p["post"]["callbacks"]["petEvent"]["petCallbackUrl"] get = c["get"] if isinstance(pet_schema, Schema) and pet_schema.many is True: assert ( get["responses"]["200"]["content"]["application/json"]["schema"]["type"] == "array" ) schema_reference = get["responses"]["200"]["content"]["application/json"][ "schema" ]["items"] assert ( get["responses"]["200"]["headers"]["PetHeader"]["schema"]["type"] == "array" ) header_reference = get["responses"]["200"]["headers"]["PetHeader"][ "schema" ]["items"] else: schema_reference = get["responses"]["200"]["content"]["application/json"][ "schema" ] header_reference = get["responses"]["200"]["headers"]["PetHeader"]["schema"] assert schema_reference == build_ref(spec_fixture.spec, "schema", "Pet") assert header_reference == build_ref(spec_fixture.spec, "schema", "Pet") assert len(spec_fixture.spec.components.schemas) == 1 resolved_schema = spec_fixture.spec.components.schemas["Pet"] assert resolved_schema == spec_fixture.openapi.schema2jsonschema(PetSchema) assert get["responses"]["200"]["description"] == "successful operation" @pytest.mark.parametrize("spec_fixture", ("2.0",), indirect=True) def test_schema_expand_parameters_v2(self, spec_fixture): spec_fixture.spec.path( path="/pet", operations={ "get": {"parameters": [{"in": "query", "schema": PetSchema}]}, "post": { "parameters": [ { "in": "body", "description": "a pet schema", "required": True, "name": "pet", "schema": PetSchema, } ] }, }, ) p = get_paths(spec_fixture.spec)["/pet"] get = p["get"] assert get["parameters"] == spec_fixture.openapi.schema2parameters( PetSchema(), location="query" ) post = p["post"] assert post["parameters"] == spec_fixture.openapi.schema2parameters( PetSchema, location="body", required=True, name="pet", description="a pet schema", ) @pytest.mark.parametrize("spec_fixture", ("3.0.0",), indirect=True) def test_schema_expand_parameters_v3(self, spec_fixture): spec_fixture.spec.path( path="/pet", operations={ "get": {"parameters": [{"in": "query", "schema": PetSchema}]}, "post": { "requestBody": { "description": "a pet schema", "required": True, "content": {"application/json": {"schema": PetSchema}}, } }, }, ) p = get_paths(spec_fixture.spec)["/pet"] get = p["get"] assert get["parameters"] == spec_fixture.openapi.schema2parameters( PetSchema(), location="query" ) for parameter in get["parameters"]: description = parameter.get("description", False) assert description name = parameter["name"] assert description == PetSchema.description[name] post = p["post"] post_schema = spec_fixture.marshmallow_plugin.resolver.resolve_schema_dict( PetSchema ) assert ( post["requestBody"]["content"]["application/json"]["schema"] == post_schema ) assert post["requestBody"]["description"] == "a pet schema" assert post["requestBody"]["required"] @pytest.mark.parametrize("spec_fixture", ("3.0.0",), indirect=True) def test_callback_schema_expand_parameters_v3(self, make_pet_callback_spec): spec_fixture = make_pet_callback_spec( { "get": {"parameters": [{"in": "query", "schema": PetSchema}]}, "post": { "requestBody": { "description": "a pet schema", "required": True, "content": {"application/json": {"schema": PetSchema}}, } }, } ) p = get_paths(spec_fixture.spec)["/pet"] c = p["post"]["callbacks"]["petEvent"]["petCallbackUrl"] get = c["get"] assert get["parameters"] == spec_fixture.openapi.schema2parameters( PetSchema(), location="query" ) for parameter in get["parameters"]: description = parameter.get("description", False) assert description name = parameter["name"] assert description == PetSchema.description[name] post = c["post"] post_schema = spec_fixture.marshmallow_plugin.resolver.resolve_schema_dict( PetSchema ) assert ( post["requestBody"]["content"]["application/json"]["schema"] == post_schema ) assert post["requestBody"]["description"] == "a pet schema" assert post["requestBody"]["required"] @pytest.mark.parametrize("spec_fixture", ("2.0",), indirect=True) def test_schema_uses_ref_if_available_v2(self, spec_fixture): spec_fixture.spec.components.schema("Pet", schema=PetSchema) spec_fixture.spec.path( path="/pet", operations={"get": {"responses": {200: {"schema": PetSchema}}}} ) get = get_paths(spec_fixture.spec)["/pet"]["get"] assert get["responses"]["200"]["schema"] == build_ref( spec_fixture.spec, "schema", "Pet" ) @pytest.mark.parametrize("spec_fixture", ("3.0.0",), indirect=True) def test_schema_uses_ref_if_available_v3(self, spec_fixture): spec_fixture.spec.components.schema("Pet", schema=PetSchema) spec_fixture.spec.path( path="/pet", operations={ "get": { "responses": { 200: {"content": {"application/json": {"schema": PetSchema}}} } } }, ) get = get_paths(spec_fixture.spec)["/pet"]["get"] assert get["responses"]["200"]["content"]["application/json"][ "schema" ] == build_ref(spec_fixture.spec, "schema", "Pet") @pytest.mark.parametrize("spec_fixture", ("3.0.0",), indirect=True) def test_callback_schema_uses_ref_if_available_v3(self, make_pet_callback_spec): spec_fixture = make_pet_callback_spec( { "get": { "responses": { "200": {"content": {"application/json": {"schema": PetSchema}}} } } } ) p = get_paths(spec_fixture.spec)["/pet"] c = p["post"]["callbacks"]["petEvent"]["petCallbackUrl"] get = c["get"] assert get["responses"]["200"]["content"]["application/json"][ "schema" ] == build_ref(spec_fixture.spec, "schema", "Pet") def test_schema_uses_ref_if_available_name_resolver_returns_none_v2(self): def resolver(schema): return None spec = APISpec( title="Test auto-reference", version="0.1", openapi_version="2.0", plugins=(MarshmallowPlugin(schema_name_resolver=resolver),), ) spec.components.schema("Pet", schema=PetSchema) spec.path( path="/pet", operations={"get": {"responses": {200: {"schema": PetSchema}}}} ) get = get_paths(spec)["/pet"]["get"] assert get["responses"]["200"]["schema"] == build_ref(spec, "schema", "Pet") def test_schema_uses_ref_if_available_name_resolver_returns_none_v3(self): def resolver(schema): return None spec = APISpec( title="Test auto-reference", version="0.1", openapi_version="3.0.0", plugins=(MarshmallowPlugin(schema_name_resolver=resolver),), ) spec.components.schema("Pet", schema=PetSchema) spec.path( path="/pet", operations={ "get": { "responses": { 200: {"content": {"application/json": {"schema": PetSchema}}} } } }, ) get = get_paths(spec)["/pet"]["get"] assert get["responses"]["200"]["content"]["application/json"][ "schema" ] == build_ref(spec, "schema", "Pet") @pytest.mark.parametrize("spec_fixture", ("2.0",), indirect=True) def test_schema_resolver_allof_v2(self, spec_fixture): spec_fixture.spec.components.schema("Pet", schema=PetSchema) spec_fixture.spec.components.schema("Sample", schema=SampleSchema) spec_fixture.spec.path( path="/pet", operations={ "get": { "responses": {200: {"schema": {"allOf": [PetSchema, SampleSchema]}}} } }, ) get = get_paths(spec_fixture.spec)["/pet"]["get"] assert get["responses"]["200"]["schema"] == { "allOf": [ build_ref(spec_fixture.spec, "schema", "Pet"), build_ref(spec_fixture.spec, "schema", "Sample"), ] } @pytest.mark.parametrize("spec_fixture", ("3.0.0",), indirect=True) @pytest.mark.parametrize("combinator", ["oneOf", "anyOf", "allOf"]) def test_schema_resolver_oneof_anyof_allof_v3(self, spec_fixture, combinator): spec_fixture.spec.components.schema("Pet", schema=PetSchema) spec_fixture.spec.path( path="/pet", operations={ "get": { "responses": { 200: { "content": { "application/json": { "schema": {combinator: [PetSchema, SampleSchema]} } } } } } }, ) get = get_paths(spec_fixture.spec)["/pet"]["get"] assert get["responses"]["200"]["content"]["application/json"]["schema"] == { combinator: [ build_ref(spec_fixture.spec, "schema", "Pet"), build_ref(spec_fixture.spec, "schema", "Sample"), ] } @pytest.mark.parametrize("spec_fixture", ("2.0",), indirect=True) def test_schema_resolver_not_v2(self, spec_fixture): spec_fixture.spec.components.schema("Pet", schema=PetSchema) spec_fixture.spec.path( path="/pet", operations={"get": {"responses": {200: {"schema": {"not": PetSchema}}}}}, ) get = get_paths(spec_fixture.spec)["/pet"]["get"] assert get["responses"]["200"]["schema"] == { "not": build_ref(spec_fixture.spec, "schema", "Pet"), } @pytest.mark.parametrize("spec_fixture", ("3.0.0",), indirect=True) def test_schema_resolver_not_v3(self, spec_fixture): spec_fixture.spec.components.schema("Pet", schema=PetSchema) spec_fixture.spec.path( path="/pet", operations={ "get": { "responses": { 200: { "content": { "application/json": {"schema": {"not": PetSchema}} } } } } }, ) get = get_paths(spec_fixture.spec)["/pet"]["get"] assert get["responses"]["200"]["content"]["application/json"]["schema"] == { "not": build_ref(spec_fixture.spec, "schema", "Pet"), } @pytest.mark.parametrize( "pet_schema", (PetSchema, PetSchema(), "tests.schemas.PetSchema"), ) def test_schema_name_resolver_returns_none_v2(self, pet_schema): def resolver(schema): return None spec = APISpec( title="Test resolver returns None", version="0.1", openapi_version="2.0", plugins=(MarshmallowPlugin(schema_name_resolver=resolver),), ) spec.path( path="/pet", operations={"get": {"responses": {200: {"schema": pet_schema}}}}, ) get = get_paths(spec)["/pet"]["get"] assert "properties" in get["responses"]["200"]["schema"] @pytest.mark.parametrize( "pet_schema", (PetSchema, PetSchema(), "tests.schemas.PetSchema"), ) def test_schema_name_resolver_returns_none_v3(self, pet_schema): def resolver(schema): return None spec = APISpec( title="Test resolver returns None", version="0.1", openapi_version="3.0.0", plugins=(MarshmallowPlugin(schema_name_resolver=resolver),), ) spec.path( path="/pet", operations={ "get": { "responses": { 200: {"content": {"application/json": {"schema": pet_schema}}} } } }, ) get = get_paths(spec)["/pet"]["get"] assert ( "properties" in get["responses"]["200"]["content"]["application/json"]["schema"] ) def test_callback_schema_uses_ref_if_available_name_resolver_returns_none_v3(self): def resolver(schema): return None spec = APISpec( title="Test auto-reference", version="0.1", openapi_version="3.0.0", plugins=(MarshmallowPlugin(schema_name_resolver=resolver),), ) spec.components.schema("Pet", schema=PetSchema) spec.path( path="/pet", operations={ "post": { "callbacks": { "petEvent": { "petCallbackUrl": { "get": { "responses": { "200": { "content": { "application/json": { "schema": PetSchema } } } } } } } } } }, ) p = get_paths(spec)["/pet"] c = p["post"]["callbacks"]["petEvent"]["petCallbackUrl"] get = c["get"] assert get["responses"]["200"]["content"]["application/json"][ "schema" ] == build_ref(spec, "schema", "Pet") @pytest.mark.parametrize("spec_fixture", ("2.0",), indirect=True) def test_schema_uses_ref_in_parameters_and_request_body_if_available_v2( self, spec_fixture ): spec_fixture.spec.components.schema("Pet", schema=PetSchema) spec_fixture.spec.path( path="/pet", operations={ "get": {"parameters": [{"in": "query", "schema": PetSchema}]}, "post": {"parameters": [{"in": "body", "schema": PetSchema}]}, }, ) p = get_paths(spec_fixture.spec)["/pet"] assert "schema" not in p["get"]["parameters"][0] post = p["post"] assert len(post["parameters"]) == 1 assert post["parameters"][0]["schema"] == build_ref( spec_fixture.spec, "schema", "Pet" ) @pytest.mark.parametrize("spec_fixture", ("3.0.0",), indirect=True) def test_schema_uses_ref_in_parameters_and_request_body_if_available_v3( self, spec_fixture ): spec_fixture.spec.components.schema("Pet", schema=PetSchema) spec_fixture.spec.path( path="/pet", operations={ "get": {"parameters": [{"in": "query", "schema": PetSchema}]}, "post": { "requestBody": { "content": {"application/json": {"schema": PetSchema}} } }, }, ) p = get_paths(spec_fixture.spec)["/pet"] assert "schema" in p["get"]["parameters"][0] post = p["post"] schema_ref = post["requestBody"]["content"]["application/json"]["schema"] assert schema_ref == build_ref(spec_fixture.spec, "schema", "Pet") @pytest.mark.parametrize("spec_fixture", ("3.0.0",), indirect=True) def test_callback_schema_uses_ref_in_parameters_and_request_body_if_available_v3( self, make_pet_callback_spec ): spec_fixture = make_pet_callback_spec( { "get": {"parameters": [{"in": "query", "schema": PetSchema}]}, "post": { "requestBody": { "content": {"application/json": {"schema": PetSchema}} } }, } ) p = get_paths(spec_fixture.spec)["/pet"] c = p["post"]["callbacks"]["petEvent"]["petCallbackUrl"] assert "schema" in c["get"]["parameters"][0] post = c["post"] schema_ref = post["requestBody"]["content"]["application/json"]["schema"] assert schema_ref == build_ref(spec_fixture.spec, "schema", "Pet") @pytest.mark.parametrize("spec_fixture", ("2.0",), indirect=True) def test_schema_array_uses_ref_if_available_v2(self, spec_fixture): spec_fixture.spec.components.schema("Pet", schema=PetSchema) spec_fixture.spec.path( path="/pet", operations={ "get": { "parameters": [ { "name": "petSchema", "in": "body", "schema": {"type": "array", "items": PetSchema}, } ], "responses": { 200: {"schema": {"type": "array", "items": PetSchema}} }, } }, ) get = get_paths(spec_fixture.spec)["/pet"]["get"] assert len(get["parameters"]) == 1 resolved_schema = { "type": "array", "items": build_ref(spec_fixture.spec, "schema", "Pet"), } assert get["parameters"][0]["schema"] == resolved_schema assert get["responses"]["200"]["schema"] == resolved_schema @pytest.mark.parametrize("spec_fixture", ("3.0.0",), indirect=True) def test_schema_array_uses_ref_if_available_v3(self, spec_fixture): spec_fixture.spec.components.schema("Pet", schema=PetSchema) spec_fixture.spec.path( path="/pet", operations={ "get": { "parameters": [ { "name": "Pet", "in": "query", "content": { "application/json": { "schema": {"type": "array", "items": PetSchema} } }, } ], "responses": { 200: { "content": { "application/json": { "schema": {"type": "array", "items": PetSchema} } } } }, } }, ) get = get_paths(spec_fixture.spec)["/pet"]["get"] assert len(get["parameters"]) == 1 resolved_schema = { "type": "array", "items": build_ref(spec_fixture.spec, "schema", "Pet"), } request_schema = get["parameters"][0]["content"]["application/json"]["schema"] assert request_schema == resolved_schema response_schema = get["responses"]["200"]["content"]["application/json"][ "schema" ] assert response_schema == resolved_schema @pytest.mark.parametrize("spec_fixture", ("3.0.0",), indirect=True) def test_callback_schema_array_uses_ref_if_available_v3( self, make_pet_callback_spec ): spec_fixture = make_pet_callback_spec( { "get": { "parameters": [ { "name": "Pet", "in": "query", "content": { "application/json": { "schema": {"type": "array", "items": PetSchema} } }, } ], "responses": { "200": { "content": { "application/json": { "schema": {"type": "array", "items": PetSchema} } } } }, } } ) p = get_paths(spec_fixture.spec)["/pet"] c = p["post"]["callbacks"]["petEvent"]["petCallbackUrl"] get = c["get"] assert len(get["parameters"]) == 1 resolved_schema = { "type": "array", "items": build_ref(spec_fixture.spec, "schema", "Pet"), } request_schema = get["parameters"][0]["content"]["application/json"]["schema"] assert request_schema == resolved_schema response_schema = get["responses"]["200"]["content"]["application/json"][ "schema" ] assert response_schema == resolved_schema @pytest.mark.parametrize("spec_fixture", ("2.0",), indirect=True) def test_schema_partially_v2(self, spec_fixture): spec_fixture.spec.components.schema("Pet", schema=PetSchema) spec_fixture.spec.path( path="/parents", operations={ "get": { "responses": { 200: { "schema": { "type": "object", "properties": { "mother": PetSchema, "father": PetSchema, }, } } } } }, ) get = get_paths(spec_fixture.spec)["/parents"]["get"] assert get["responses"]["200"]["schema"] == { "type": "object", "properties": { "mother": build_ref(spec_fixture.spec, "schema", "Pet"), "father": build_ref(spec_fixture.spec, "schema", "Pet"), }, } @pytest.mark.parametrize("spec_fixture", ("3.0.0",), indirect=True) def test_schema_partially_v3(self, spec_fixture): spec_fixture.spec.components.schema("Pet", schema=PetSchema) spec_fixture.spec.path( path="/parents", operations={ "get": { "responses": { 200: { "content": { "application/json": { "schema": { "type": "object", "properties": { "mother": PetSchema, "father": PetSchema, }, } } } } } } }, ) get = get_paths(spec_fixture.spec)["/parents"]["get"] assert get["responses"]["200"]["content"]["application/json"]["schema"] == { "type": "object", "properties": { "mother": build_ref(spec_fixture.spec, "schema", "Pet"), "father": build_ref(spec_fixture.spec, "schema", "Pet"), }, } @pytest.mark.parametrize("spec_fixture", ("3.0.0",), indirect=True) def test_callback_schema_partially_v3(self, make_pet_callback_spec): spec_fixture = make_pet_callback_spec( { "get": { "responses": { "200": { "content": { "application/json": { "schema": { "type": "object", "properties": { "mother": PetSchema, "father": PetSchema, }, } } } } } } } ) p = get_paths(spec_fixture.spec)["/pet"] c = p["post"]["callbacks"]["petEvent"]["petCallbackUrl"] get = c["get"] assert get["responses"]["200"]["content"]["application/json"]["schema"] == { "type": "object", "properties": { "mother": build_ref(spec_fixture.spec, "schema", "Pet"), "father": build_ref(spec_fixture.spec, "schema", "Pet"), }, } def test_parameter_reference(self, spec_fixture): if spec_fixture.spec.openapi_version.major < 3: param = {"schema": PetSchema} else: param = {"content": {"application/json": {"schema": PetSchema}}} spec_fixture.spec.components.parameter("Pet", "body", param) spec_fixture.spec.path( path="/parents", operations={"get": {"parameters": ["Pet"]}} ) get = get_paths(spec_fixture.spec)["/parents"]["get"] assert get["parameters"] == [build_ref(spec_fixture.spec, "parameter", "Pet")] def test_response_reference(self, spec_fixture): if spec_fixture.spec.openapi_version.major < 3: resp = {"schema": PetSchema} else: resp = {"content": {"application/json": {"schema": PetSchema}}} spec_fixture.spec.components.response("Pet", resp) spec_fixture.spec.path( path="/parents", operations={"get": {"responses": {"200": "Pet"}}} ) get = get_paths(spec_fixture.spec)["/parents"]["get"] assert get["responses"] == { "200": build_ref(spec_fixture.spec, "response", "Pet") } def test_schema_global_state_untouched_2json(self, spec_fixture): assert get_nested_schema(RunSchema, "sample") is None data = spec_fixture.openapi.schema2jsonschema(RunSchema) json.dumps(data) assert get_nested_schema(RunSchema, "sample") is None def test_schema_global_state_untouched_2parameters(self, spec_fixture): assert get_nested_schema(RunSchema, "sample") is None data = spec_fixture.openapi.schema2parameters(RunSchema, location="json") json.dumps(data) assert get_nested_schema(RunSchema, "sample") is None def test_resolve_schema_dict_ref_as_string(self, spec): """Test schema ref passed as string""" # The case tested here is a reference passed as string, not a # marshmallow Schema passed by name as string. We want to ensure the # MarshmallowPlugin does not interfere with the feature interpreting # strings as references. Therefore, we use a specific name to ensure # there is no Schema with that name in the marshmallow registry from # somewhere else in the tests. # e.g. PetSchema is in the registry already so it wouldn't work. schema = {"schema": "SomeSpecificPetSchema"} if spec.openapi_version.major >= 3: schema = {"content": {"application/json": schema}} spec.path("/pet/{petId}", operations={"get": {"responses": {"200": schema}}}) resp = get_paths(spec)["/pet/{petId}"]["get"]["responses"]["200"] if spec.openapi_version.major < 3: schema = resp["schema"] else: schema = resp["content"]["application/json"]["schema"] assert schema == build_ref(spec, "schema", "SomeSpecificPetSchema") class TestCircularReference: def test_circular_referencing_schemas(self, spec): spec.components.schema("Analysis", schema=AnalysisSchema) definitions = get_schemas(spec) ref = definitions["Analysis"]["properties"]["sample"] assert ref == build_ref(spec, "schema", "Sample") # Regression tests for issue #55 class TestSelfReference: def test_self_referencing_field_single(self, spec): spec.components.schema("SelfReference", schema=SelfReferencingSchema) definitions = get_schemas(spec) ref = definitions["SelfReference"]["properties"]["single"] assert ref == build_ref(spec, "schema", "SelfReference") def test_self_referencing_field_many(self, spec): spec.components.schema("SelfReference", schema=SelfReferencingSchema) definitions = get_schemas(spec) result = definitions["SelfReference"]["properties"]["multiple"] assert result == { "type": "array", "items": build_ref(spec, "schema", "SelfReference"), } class TestOrderedSchema: def test_ordered_schema(self, spec): spec.components.schema("Ordered", schema=OrderedSchema) result = get_schemas(spec)["Ordered"]["properties"] assert list(result.keys()) == ["field1", "field2", "field3", "field4", "field5"] class TestFieldWithCustomProps: def test_field_with_custom_props(self, spec): spec.components.schema("PatternedObject", schema=PatternedObjectSchema) result = get_schemas(spec)["PatternedObject"]["properties"]["count"] assert "x-count" in result assert result["x-count"] == 1 def test_field_with_custom_props_passed_as_snake_case(self, spec): spec.components.schema("PatternedObject", schema=PatternedObjectSchema) result = get_schemas(spec)["PatternedObject"]["properties"]["count2"] assert "x-count2" in result assert result["x-count2"] == 2 class TestSchemaWithDefaultValues: def test_schema_with_default_values(self, spec): spec.components.schema("DefaultValuesSchema", schema=DefaultValuesSchema) definitions = get_schemas(spec) props = definitions["DefaultValuesSchema"]["properties"] assert props["number_auto_default"]["default"] == 12 assert props["number_manual_default"]["default"] == 42 assert "default" not in props["string_callable_default"] assert props["string_manual_default"]["default"] == "Manual" assert "default" not in props["numbers"] class TestDictValues: def test_dict_values_resolve_to_additional_properties(self, spec): class SchemaWithDict(Schema): dict_field = Dict(values=String()) spec.components.schema("SchemaWithDict", schema=SchemaWithDict) result = get_schemas(spec)["SchemaWithDict"]["properties"]["dict_field"] assert result == {"type": "object", "additionalProperties": {"type": "string"}} def test_dict_with_empty_values_field(self, spec): class SchemaWithDict(Schema): dict_field = Dict() spec.components.schema("SchemaWithDict", schema=SchemaWithDict) result = get_schemas(spec)["SchemaWithDict"]["properties"]["dict_field"] assert result == {"type": "object", "additionalProperties": {}} def test_dict_with_nested(self, spec): class SchemaWithDict(Schema): dict_field = Dict(values=Nested(PetSchema)) spec.components.schema("SchemaWithDict", schema=SchemaWithDict) assert len(get_schemas(spec)) == 2 result = get_schemas(spec)["SchemaWithDict"]["properties"]["dict_field"] assert result == { "additionalProperties": build_ref(spec, "schema", "Pet"), "type": "object", } class TestList: def test_list_with_nested(self, spec): class SchemaWithList(Schema): list_field = List(Nested(PetSchema)) spec.components.schema("SchemaWithList", schema=SchemaWithList) assert len(get_schemas(spec)) == 2 result = get_schemas(spec)["SchemaWithList"]["properties"]["list_field"] assert result == {"items": build_ref(spec, "schema", "Pet"), "type": "array"} class TestTimeDelta: def test_timedelta_x_unit(self, spec): class SchemaWithTimeDelta(Schema): sec = TimeDelta("seconds") day = TimeDelta("days") spec.components.schema("SchemaWithTimeDelta", schema=SchemaWithTimeDelta) assert ( get_schemas(spec)["SchemaWithTimeDelta"]["properties"]["sec"]["x-unit"] == "seconds" ) assert ( get_schemas(spec)["SchemaWithTimeDelta"]["properties"]["day"]["x-unit"] == "days" ) apispec-6.8.1/tests/test_ext_marshmallow_common.py000066400000000000000000000062441473713346600225270ustar00rootroot00000000000000import pytest from marshmallow import Schema, fields from apispec.ext.marshmallow.common import ( get_fields, get_unique_schema_name, make_schema_key, ) from .schemas import PetSchema, SampleSchema class TestMakeSchemaKey: def test_raise_if_schema_class_passed(self): with pytest.raises(TypeError, match="based on a Schema instance"): make_schema_key(PetSchema) def test_same_schemas_instances_equal(self): assert make_schema_key(PetSchema()) == make_schema_key(PetSchema()) @pytest.mark.parametrize("structure", (list, set)) def test_same_schemas_instances_unhashable_modifiers_equal(self, structure): modifier = [str(i) for i in range(1000)] assert make_schema_key( PetSchema(load_only=structure(modifier)) ) == make_schema_key(PetSchema(load_only=structure(modifier[::-1]))) def test_different_schemas_not_equal(self): assert make_schema_key(PetSchema()) != make_schema_key(SampleSchema()) def test_instances_with_different_modifiers_not_equal(self): assert make_schema_key(PetSchema()) != make_schema_key(PetSchema(partial=True)) class TestUniqueName: def test_unique_name(self, spec): properties = { "id": {"type": "integer", "format": "int64"}, "name": {"type": "string", "example": "doggie"}, } name = get_unique_schema_name(spec.components, "Pet") assert name == "Pet" spec.components.schema("Pet", properties=properties) with pytest.warns( UserWarning, match="Multiple schemas resolved to the name Pet" ): name_1 = get_unique_schema_name(spec.components, "Pet") assert name_1 == "Pet1" spec.components.schema("Pet1", properties=properties) with pytest.warns( UserWarning, match="Multiple schemas resolved to the name Pet" ): name_2 = get_unique_schema_name(spec.components, "Pet") assert name_2 == "Pet2" class TestGetFields: @pytest.mark.parametrize("exclude_type", (tuple, list)) @pytest.mark.parametrize("dump_only_type", (tuple, list)) def test_get_fields_meta_exclude_dump_only_as_list_and_tuple( self, exclude_type, dump_only_type ): class ExcludeSchema(Schema): field1 = fields.Int() field2 = fields.Int() field3 = fields.Int() field4 = fields.Int() field5 = fields.Int() class Meta: ordered = True exclude = exclude_type(("field1", "field2")) dump_only = dump_only_type(("field3", "field4")) assert list(get_fields(ExcludeSchema).keys()) == ["field3", "field4", "field5"] assert list(get_fields(ExcludeSchema, exclude_dump_only=True).keys()) == [ "field5" ] # regression test for https://github.com/marshmallow-code/apispec/issues/673 def test_schema_with_field_named_fields(self): class TestSchema(Schema): fields = fields.Int() schema_fields = get_fields(TestSchema) assert list(schema_fields.keys()) == ["fields"] assert isinstance(schema_fields["fields"], fields.Int) apispec-6.8.1/tests/test_ext_marshmallow_field.py000066400000000000000000000536461473713346600223320ustar00rootroot00000000000000import datetime as dt import re from enum import Enum import pytest from marshmallow import Schema, fields, validate from .schemas import CategorySchema, CustomIntegerField, CustomList, CustomStringField from .utils import build_ref, get_schemas def test_field2choices_preserving_order(openapi): choices = ["a", "b", "c", "aa", "0", "cc"] field = fields.String(validate=validate.OneOf(choices)) assert openapi.field2choices(field) == {"enum": choices} @pytest.mark.parametrize( ("FieldClass", "jsontype"), [ (fields.Integer, "integer"), (fields.Float, "number"), (fields.String, "string"), (fields.Str, "string"), (fields.Boolean, "boolean"), (fields.Bool, "boolean"), (fields.UUID, "string"), (fields.DateTime, "string"), (fields.Date, "string"), (fields.Time, "string"), (fields.TimeDelta, "integer"), (fields.Email, "string"), (fields.URL, "string"), (fields.IP, "string"), (fields.IPv4, "string"), (fields.IPv6, "string"), # Custom fields inherit types from their parents (CustomStringField, "string"), (CustomIntegerField, "integer"), ], ) def test_field2property_type(FieldClass, jsontype, spec_fixture): field = FieldClass() res = spec_fixture.openapi.field2property(field) assert res["type"] == jsontype def test_field2property_no_type(spec_fixture): field = fields.Raw() res = spec_fixture.openapi.field2property(field) assert "type" not in res @pytest.mark.parametrize("ListClass", [fields.List, CustomList]) def test_formatted_field_translates_to_array(ListClass, spec_fixture): field = ListClass(fields.String) res = spec_fixture.openapi.field2property(field) assert res["type"] == "array" assert res["items"] == spec_fixture.openapi.field2property(fields.String()) @pytest.mark.parametrize( ("FieldClass", "expected_format"), [ (fields.UUID, "uuid"), (fields.DateTime, "date-time"), (fields.Date, "date"), (fields.Email, "email"), (fields.URL, "url"), (fields.IP, "ip"), (fields.IPv4, "ipv4"), (fields.IPv6, "ipv6"), ], ) def test_field2property_formats(FieldClass, expected_format, spec_fixture): field = FieldClass() res = spec_fixture.openapi.field2property(field) assert res["format"] == expected_format def test_field_with_description(spec_fixture): field = fields.Str(metadata={"description": "a username"}) res = spec_fixture.openapi.field2property(field) assert res["description"] == "a username" def test_field_with_load_default(spec_fixture): field = fields.Str(dump_default="foo", load_default="bar") res = spec_fixture.openapi.field2property(field) assert res["default"] == "bar" def test_boolean_field_with_false_load_default(spec_fixture): field = fields.Boolean(dump_default=None, load_default=False) res = spec_fixture.openapi.field2property(field) assert res["default"] is False def test_datetime_field_with_load_default(spec_fixture): field = fields.Date(load_default=dt.date(2014, 7, 18)) res = spec_fixture.openapi.field2property(field) assert res["default"] == dt.date(2014, 7, 18).isoformat() def test_field_with_load_default_callable(spec_fixture): field = fields.Str(load_default=lambda: "dummy") res = spec_fixture.openapi.field2property(field) assert "default" not in res def test_field_with_default(spec_fixture): field = fields.Str(metadata={"default": "Manual default"}) res = spec_fixture.openapi.field2property(field) assert res["default"] == "Manual default" def test_field_with_default_and_load_default(spec_fixture): field = fields.Int(load_default=12, metadata={"default": 42}) res = spec_fixture.openapi.field2property(field) assert res["default"] == 42 def test_field_with_choices(spec_fixture): field = fields.Str(validate=validate.OneOf(["freddie", "brian", "john"])) res = spec_fixture.openapi.field2property(field) assert set(res["enum"]) == {"freddie", "brian", "john"} def test_field_with_nullable_choices(spec_fixture): field = fields.Str( validate=validate.OneOf(["freddie", "brian", "john"]), allow_none=True ) res = spec_fixture.openapi.field2property(field) assert set(res["enum"]) == {"freddie", "brian", "john", None} def test_field_with_nullable_choices_returns_only_one_none(spec_fixture): field = fields.Str( validate=validate.OneOf(["freddie", "brian", "john", None]), allow_none=True ) res = spec_fixture.openapi.field2property(field) assert res["enum"] == ["freddie", "brian", "john", None] def test_field_with_equal(spec_fixture): field = fields.Str(validate=validate.Equal("only choice")) res = spec_fixture.openapi.field2property(field) assert res["enum"] == ["only choice"] def test_only_allows_valid_properties_in_metadata(spec_fixture): field = fields.Str( load_default="foo", metadata={ "description": "foo", "not_valid": "lol", "allOf": ["bar"], "enum": ["red", "blue"], }, ) res = spec_fixture.openapi.field2property(field) assert res["default"] == field.load_default assert "description" in res assert "enum" in res assert "allOf" in res assert "not_valid" not in res def test_field_with_choices_multiple(spec_fixture): field = fields.Str( validate=[ validate.OneOf(["freddie", "brian", "john"]), validate.OneOf(["brian", "john", "roger"]), ] ) res = spec_fixture.openapi.field2property(field) assert set(res["enum"]) == {"brian", "john"} def test_field_with_additional_metadata(spec_fixture): field = fields.Str(metadata={"minLength": 6, "maxLength": 100}) res = spec_fixture.openapi.field2property(field) assert res["maxLength"] == 100 assert res["minLength"] == 6 @pytest.mark.parametrize("spec_fixture", ("2.0", "3.0.0", "3.1.0"), indirect=True) def test_field_with_allow_none(spec_fixture): field = fields.Str(allow_none=True) res = spec_fixture.openapi.field2property(field) if spec_fixture.openapi.openapi_version.major < 3: assert res["x-nullable"] is True elif spec_fixture.openapi.openapi_version.minor < 1: assert res["nullable"] is True else: assert "nullable" not in res assert res["type"] == ["string", "null"] @pytest.mark.parametrize("spec_fixture", ("2.0", "3.0.0", "3.1.0"), indirect=True) @pytest.mark.parametrize("field_class", [fields.Field, fields.Raw]) def test_nullable_field_with_no_type(spec_fixture, field_class): field = field_class(allow_none=True) res = spec_fixture.openapi.field2property(field) if spec_fixture.openapi.openapi_version.major < 3: assert res["x-nullable"] is True elif spec_fixture.openapi.openapi_version.minor < 1: assert res["nullable"] is True else: assert "nullable" not in res assert "type" not in res @pytest.mark.parametrize("spec_fixture", ("2.0", "3.0.0", "3.1.0"), indirect=True) def test_nested_nullable(spec_fixture): class Child(Schema): name = fields.Str() field = fields.Nested(Child, allow_none=True) res = spec_fixture.openapi.field2property(field) version = spec_fixture.openapi.openapi_version if version.major < 3: assert res == {"$ref": "#/definitions/Child", "x-nullable": True} elif version.major == 3 and version.minor < 1: assert res == { "anyOf": [ {"type": "object", "nullable": True}, {"$ref": "#/components/schemas/Child"}, ] } else: assert res == { "anyOf": [{"$ref": "#/components/schemas/Child"}, {"type": "null"}] } @pytest.mark.parametrize("spec_fixture", ("2.0", "3.0.0", "3.1.0"), indirect=True) def test_nested_nullable_with_metadata(spec_fixture): # Regression test for https://github.com/marshmallow-code/apispec/issues/955 class Child(Schema): name = fields.Str() field = fields.Nested( Child, allow_none=True, metadata={"description": "foo"}, ) res = spec_fixture.openapi.field2property(field) version = spec_fixture.openapi.openapi_version if version.major < 3: assert res == { "allOf": [ {"$ref": "#/definitions/Child"}, ], "x-nullable": True, "description": "foo", } elif version.major == 3 and version.minor < 1: assert res == { "anyOf": [ {"$ref": "#/components/schemas/Child"}, {"type": "object", "nullable": True}, ], "description": "foo", } else: assert res == { "anyOf": [{"$ref": "#/components/schemas/Child"}, {"type": "null"}], "description": "foo", } @pytest.mark.parametrize("spec_fixture", ("2.0", "3.0.0", "3.1.0"), indirect=True) def test_nullable_pluck(spec_fixture): class Example(Schema): name = fields.Str() field = fields.Pluck(Example, "name", allow_none=True) res = spec_fixture.openapi.field2property(field) version = spec_fixture.openapi.openapi_version if version.major < 3: assert res == {"type": "string", "x-nullable": True} elif version.major == 3 and version.minor < 1: assert res == {"type": "string", "nullable": True} else: assert res == {"type": ["string", "null"]} def test_field_with_dump_only(spec_fixture): field = fields.Str(dump_only=True) res = spec_fixture.openapi.field2property(field) assert res["readOnly"] is True @pytest.mark.parametrize("spec_fixture", ("2.0", "3.0.0", "3.1.0"), indirect=True) def test_field_with_load_only(spec_fixture): field = fields.Str(load_only=True) res = spec_fixture.openapi.field2property(field) if spec_fixture.openapi.openapi_version.major < 3: assert "writeOnly" not in res else: assert res["writeOnly"] is True def test_field_with_range_no_type(spec_fixture): field = fields.Raw(validate=validate.Range(min=1, max=10)) res = spec_fixture.openapi.field2property(field) assert res["x-minimum"] == 1 assert res["x-maximum"] == 10 assert "type" not in res @pytest.mark.parametrize("field", (fields.Float, fields.Integer)) def test_field_with_range_string_type(spec_fixture, field): field = field(validate=validate.Range(min=1, max=10)) res = spec_fixture.openapi.field2property(field) assert res["minimum"] == 1 assert res["maximum"] == 10 assert isinstance(res["type"], str) @pytest.mark.parametrize("spec_fixture", ("3.1.0",), indirect=True) def test_field_with_range_type_list_with_number(spec_fixture): class NullableInteger(fields.Field): """Nullable integer""" spec_fixture.openapi.map_to_openapi_type(NullableInteger, ["integer", "null"], None) field = NullableInteger(validate=validate.Range(min=1, max=10)) res = spec_fixture.openapi.field2property(field) assert res["minimum"] == 1 assert res["maximum"] == 10 assert res["type"] == ["integer", "null"] @pytest.mark.parametrize("spec_fixture", ("3.1.0",), indirect=True) def test_field_with_range_type_list_without_number(spec_fixture): class NullableInteger(fields.Field): """Nullable integer""" spec_fixture.openapi.map_to_openapi_type(NullableInteger, ["string", "null"], None) field = NullableInteger(validate=validate.Range(min=1, max=10)) res = spec_fixture.openapi.field2property(field) assert res["x-minimum"] == 1 assert res["x-maximum"] == 10 assert res["type"] == ["string", "null"] def test_field_with_range_datetime_type(spec_fixture): field = fields.DateTime( validate=validate.Range( min=dt.datetime(1900, 1, 1), max=dt.datetime(2000, 1, 1), ) ) res = spec_fixture.openapi.field2property(field) assert res["x-minimum"] == "1900-01-01T00:00:00" assert res["x-maximum"] == "2000-01-01T00:00:00" assert isinstance(res["type"], str) def test_field_with_str_regex(spec_fixture): regex_str = "^[a-zA-Z0-9]$" field = fields.Str(validate=validate.Regexp(regex_str)) ret = spec_fixture.openapi.field2property(field) assert ret["pattern"] == regex_str def test_field_with_pattern_obj_regex(spec_fixture): regex_str = "^[a-zA-Z0-9]$" field = fields.Str(validate=validate.Regexp(re.compile(regex_str))) ret = spec_fixture.openapi.field2property(field) assert ret["pattern"] == regex_str def test_field_with_no_pattern(spec_fixture): field = fields.Str() ret = spec_fixture.openapi.field2property(field) assert "pattern" not in ret def test_field_with_multiple_patterns(recwarn, spec_fixture): regex_validators = [validate.Regexp("winner"), validate.Regexp("loser")] field = fields.Str(validate=regex_validators) with pytest.warns(UserWarning, match="More than one regex validator"): ret = spec_fixture.openapi.field2property(field) assert ret["pattern"] == "winner" def test_enum_symbol_field(spec_fixture): class MyEnum(Enum): one = 1 two = 2 field = fields.Enum(MyEnum) ret = spec_fixture.openapi.field2property(field) assert ret["type"] == "string" assert ret["enum"] == ["one", "two"] @pytest.mark.parametrize("by_value", [fields.Integer, True]) def test_enum_value_field(spec_fixture, by_value): class MyEnum(Enum): one = 1 two = 2 field = fields.Enum(MyEnum, by_value=by_value) ret = spec_fixture.openapi.field2property(field) if by_value is True: assert "type" not in ret else: assert ret["type"] == "integer" assert ret["enum"] == [1, 2] def test_nullable_enum(spec_fixture): class MyEnum(Enum): one = 1 two = 2 field = fields.Enum(MyEnum, allow_none=True, by_value=True) ret = spec_fixture.openapi.field2property(field) assert ret["enum"] == [1, 2, None] def test_nullable_enum_returns_only_one_none(spec_fixture): class MyEnum(Enum): one = 1 two = 2 three = None field = fields.Enum(MyEnum, allow_none=True, by_value=True) ret = spec_fixture.openapi.field2property(field) assert ret["enum"] == [1, 2, None] @pytest.mark.parametrize("spec_fixture", ("2.0", "3.0.0", "3.1.0"), indirect=True) def test_field2property_nested_spec_metadatas(spec_fixture): class Child(Schema): name = fields.Str() category = fields.Nested( Child, metadata={ "description": "A category", "invalid_property": "not in the result", "x_extension": "A great extension", }, ) result = spec_fixture.openapi.field2property(category) version = spec_fixture.openapi.openapi_version if version.major < 3 or version.minor < 1: assert result == { "allOf": [build_ref(spec_fixture.spec, "schema", "Child")], "description": "A category", "x-extension": "A great extension", } else: assert result == { **build_ref(spec_fixture.spec, "schema", "Child"), "description": "A category", "x-extension": "A great extension", } def test_field2property_nested_spec(spec_fixture): spec_fixture.spec.components.schema("Category", schema=CategorySchema) category = fields.Nested(CategorySchema) assert spec_fixture.openapi.field2property(category) == build_ref( spec_fixture.spec, "schema", "Category" ) def test_field2property_nested_many_spec(spec_fixture): spec_fixture.spec.components.schema("Category", schema=CategorySchema) category = fields.Nested(CategorySchema, many=True) ret = spec_fixture.openapi.field2property(category) assert ret["type"] == "array" assert ret["items"] == build_ref(spec_fixture.spec, "schema", "Category") def test_field2property_nested_ref(spec_fixture): category = fields.Nested(CategorySchema) ref = spec_fixture.openapi.field2property(category) assert ref == build_ref(spec_fixture.spec, "schema", "Category") def test_field2property_nested_many(spec_fixture): categories = fields.Nested(CategorySchema, many=True) res = spec_fixture.openapi.field2property(categories) assert res["type"] == "array" assert res["items"] == build_ref(spec_fixture.spec, "schema", "Category") def test_nested_field_with_property(spec_fixture): category_1 = fields.Nested(CategorySchema) category_2 = fields.Nested(CategorySchema, dump_only=True) category_3 = fields.Nested(CategorySchema, many=True) category_4 = fields.Nested(CategorySchema, many=True, dump_only=True) spec_fixture.spec.components.schema("Category", schema=CategorySchema) assert spec_fixture.openapi.field2property(category_1) == build_ref( spec_fixture.spec, "schema", "Category" ) assert spec_fixture.openapi.field2property(category_2) == { "allOf": [build_ref(spec_fixture.spec, "schema", "Category")], "readOnly": True, } assert spec_fixture.openapi.field2property(category_3) == { "items": build_ref(spec_fixture.spec, "schema", "Category"), "type": "array", } assert spec_fixture.openapi.field2property(category_4) == { "items": build_ref(spec_fixture.spec, "schema", "Category"), "readOnly": True, "type": "array", } def test_datetime2property_iso(spec_fixture): field = fields.DateTime(format="iso") res = spec_fixture.openapi.field2property(field) assert res == { "type": "string", "format": "date-time", } def test_datetime2property_rfc(spec_fixture): field = fields.DateTime(format="rfc") res = spec_fixture.openapi.field2property(field) assert res == { "type": "string", "format": None, "example": "Wed, 02 Oct 2002 13:00:00 GMT", "pattern": r"((Mon|Tue|Wed|Thu|Fri|Sat|Sun), ){0,1}\d{2} " + r"(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) \d{4} \d{2}:\d{2}:\d{2} " + r"(UT|GMT|EST|EDT|CST|CDT|MST|MDT|PST|PDT|(Z|A|M|N)|(\+|-)\d{4})", } def test_datetime2property_timestamp(spec_fixture): field = fields.DateTime(format="timestamp") res = spec_fixture.openapi.field2property(field) assert res == { "type": "number", "format": "float", "min": "0", "example": "1676451245.596", } def test_datetime2property_timestamp_ms(spec_fixture): field = fields.DateTime(format="timestamp_ms") res = spec_fixture.openapi.field2property(field) assert res == { "type": "number", "format": "float", "min": "0", "example": "1676451277514.654", } def test_datetime2property_custom_format(spec_fixture): field = fields.DateTime( format="%d-%m%Y %H:%M:%S", metadata={ "pattern": r"^((?:(\d{4}-\d{2}-\d{2})T(\d{2}:\d{2}:\d{2}(?:\.\d+)?))(Z|[\+-]\d{2}:\d{2})?)$" }, ) res = spec_fixture.openapi.field2property(field) assert res == { "type": "string", "format": None, "pattern": r"^((?:(\d{4}-\d{2}-\d{2})T(\d{2}:\d{2}:\d{2}(?:\.\d+)?))(Z|[\+-]\d{2}:\d{2})?)$", } def test_datetime2property_custom_format_missing_regex(spec_fixture): field = fields.DateTime(format="%d-%m%Y %H:%M:%S") res = spec_fixture.openapi.field2property(field) assert res == { "type": "string", "format": None, "pattern": None, } class TestField2PropertyPluck: @pytest.fixture(autouse=True) def _setup(self, spec_fixture): self.field2property = spec_fixture.openapi.field2property self.spec = spec_fixture.spec self.spec.components.schema("Category", schema=CategorySchema) self.unplucked = get_schemas(self.spec)["Category"]["properties"]["breed"] def test_spec(self, spec_fixture): breed = fields.Pluck(CategorySchema, "breed") assert self.field2property(breed) == self.unplucked def test_with_property(self): breed = fields.Pluck(CategorySchema, "breed", dump_only=True) assert self.field2property(breed) == {**self.unplucked, "readOnly": True} def test_metadata(self): breed = fields.Pluck( CategorySchema, "breed", metadata={ "description": "Category breed", "invalid_property": "not in the result", "x_extension": "A great extension", }, ) assert self.field2property(breed) == { **self.unplucked, "description": "Category breed", "x-extension": "A great extension", } def test_many(self): breed = fields.Pluck(CategorySchema, "breed", many=True) assert self.field2property(breed) == {"type": "array", "items": self.unplucked} def test_many_with_property(self): breed = fields.Pluck(CategorySchema, "breed", many=True, dump_only=True) assert self.field2property(breed) == { "items": self.unplucked, "type": "array", "readOnly": True, } def test_custom_properties_for_custom_fields(spec_fixture): def custom_string2properties(self, field, **kwargs): ret = {} if isinstance(field, CustomStringField): if self.openapi_version.major == 2: ret["x-customString"] = True else: ret["x-customString"] = False return ret spec_fixture.marshmallow_plugin.converter.add_attribute_function( custom_string2properties ) properties = spec_fixture.marshmallow_plugin.converter.field2property( CustomStringField() ) assert properties["x-customString"] == ( spec_fixture.openapi.openapi_version.major == 2 ) def test_field2property_with_non_string_metadata_keys(spec_fixture): class _DesertSentinel: pass field = fields.Boolean(metadata={"description": "A description"}) field.metadata[_DesertSentinel()] = "to be ignored" result = spec_fixture.openapi.field2property(field) assert result == {"description": "A description", "type": "boolean"} apispec-6.8.1/tests/test_ext_marshmallow_openapi.py000066400000000000000000000573661473713346600227050ustar00rootroot00000000000000from datetime import datetime import pytest from marshmallow import EXCLUDE, INCLUDE, RAISE, Schema, fields, validate from packaging.version import Version from apispec import APISpec, exceptions from apispec.ext.marshmallow import MarshmallowPlugin, OpenAPIConverter from .schemas import CustomList, CustomStringField from .utils import build_ref, get_schemas, validate_spec class TestMarshmallowFieldToOpenAPI: def test_fields_with_load_default_load(self, openapi): class MySchema(Schema): field = fields.Str(dump_default="foo", load_default="bar") res = openapi.schema2parameters(MySchema, location="query") if openapi.openapi_version.major < 3: assert res[0]["default"] == "bar" else: assert res[0]["schema"]["default"] == "bar" # json/body is invalid for OpenAPI 3 @pytest.mark.parametrize("openapi", ("2.0",), indirect=True) def test_fields_default_location_mapping_if_schema_many(self, openapi): class ExampleSchema(Schema): id = fields.Int() schema = ExampleSchema(many=True) res = openapi.schema2parameters(schema=schema, location="json") assert res[0]["in"] == "body" def test_fields_with_dump_only(self, openapi): class UserSchema(Schema): name = fields.Str(dump_only=True) res = openapi.schema2parameters(schema=UserSchema(), location="query") assert len(res) == 0 class UserSchema2(Schema): name = fields.Str() class Meta: dump_only = ("name",) res = openapi.schema2parameters(schema=UserSchema2(), location="query") assert len(res) == 0 class TestMarshmallowSchemaToModelDefinition: def test_schema2jsonschema_with_explicit_fields(self, openapi): class UserSchema(Schema): _id = fields.Int() email = fields.Email(metadata={"description": "email address of the user"}) name = fields.Str() class Meta: title = "User" res = openapi.schema2jsonschema(UserSchema) assert res["title"] == "User" assert res["type"] == "object" props = res["properties"] assert props["_id"]["type"] == "integer" assert props["email"]["type"] == "string" assert props["email"]["format"] == "email" assert props["email"]["description"] == "email address of the user" def test_schema2jsonschema_override_name(self, openapi): class ExampleSchema(Schema): _id = fields.Int(data_key="id") _global = fields.Int(data_key="global") class Meta: exclude = ("_global",) res = openapi.schema2jsonschema(ExampleSchema) assert res["type"] == "object" props = res["properties"] # `_id` renamed to `id` assert "_id" not in props and props["id"]["type"] == "integer" # `_global` excluded correctly assert "_global" not in props and "global" not in props def test_required_fields(self, openapi): class BandSchema(Schema): drummer = fields.Str(required=True) bassist = fields.Str() res = openapi.schema2jsonschema(BandSchema) assert res["required"] == ["drummer"] def test_partial(self, openapi): class BandSchema(Schema): drummer = fields.Str(required=True) bassist = fields.Str(required=True) res = openapi.schema2jsonschema(BandSchema(partial=True)) assert "required" not in res res = openapi.schema2jsonschema(BandSchema(partial=("drummer",))) assert res["required"] == ["bassist"] def test_no_required_fields(self, openapi): class BandSchema(Schema): drummer = fields.Str() bassist = fields.Str() res = openapi.schema2jsonschema(BandSchema) assert "required" not in res def test_title_and_description_may_be_added(self, openapi): class UserSchema(Schema): class Meta: title = "User" description = "A registered user" res = openapi.schema2jsonschema(UserSchema) assert res["description"] == "A registered user" assert res["title"] == "User" def test_excluded_fields(self, openapi): class WhiteStripesSchema(Schema): class Meta: exclude = ("bassist",) guitarist = fields.Str() drummer = fields.Str() bassist = fields.Str() res = openapi.schema2jsonschema(WhiteStripesSchema) assert set(res["properties"].keys()) == {"guitarist", "drummer"} def test_unknown_values_disallow(self, openapi): class UnknownRaiseSchema(Schema): class Meta: unknown = RAISE first = fields.Str() res = openapi.schema2jsonschema(UnknownRaiseSchema) assert res["additionalProperties"] is False def test_unknown_values_allow(self, openapi): class UnknownIncludeSchema(Schema): class Meta: unknown = INCLUDE first = fields.Str() res = openapi.schema2jsonschema(UnknownIncludeSchema) assert res["additionalProperties"] is True def test_unknown_values_ignore(self, openapi): class UnknownExcludeSchema(Schema): class Meta: unknown = EXCLUDE first = fields.Str() res = openapi.schema2jsonschema(UnknownExcludeSchema) assert "additionalProperties" not in res def test_only_explicitly_declared_fields_are_translated(self, openapi): class UserSchema(Schema): _id = fields.Int() class Meta: title = "User" fields = ("_id", "email") with pytest.warns( UserWarning, match="Only explicitly-declared fields will be included in the Schema Object.", ): res = openapi.schema2jsonschema(UserSchema) assert res["type"] == "object" props = res["properties"] assert "_id" in props assert "email" not in props def test_observed_field_name_for_required_field(self, openapi): fields_dict = {"user_id": fields.Int(data_key="id", required=True)} res = openapi.fields2jsonschema(fields_dict) assert res["required"] == ["id"] @pytest.mark.parametrize("many", (True, False)) def test_schema_instance_inspection(self, openapi, many): class UserSchema(Schema): _id = fields.Int() res = openapi.schema2jsonschema(UserSchema(many=many)) assert res["type"] == "object" props = res["properties"] assert "_id" in props def test_raises_error_if_no_declared_fields(self, openapi): class NotASchema: pass expected_error = ( f"{NotASchema!r} is neither a Schema class nor a Schema instance." ) with pytest.raises(ValueError, match=expected_error): openapi.schema2jsonschema(NotASchema) class TestMarshmallowSchemaToParameters: def test_custom_properties_for_custom_fields(self, spec_fixture): class DelimitedList(fields.List): """Delimited list field""" def delimited_list2param(self, field: fields.Field, **kwargs) -> dict: ret: dict = {} if isinstance(field, DelimitedList): if self.openapi_version.major < 3: ret["collectionFormat"] = "csv" else: ret["explode"] = False ret["style"] = "form" return ret spec_fixture.marshmallow_plugin.converter.add_parameter_attribute_function( delimited_list2param ) class MySchema(Schema): delimited_list = DelimitedList(fields.Int) param = spec_fixture.marshmallow_plugin.converter.schema2parameters( MySchema(), location="query" )[0] if spec_fixture.openapi.openapi_version.major < 3: assert param["collectionFormat"] == "csv" else: assert param["explode"] is False assert param["style"] == "form" def test_field_required(self, openapi): field = fields.Str(required=True) res = openapi._field2parameter(field, name="field", location="query") assert res["required"] is True def test_field_deprecated(self, openapi): field = fields.Str(metadata={"deprecated": True}) res = openapi._field2parameter(field, name="field", location="query") assert res["deprecated"] is True def test_schema_partial(self, openapi): class UserSchema(Schema): field = fields.Str(required=True) res_nodump = openapi.schema2parameters( UserSchema(partial=True), location="query" ) param = res_nodump[0] assert param["required"] is False def test_schema_partial_list(self, openapi): class UserSchema(Schema): field = fields.Str(required=True) partial_field = fields.Str(required=True) res_nodump = openapi.schema2parameters( UserSchema(partial=("partial_field",)), location="query" ) param = next(p for p in res_nodump if p["name"] == "field") assert param["required"] is True param = next(p for p in res_nodump if p["name"] == "partial_field") assert param["required"] is False @pytest.mark.parametrize("ListClass", [fields.List, CustomList]) def test_field_list(self, ListClass, openapi): field = ListClass(fields.Str) res = openapi._field2parameter(field, name="field", location="query") assert res["in"] == "query" if openapi.openapi_version.major < 3: assert res["type"] == "array" assert res["items"]["type"] == "string" assert res["collectionFormat"] == "multi" else: assert res["schema"]["type"] == "array" assert res["schema"]["items"]["type"] == "string" assert res["style"] == "form" assert res["explode"] is True # json/body is invalid for OpenAPI 3 @pytest.mark.parametrize("openapi", ("2.0",), indirect=True) def test_schema_body(self, openapi): class UserSchema(Schema): name = fields.Str() email = fields.Email() res = openapi.schema2parameters(UserSchema, location="body") assert len(res) == 1 param = res[0] assert param["in"] == "body" assert param["schema"] == {"$ref": "#/definitions/User"} # json/body is invalid for OpenAPI 3 @pytest.mark.parametrize("openapi", ("2.0",), indirect=True) def test_schema_body_with_dump_only(self, openapi): class UserSchema(Schema): name = fields.Str() email = fields.Email(dump_only=True) res_nodump = openapi.schema2parameters(UserSchema, location="body") assert len(res_nodump) == 1 param = res_nodump[0] assert param["in"] == "body" assert param["schema"] == build_ref(openapi.spec, "schema", "User") # json/body is invalid for OpenAPI 3 @pytest.mark.parametrize("openapi", ("2.0",), indirect=True) def test_schema_body_many(self, openapi): class UserSchema(Schema): name = fields.Str() email = fields.Email() res = openapi.schema2parameters(UserSchema(many=True), location="body") assert len(res) == 1 param = res[0] assert param["in"] == "body" assert param["schema"]["type"] == "array" assert param["schema"]["items"] == {"$ref": "#/definitions/User"} def test_schema_query(self, openapi): class UserSchema(Schema): name = fields.Str() email = fields.Email() res = openapi.schema2parameters(UserSchema, location="query") assert len(res) == 2 res.sort(key=lambda param: param["name"]) assert res[0]["name"] == "email" assert res[0]["in"] == "query" assert res[1]["name"] == "name" assert res[1]["in"] == "query" def test_schema_query_instance(self, openapi): class UserSchema(Schema): name = fields.Str() email = fields.Email() res = openapi.schema2parameters(UserSchema(), location="query") assert len(res) == 2 res.sort(key=lambda param: param["name"]) assert res[0]["name"] == "email" assert res[0]["in"] == "query" assert res[1]["name"] == "name" assert res[1]["in"] == "query" def test_schema_query_instance_many_should_raise_exception(self, openapi): class UserSchema(Schema): name = fields.Str() email = fields.Email() with pytest.raises(AssertionError): openapi.schema2parameters(UserSchema(many=True), location="query") def test_fields_query(self, openapi): class MySchema(Schema): name = fields.Str() email = fields.Email() res = openapi.schema2parameters(MySchema, location="query") assert len(res) == 2 res.sort(key=lambda param: param["name"]) assert res[0]["name"] == "email" assert res[0]["in"] == "query" assert res[1]["name"] == "name" assert res[1]["in"] == "query" def test_raises_error_if_not_a_schema(self, openapi): class NotASchema: pass expected_error = ( f"{NotASchema!r} is neither a Schema class nor a Schema instance." ) with pytest.raises(ValueError, match=expected_error): openapi.schema2jsonschema(NotASchema) class CategorySchema(Schema): id = fields.Int() name = fields.Str(required=True) breed = fields.Str(dump_only=True) class PageSchema(Schema): offset = fields.Int() limit = fields.Int() class PetSchema(Schema): category = fields.Nested(CategorySchema, many=True) name = fields.Str() class TestNesting: def test_schema2jsonschema_with_nested_fields(self, spec_fixture): res = spec_fixture.openapi.schema2jsonschema(PetSchema) props = res["properties"] assert props["category"]["items"] == build_ref( spec_fixture.spec, "schema", "Category" ) @pytest.mark.parametrize("modifier", ("only", "exclude")) def test_schema2jsonschema_with_nested_fields_only_exclude( self, spec_fixture, modifier ): class Child(Schema): i = fields.Int() j = fields.Int() class Parent(Schema): child = fields.Nested(Child, **{modifier: ("i",)}) spec_fixture.openapi.schema2jsonschema(Parent) props = get_schemas(spec_fixture.spec)["Child"]["properties"] assert ("i" in props) == (modifier == "only") assert ("j" not in props) == (modifier == "only") def test_schema2jsonschema_with_plucked_field(self, spec_fixture): class PetSchema(Schema): breed = fields.Pluck(CategorySchema, "breed") category_schema = spec_fixture.openapi.schema2jsonschema(CategorySchema) pet_schema = spec_fixture.openapi.schema2jsonschema(PetSchema) assert ( pet_schema["properties"]["breed"] == category_schema["properties"]["breed"] ) def test_schema2jsonschema_with_nested_fields_with_adhoc_changes( self, spec_fixture ): category_schema = CategorySchema() category_schema.fields["id"].required = True class PetSchema(Schema): category = fields.Nested(category_schema, many=True) name = fields.Str() spec_fixture.spec.components.schema("Pet", schema=PetSchema) props = get_schemas(spec_fixture.spec) assert props["Category"] == spec_fixture.openapi.schema2jsonschema( category_schema ) assert set(props["Category"]["required"]) == {"id", "name"} props["Category"]["required"] = ["name"] assert props["Category"] == spec_fixture.openapi.schema2jsonschema( CategorySchema ) def test_schema2jsonschema_with_plucked_fields_with_adhoc_changes( self, spec_fixture ): category_schema = CategorySchema() category_schema.fields["breed"].dump_only = True class PetSchema(Schema): breed = fields.Pluck(category_schema, "breed", many=True) spec_fixture.spec.components.schema("Pet", schema=PetSchema) props = get_schemas(spec_fixture.spec)["Pet"]["properties"] assert props["breed"]["items"]["readOnly"] is True def test_schema2jsonschema_with_nested_excluded_fields(self, spec): category_schema = CategorySchema(exclude=("breed",)) class PetSchema(Schema): category = fields.Nested(category_schema) spec.components.schema("Pet", schema=PetSchema) category_props = get_schemas(spec)["Category"]["properties"] assert "breed" not in category_props def test_openapi_tools_validate_v2(): ma_plugin = MarshmallowPlugin() spec = APISpec( title="Pets", version="0.1", plugins=(ma_plugin,), openapi_version="2.0" ) openapi = ma_plugin.converter assert openapi is not None spec.components.schema("Category", schema=CategorySchema) spec.components.schema("Pet", {"discriminator": "name"}, schema=PetSchema) spec.path( view=None, path="/category/{category_id}", operations={ "get": { "parameters": [ {"name": "q", "in": "query", "type": "string"}, { "name": "category_id", "in": "path", "required": True, "type": "string", }, openapi._field2parameter( field=fields.List( fields.Str(), validate=validate.OneOf(["freddie", "roger"]), ), location="query", name="body", ), ] + openapi.schema2parameters(PageSchema, location="query"), "responses": {200: {"schema": PetSchema, "description": "A pet"}}, }, "post": { "parameters": ( [ { "name": "category_id", "in": "path", "required": True, "type": "string", } ] + openapi.schema2parameters(CategorySchema, location="body") ), "responses": {201: {"schema": PetSchema, "description": "A pet"}}, }, }, ) try: validate_spec(spec) except exceptions.OpenAPIError as error: pytest.fail(str(error)) def test_openapi_tools_validate_v3(): ma_plugin = MarshmallowPlugin() spec = APISpec( title="Pets", version="0.1", plugins=(ma_plugin,), openapi_version="3.0.0" ) openapi = ma_plugin.converter assert openapi is not None spec.components.schema("Category", schema=CategorySchema) spec.components.schema("Pet", schema=PetSchema) spec.path( view=None, path="/category/{category_id}", operations={ "get": { "parameters": [ {"name": "q", "in": "query", "schema": {"type": "string"}}, { "name": "category_id", "in": "path", "required": True, "schema": {"type": "string"}, }, openapi._field2parameter( field=fields.List( fields.Str(), validate=validate.OneOf(["freddie", "roger"]), ), location="query", name="body", ), ] + openapi.schema2parameters(PageSchema, location="query"), "responses": { 200: { "description": "success", "content": {"application/json": {"schema": PetSchema}}, } }, }, "post": { "parameters": ( [ { "name": "category_id", "in": "path", "required": True, "schema": {"type": "string"}, } ] ), "requestBody": { "content": {"application/json": {"schema": CategorySchema}} }, "responses": { 201: { "description": "created", "content": {"application/json": {"schema": PetSchema}}, } }, }, }, ) try: validate_spec(spec) except exceptions.OpenAPIError as error: pytest.fail(str(error)) def test_openapi_converter_openapi_version_types(): spec = APISpec(title="Pets", version="0.1", openapi_version="2.0") converter_with_version = OpenAPIConverter(Version("3.1"), None, spec) converter_with_str_version = OpenAPIConverter("3.1", None, spec) assert ( converter_with_version.openapi_version == converter_with_str_version.openapi_version ) class TestFieldValidation: class ValidationSchema(Schema): id = fields.Int(dump_only=True) range = fields.Int(validate=validate.Range(min=1, max=10)) range_no_upper = fields.Float(validate=validate.Range(min=1)) multiple_ranges = fields.Int( validate=[ validate.Range(min=1), validate.Range(min=3), validate.Range(max=10), validate.Range(max=7), ] ) list_length = fields.List(fields.Str, validate=validate.Length(min=1, max=10)) custom_list_length = CustomList( fields.Str, validate=validate.Length(min=1, max=10) ) string_length = fields.Str(validate=validate.Length(min=1, max=10)) custom_field_length = CustomStringField(validate=validate.Length(min=1, max=10)) multiple_lengths = fields.Str( validate=[ validate.Length(min=1), validate.Length(min=3), validate.Length(max=10), validate.Length(max=7), ] ) equal_length = fields.Str( validate=[validate.Length(equal=5), validate.Length(min=1, max=10)] ) date_range = fields.DateTime( validate=validate.Range( min=datetime(1900, 1, 1), ) ) @pytest.mark.parametrize( ("field", "properties"), [ ("range", {"minimum": 1, "maximum": 10}), ("range_no_upper", {"minimum": 1}), ("multiple_ranges", {"minimum": 3, "maximum": 7}), ("list_length", {"minItems": 1, "maxItems": 10}), ("custom_list_length", {"minItems": 1, "maxItems": 10}), ("string_length", {"minLength": 1, "maxLength": 10}), ("custom_field_length", {"minLength": 1, "maxLength": 10}), ("multiple_lengths", {"minLength": 3, "maxLength": 7}), ("equal_length", {"minLength": 5, "maxLength": 5}), ("date_range", {"x-minimum": "1900-01-01T00:00:00"}), ], ) def test_properties(self, field, properties, spec): spec.components.schema("Validation", schema=self.ValidationSchema) result = get_schemas(spec)["Validation"]["properties"][field] for attr, expected_value in properties.items(): assert attr in result assert result[attr] == expected_value apispec-6.8.1/tests/test_utils.py000066400000000000000000000016261473713346600171100ustar00rootroot00000000000000from apispec import utils def test_build_reference(): assert utils.build_reference("schema", 2, "Test") == {"$ref": "#/definitions/Test"} assert utils.build_reference("parameter", 2, "Test") == { "$ref": "#/parameters/Test" } assert utils.build_reference("response", 2, "Test") == {"$ref": "#/responses/Test"} assert utils.build_reference("security_scheme", 2, "Test") == { "$ref": "#/securityDefinitions/Test" } assert utils.build_reference("schema", 3, "Test") == { "$ref": "#/components/schemas/Test" } assert utils.build_reference("parameter", 3, "Test") == { "$ref": "#/components/parameters/Test" } assert utils.build_reference("response", 3, "Test") == { "$ref": "#/components/responses/Test" } assert utils.build_reference("security_scheme", 3, "Test") == { "$ref": "#/components/securitySchemes/Test" } apispec-6.8.1/tests/test_yaml_utils.py000066400000000000000000000024051473713346600201260ustar00rootroot00000000000000import pytest from apispec import yaml_utils def test_load_yaml_from_docstring(): def f(): """ Foo bar baz quux --- herp: 1 derp: 2 """ result = yaml_utils.load_yaml_from_docstring(f.__doc__) assert result == {"herp": 1, "derp": 2} @pytest.mark.parametrize("docstring", (None, "", "---")) def test_load_yaml_from_docstring_empty_docstring(docstring): assert yaml_utils.load_yaml_from_docstring(docstring) == {} @pytest.mark.parametrize("docstring", (None, "", "---")) def test_load_operations_from_docstring_empty_docstring(docstring): assert yaml_utils.load_operations_from_docstring(docstring) == {} def test_dict_to_yaml_unicode(): assert yaml_utils.dict_to_yaml({"가": "나"}) == '"\\uAC00": "\\uB098"\n' assert yaml_utils.dict_to_yaml({"가": "나"}, {"allow_unicode": True}) == "가: 나\n" def test_dict_to_yaml_keys_are_not_sorted_by_default(): assert yaml_utils.dict_to_yaml({"herp": 1, "derp": 2}) == "herp: 1\nderp: 2\n" def test_dict_to_yaml_keys_can_be_sorted_with_yaml_dump_kwargs(): assert ( yaml_utils.dict_to_yaml( {"herp": 1, "derp": 2}, yaml_dump_kwargs={"sort_keys": True} ) == "derp: 2\nherp: 1\n" ) apispec-6.8.1/tests/utils.py000066400000000000000000000034161473713346600160500ustar00rootroot00000000000000"""Utilities to get elements of generated spec""" import openapi_spec_validator from openapi_spec_validator.exceptions import OpenAPISpecValidatorError from apispec import exceptions from apispec.core import APISpec from apispec.utils import build_reference def get_schemas(spec): if spec.openapi_version.major < 3: return spec.to_dict()["definitions"] return spec.to_dict()["components"]["schemas"] def get_responses(spec): if spec.openapi_version.major < 3: return spec.to_dict()["responses"] return spec.to_dict()["components"]["responses"] def get_parameters(spec): if spec.openapi_version.major < 3: return spec.to_dict()["parameters"] return spec.to_dict()["components"]["parameters"] def get_headers(spec): if spec.openapi_version.major < 3: return spec.to_dict()["headers"] return spec.to_dict()["components"]["headers"] def get_examples(spec): return spec.to_dict()["components"]["examples"] def get_security_schemes(spec): if spec.openapi_version.major < 3: return spec.to_dict()["securityDefinitions"] return spec.to_dict()["components"]["securitySchemes"] def get_paths(spec): return spec.to_dict()["paths"] def build_ref(spec, component_type, obj): return build_reference(component_type, spec.openapi_version.major, obj) def validate_spec(spec: APISpec) -> bool: """Validate the output of an :class:`APISpec` object against the OpenAPI specification. :raise: apispec.exceptions.OpenAPIError if validation fails. """ try: # Coerce to dict to satisfy Pyright openapi_spec_validator.validate(dict(spec.to_dict())) except OpenAPISpecValidatorError as err: raise exceptions.OpenAPIError(*err.args) from err else: return True apispec-6.8.1/tox.ini000066400000000000000000000014121473713346600145010ustar00rootroot00000000000000[tox] envlist= lint py{39,310,311,312,313}-marshmallow3 py313-marshmallowdev docs [testenv] extras = tests deps = marshmallow3: marshmallow>=3.10.0,<4.0.0 marshmallowdev: https://github.com/marshmallow-code/marshmallow/archive/dev.tar.gz commands = pytest {posargs} [testenv:lint] deps = pre-commit~=3.5 skip_install = true commands = pre-commit run --all-files [testenv:docs] extras = docs commands = sphinx-build docs/ docs/_build {posargs} ; Below tasks are for development only (not run in CI) [testenv:watch-docs] deps = sphinx-autobuild extras = docs commands = sphinx-autobuild --open-browser docs/ docs/_build {posargs} --watch src/apispec --delay 2 [testenv:watch-readme] deps = restview skip_install = true commands = restview README.rst