diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml deleted file mode 100644 index 485b4c55..00000000 --- a/.github/FUNDING.yml +++ /dev/null @@ -1,2 +0,0 @@ -open_collective: "marshmallow" -tidelift: "pypi/apispec" diff --git a/.github/dependabot.yml b/.github/dependabot.yml deleted file mode 100644 index a04147ca..00000000 --- a/.github/dependabot.yml +++ /dev/null @@ -1,11 +0,0 @@ -version: 2 -updates: -- package-ecosystem: pip - directory: "/" - schedule: - interval: daily - open-pull-requests-limit: 10 -- package-ecosystem: "github-actions" - directory: "/" - schedule: - interval: "monthly" diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml deleted file mode 100644 index eaf87993..00000000 --- a/.github/workflows/build-release.yml +++ /dev/null @@ -1,81 +0,0 @@ -name: 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.10-ma3", python: "3.10", tox: py310-marshmallow3 } - - { name: "3.14-ma3", python: "3.14", tox: py314-marshmallow3 } - - { name: "3.10-ma4", python: "3.10", tox: py310-marshmallow4 } - - { name: "3.14-ma4", python: "3.14", tox: py314-marshmallow4 } - - { name: "lowest", python: "3.10", tox: py310-lowest } - - { name: "3.14-madev", python: "3.14", tox: py314-marshmallowdev } - steps: - - uses: actions/checkout@v5.0.0 - - uses: actions/setup-python@v6 - 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@v5.0.0 - - uses: actions/setup-python@v6 - with: - python-version: "3.14" - - 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@v5 - 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@v5.0.0 - - uses: actions/setup-python@v6 - with: - python-version: "3.14" - - 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@v6 - with: - name: python-package-distributions - path: dist/ - - name: Publish distribution to PyPI - uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/.gitignore b/.gitignore deleted file mode 100644 index 8ba96d31..00000000 --- a/.gitignore +++ /dev/null @@ -1,108 +0,0 @@ -# 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/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml deleted file mode 100644 index 39cdcb21..00000000 --- a/.pre-commit-config.yaml +++ /dev/null @@ -1,23 +0,0 @@ -ci: - autoupdate_schedule: monthly -repos: -- repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.14.3 - hooks: - - id: ruff - - id: ruff-format -- repo: https://github.com/python-jsonschema/check-jsonschema - rev: 0.34.1 - hooks: - - id: check-github-workflows - - id: check-readthedocs -- repo: https://github.com/asottile/blacken-docs - rev: 1.20.0 - hooks: - - id: blacken-docs - additional_dependencies: [black==24.10.0] -- repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.18.2 - hooks: - - id: mypy - additional_dependencies: ["marshmallow>=3.24.1,<5", "types-PyYAML"] diff --git a/AUTHORS.rst b/AUTHORS.rst deleted file mode 100644 index e2b6758b..00000000 --- a/AUTHORS.rst +++ /dev/null @@ -1,90 +0,0 @@ -******* -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 `_ -- Robin `@allrob23 `_ -- Xingang Zhang `@0x0400 `_ -- Lewis Haley `@LewisHaley `_ diff --git a/CHANGELOG.rst b/CHANGELOG.rst deleted file mode 100644 index 98aabc10..00000000 --- a/CHANGELOG.rst +++ /dev/null @@ -1,1446 +0,0 @@ -Changelog ---------- - -6.9.0 (unreleased) -****************** - -Other changes: - -- Officially support Python 3.14 (:pr:`998`). -- Drop support for Python 3.8 (:pr:`994`). - -6.8.4 (2025-09-22) -****************** - -Bug fixes: - -- Set ``TimeDelta`` field type according to ``serialization_type`` (:pr:`990`). - Thanks :user:`LewisHaley` for the PR. - -6.8.3 (2025-09-07) -****************** - -Bug fixes: - -- ``MarshmallowPlugin``: set ``additionnalProperties`` to ``False`` if - ``unknown`` is not set (:pr:`988`). - Thanks :user:`mwgamble` for reporting. -- ``MarshmallowPlugin``: set ``additionnalProperties`` according to ``unknown`` - value passed at schema instantiation, not only as ``Meta`` attribute - (:pr:`988`). - -6.8.2 (2025-05-12) -****************** - -Bug fixes: - -- ``MarshmallowPlugin`` doesn't override ``additionalProperties`` explicitly - passed to ``fields.Dict`` (:pr:`976`). - Thanks :user:`0x0400` for the PR. - -- Perf improvement to ``filter_excluded_fields`` (:issue:`972`). - Thanks :user:`allrob23` for the PR. - -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. diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst deleted file mode 100644 index 6cc5bba0..00000000 --- a/CONTRIBUTING.rst +++ /dev/null @@ -1,119 +0,0 @@ -Contributing 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 diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 391528f8..00000000 --- a/LICENSE +++ /dev/null @@ -1,19 +0,0 @@ -Copyright 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. diff --git a/README.rst b/README.rst deleted file mode 100644 index d472589f..00000000 --- a/README.rst +++ /dev/null @@ -1,295 +0,0 @@ -******* -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. diff --git a/RELEASING.md b/RELEASING.md deleted file mode 100644 index 563db130..00000000 --- a/RELEASING.md +++ /dev/null @@ -1,8 +0,0 @@ -# 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. diff --git a/SECURITY.md b/SECURITY.md deleted file mode 100644 index 92ec2bb6..00000000 --- a/SECURITY.md +++ /dev/null @@ -1,5 +0,0 @@ -# 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. diff --git a/docs/Makefile b/docs/Makefile deleted file mode 100644 index 0e35bee9..00000000 --- a/docs/Makefile +++ /dev/null @@ -1,177 +0,0 @@ -# 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." diff --git a/docs/api_core.rst b/docs/api_core.rst deleted file mode 100644 index 0798ecb7..00000000 --- a/docs/api_core.rst +++ /dev/null @@ -1,27 +0,0 @@ -Core API -======== - -apispec -------- - -.. automodule:: apispec - :members: - - -apispec.core ------------- - -.. automodule:: apispec.core - :members: Components - -apispec.exceptions ------------------- - -.. automodule:: apispec.exceptions - :members: - -apispec.utils -------------- - -.. automodule:: apispec.utils - :members: diff --git a/docs/api_ext.rst b/docs/api_ext.rst deleted file mode 100644 index 5710c535..00000000 --- a/docs/api_ext.rst +++ /dev/null @@ -1,31 +0,0 @@ -Built-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: diff --git a/docs/authors.rst b/docs/authors.rst deleted file mode 100644 index e122f914..00000000 --- a/docs/authors.rst +++ /dev/null @@ -1 +0,0 @@ -.. include:: ../AUTHORS.rst diff --git a/docs/changelog.rst b/docs/changelog.rst deleted file mode 100644 index ec9ad755..00000000 --- a/docs/changelog.rst +++ /dev/null @@ -1,4 +0,0 @@ -.. seealso:: - Need help upgrading to a newer version? Check out the :doc:`upgrading guide `. - -.. include:: ../CHANGELOG.rst diff --git a/docs/conf.py b/docs/conf.py deleted file mode 100755 index da7226b2..00000000 --- a/docs/conf.py +++ /dev/null @@ -1,36 +0,0 @@ -import 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()] diff --git a/docs/contributing.rst b/docs/contributing.rst deleted file mode 100644 index e582053e..00000000 --- a/docs/contributing.rst +++ /dev/null @@ -1 +0,0 @@ -.. include:: ../CONTRIBUTING.rst diff --git a/docs/ecosystem.rst b/docs/ecosystem.rst deleted file mode 100644 index e51d9cf0..00000000 --- a/docs/ecosystem.rst +++ /dev/null @@ -1,6 +0,0 @@ -Ecosystem -========= - -A list of apispec-related projects can be found at the GitHub wiki here: - -https://github.com/marshmallow-code/apispec/wiki/Ecosystem diff --git a/docs/index.rst b/docs/index.rst deleted file mode 100644 index 55c83b85..00000000 --- a/docs/index.rst +++ /dev/null @@ -1,251 +0,0 @@ -******* -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 diff --git a/docs/install.rst b/docs/install.rst deleted file mode 100644 index addc3838..00000000 --- a/docs/install.rst +++ /dev/null @@ -1,35 +0,0 @@ -Install -======= - -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 diff --git a/docs/license.rst b/docs/license.rst deleted file mode 100644 index fa07e9e9..00000000 --- a/docs/license.rst +++ /dev/null @@ -1,5 +0,0 @@ -******* -License -******* - -.. literalinclude:: ../LICENSE diff --git a/docs/make.bat b/docs/make.bat deleted file mode 100644 index 2df9a8cb..00000000 --- a/docs/make.bat +++ /dev/null @@ -1,242 +0,0 @@ -@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 diff --git a/docs/quickstart.rst b/docs/quickstart.rst deleted file mode 100644 index cb862e53..00000000 --- a/docs/quickstart.rst +++ /dev/null @@ -1,113 +0,0 @@ -Quickstart -========== - -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 `. diff --git a/docs/special_topics.rst b/docs/special_topics.rst deleted file mode 100644 index 0913f3e3..00000000 --- a/docs/special_topics.rst +++ /dev/null @@ -1,168 +0,0 @@ -Special 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. diff --git a/docs/upgrading.rst b/docs/upgrading.rst deleted file mode 100644 index bb4e041c..00000000 --- a/docs/upgrading.rst +++ /dev/null @@ -1,209 +0,0 @@ -Upgrading 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`. diff --git a/docs/using_plugins.rst b/docs/using_plugins.rst deleted file mode 100644 index c0379a71..00000000 --- a/docs/using_plugins.rst +++ /dev/null @@ -1,407 +0,0 @@ -Using 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 `. diff --git a/docs/writing_plugins.rst b/docs/writing_plugins.rst deleted file mode 100644 index 66ba1f8a..00000000 --- a/docs/writing_plugins.rst +++ /dev/null @@ -1,160 +0,0 @@ -Writing 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 diff --git a/pyproject.toml b/pyproject.toml deleted file mode 100644 index 91f6aa1d..00000000 --- a/pyproject.toml +++ /dev/null @@ -1,97 +0,0 @@ -[project] -name = "apispec" -version = "6.8.4" -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.10", - "Programming Language :: Python :: 3.11", - "Programming Language :: Python :: 3.12", - "Programming Language :: Python :: 3.13", - "Programming Language :: Python :: 3.14", -] -keywords = [ - "apispec", - "swagger", - "openapi", - "specification", - "oas", - "documentation", - "spec", - "rest", - "api", -] -requires-python = ">=3.10" -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.3", - "sphinx==8.2.3", - "sphinx-issues==5.0.1", - "sphinx-rtd-theme==3.0.2", -] -tests = ["apispec[yaml,marshmallow]", "openapi-spec-validator==0.7.2", "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 diff --git a/readthedocs.yml b/readthedocs.yml deleted file mode 100644 index fd42639b..00000000 --- a/readthedocs.yml +++ /dev/null @@ -1,15 +0,0 @@ -version: 2 -sphinx: - configuration: docs/conf.py -formats: - - pdf -build: - os: ubuntu-22.04 - tools: - python: "3.13" -python: - install: - - method: pip - path: . - extra_requirements: - - docs diff --git a/src/apispec/__init__.py b/src/apispec/__init__.py deleted file mode 100644 index b10bdef0..00000000 --- a/src/apispec/__init__.py +++ /dev/null @@ -1,25 +0,0 @@ -"""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) diff --git a/src/apispec/core.py b/src/apispec/core.py deleted file mode 100644 index e9ccb406..00000000 --- a/src/apispec/core.py +++ /dev/null @@ -1,631 +0,0 @@ -"""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 diff --git a/src/apispec/exceptions.py b/src/apispec/exceptions.py deleted file mode 100644 index ef7dcdc4..00000000 --- a/src/apispec/exceptions.py +++ /dev/null @@ -1,25 +0,0 @@ -"""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.""" diff --git a/src/apispec/ext/__init__.py b/src/apispec/ext/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/src/apispec/ext/marshmallow/__init__.py b/src/apispec/ext/marshmallow/__init__.py deleted file mode 100644 index d4f1a542..00000000 --- a/src/apispec/ext/marshmallow/__init__.py +++ /dev/null @@ -1,245 +0,0 @@ -"""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, - ) diff --git a/src/apispec/ext/marshmallow/common.py b/src/apispec/ext/marshmallow/common.py deleted file mode 100644 index 6a6d4957..00000000 --- a/src/apispec/ext/marshmallow/common.py +++ /dev/null @@ -1,150 +0,0 @@ -"""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 = set(getattr(Meta, "exclude", [])) - if exclude_dump_only: - exclude.update(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) diff --git a/src/apispec/ext/marshmallow/field_converter.py b/src/apispec/ext/marshmallow/field_converter.py deleted file mode 100644 index b26972a3..00000000 --- a/src/apispec/ext/marshmallow/field_converter.py +++ /dev/null @@ -1,648 +0,0 @@ -"""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: ("number", 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) - elif "additionalProperties" not in kwargs.get("ret", {}): - 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 - # Required for Marshmallow <4. Can be removed when support for Marshmallow 3 is dropped. - # This overrides the type set in field2type_and_format (from DEFAULT_FIELD_MAPPING) - if hasattr(field, "serialization_type"): - ret["type"] = { - int: "integer", - float: "number", - }.get(field.serialization_type, "number") - 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 diff --git a/src/apispec/ext/marshmallow/openapi.py b/src/apispec/ext/marshmallow/openapi.py deleted file mode 100644 index 3663fce6..00000000 --- a/src/apispec/ext/marshmallow/openapi.py +++ /dev/null @@ -1,306 +0,0 @@ -"""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) - ) - return ret - - def list2param(self, field: marshmallow.fields.Field, **kwargs: typing.Any) -> dict: - """Return a dictionary of parameter properties from - :class:`List `. 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) - - schema_instance = resolve_schema_instance(schema) - - if hasattr(Meta, "title"): - jsonschema["title"] = Meta.title - if hasattr(Meta, "description"): - jsonschema["description"] = Meta.description - elif schema_instance.unknown != marshmallow.EXCLUDE: - jsonschema["additionalProperties"] = ( - schema_instance.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 diff --git a/src/apispec/ext/marshmallow/schema_resolver.py b/src/apispec/ext/marshmallow/schema_resolver.py deleted file mode 100644 index ca73b2c5..00000000 --- a/src/apispec/ext/marshmallow/schema_resolver.py +++ /dev/null @@ -1,301 +0,0 @@ -from .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) diff --git a/src/apispec/plugin.py b/src/apispec/plugin.py deleted file mode 100644 index f8751b3a..00000000 --- a/src/apispec/plugin.py +++ /dev/null @@ -1,94 +0,0 @@ -"""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 diff --git a/src/apispec/py.typed b/src/apispec/py.typed deleted file mode 100644 index e69de29b..00000000 diff --git a/src/apispec/utils.py b/src/apispec/utils.py deleted file mode 100644 index 0ddb831a..00000000 --- a/src/apispec/utils.py +++ /dev/null @@ -1,94 +0,0 @@ -"""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 diff --git a/src/apispec/yaml_utils.py b/src/apispec/yaml_utils.py deleted file mode 100644 index 52985dc5..00000000 --- a/src/apispec/yaml_utils.py +++ /dev/null @@ -1,51 +0,0 @@ -"""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-") - } diff --git a/tests/__init__.py b/tests/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/tests/conftest.py b/tests/conftest.py deleted file mode 100644 index 879e6602..00000000 --- a/tests/conftest.py +++ /dev/null @@ -1,35 +0,0 @@ -from 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 diff --git a/tests/schemas.py b/tests/schemas.py deleted file mode 100644 index 7c045e46..00000000 --- a/tests/schemas.py +++ /dev/null @@ -1,74 +0,0 @@ -from 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 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 diff --git a/tests/test_core.py b/tests/test_core.py deleted file mode 100644 index e18c9eb3..00000000 --- a/tests/test_core.py +++ /dev/null @@ -1,1269 +0,0 @@ -import 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", - ] diff --git a/tests/test_ext_marshmallow.py b/tests/test_ext_marshmallow.py deleted file mode 100644 index 3524ca35..00000000 --- a/tests/test_ext_marshmallow.py +++ /dev/null @@ -1,1374 +0,0 @@ -import importlib.metadata -import json - -import pytest -from marshmallow import Schema -from marshmallow.fields import ( - DateTime, - Dict, - Field, - Int, - List, - Nested, - String, - TimeDelta, -) -from packaging.version import Version - -from apispec import APISpec -from apispec.exceptions import APISpecError -from apispec.ext.marshmallow import MarshmallowPlugin, common - -from .schemas import ( - AnalysisSchema, - AnalysisWithListSchema, - DefaultValuesSchema, - PatternedObjectSchema, - PetSchema, - RunSchema, - SampleSchema, - SelfReferencingSchema, -) -from .utils import ( - build_ref, - get_headers, - get_parameters, - get_paths, - get_responses, - get_schemas, -) - -MA_VERSION = Version(importlib.metadata.version("marshmallow")) - - -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 TestFieldOrdering: - def test_field_order_preserved(self, spec): - class OrderedSchema(Schema): - if (MA_VERSION.major == 3) and (MA_VERSION.minor < 26): - - class Meta: - ordered = True - - field1 = Int() - field2 = Int() - field3 = Int() - field4 = Int() - field5 = Int() - - 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_empty_values_field_and_metadata(self, spec): - class SchemaWithDict(Schema): - dict_field = Dict(metadata={"additionalProperties": True}) - - spec.components.schema("SchemaWithDict", schema=SchemaWithDict) - result = get_schemas(spec)["SchemaWithDict"]["properties"]["dict_field"] - assert result == {"type": "object", "additionalProperties": True} - - 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" - ) diff --git a/tests/test_ext_marshmallow_common.py b/tests/test_ext_marshmallow_common.py deleted file mode 100644 index e7d9c4b3..00000000 --- a/tests/test_ext_marshmallow_common.py +++ /dev/null @@ -1,89 +0,0 @@ -import 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: - 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) diff --git a/tests/test_ext_marshmallow_field.py b/tests/test_ext_marshmallow_field.py deleted file mode 100644 index 4833a5a6..00000000 --- a/tests/test_ext_marshmallow_field.py +++ /dev/null @@ -1,687 +0,0 @@ -import datetime as dt -import importlib.metadata -import re -from enum import Enum - -import pytest -from marshmallow import Schema, fields, validate -from packaging.version import Version - -from .schemas import CategorySchema, CustomIntegerField, CustomList, CustomStringField -from .utils import build_ref, get_schemas - -MA_VERSION = Version(importlib.metadata.version("marshmallow")) - - -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, "number" if MA_VERSION.major >= 4 else "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 - - -@pytest.mark.skipif( - MA_VERSION.major >= 4, reason="Marshmallow 4 removed serialization_type attribute" -) -@pytest.mark.parametrize( - ("serialization_type", "expected_type"), - [ - (int, "integer"), - (float, "number"), - ], -) -def test_field2property_type_for_timedelta_marshmallow_3( - spec_fixture, - serialization_type, - expected_type, -): - field = fields.TimeDelta(serialization_type=serialization_type) - res = spec_fixture.openapi.field2property(field) - assert res["type"] == expected_type - - -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) -def test_nullable_raw_field(spec_fixture): - field = fields.Raw(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"} diff --git a/tests/test_ext_marshmallow_openapi.py b/tests/test_ext_marshmallow_openapi.py deleted file mode 100644 index 9d832002..00000000 --- a/tests/test_ext_marshmallow_openapi.py +++ /dev/null @@ -1,719 +0,0 @@ -import importlib.metadata -from 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 - -MA_VERSION = Version(importlib.metadata.version("marshmallow")) - - -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_default_disallow(self, openapi): - class UnknownDefaultSchema(Schema): - first = fields.Str() - - res = openapi.schema2jsonschema(UnknownDefaultSchema) - assert res["additionalProperties"] is False - - 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 - - @pytest.mark.parametrize("meta_unknown", (RAISE, INCLUDE, EXCLUDE, None)) - @pytest.mark.parametrize( - "instance_unknown,expected", ((RAISE, False), (INCLUDE, True), (EXCLUDE, None)) - ) - def test_unknown_values_instance_override_meta( - self, openapi, instance_unknown, expected, meta_unknown - ): - class UnknownSchema(Schema): - if meta_unknown is not None: - - class Meta: - unknown = meta_unknown - - first = fields.Str() - - res = openapi.schema2jsonschema(UnknownSchema(unknown=instance_unknown)) - if expected is None: - assert "additionalProperties" not in res - else: - assert res["additionalProperties"] is expected - - @pytest.mark.skipif( - MA_VERSION.major >= 4, reason="marshmallow 4 drop inferred fields" - ) - 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 diff --git a/tests/test_utils.py b/tests/test_utils.py deleted file mode 100644 index 8a9f4362..00000000 --- a/tests/test_utils.py +++ /dev/null @@ -1,24 +0,0 @@ -from 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" - } diff --git a/tests/test_yaml_utils.py b/tests/test_yaml_utils.py deleted file mode 100644 index 6d7dd28c..00000000 --- a/tests/test_yaml_utils.py +++ /dev/null @@ -1,47 +0,0 @@ -import 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" - ) diff --git a/tests/utils.py b/tests/utils.py deleted file mode 100644 index 3f5392ca..00000000 --- a/tests/utils.py +++ /dev/null @@ -1,65 +0,0 @@ -"""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 diff --git a/tox.ini b/tox.ini deleted file mode 100644 index da6b52dd..00000000 --- a/tox.ini +++ /dev/null @@ -1,36 +0,0 @@ -[tox] -envlist= - lint - py{310,311,312,313,314}-marshmallow{3,4} - py314-marshmallowdev - py310-lowest - docs - -[testenv] -extras = tests -deps = - marshmallow3: marshmallow>=3.18.0,<4.0.0 - marshmallowdev: https://github.com/marshmallow-code/marshmallow/archive/dev.tar.gz - lowest: marshmallow==3.18.0 -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