Codebase list apispec / 0a9a743
New upstream release. Kali Janitor 2 years ago
38 changed file(s) with 2247 addition(s) and 696 deletion(s). Raw diff Collapse all Expand all
0 version: 2
1 updates:
2 - package-ecosystem: pip
3 directory: "/"
4 schedule:
5 interval: daily
6 open-pull-requests-limit: 10
00 repos:
11 - repo: https://github.com/asottile/pyupgrade
2 rev: v2.4.1
2 rev: v2.11.0
33 hooks:
44 - id: pyupgrade
5 args: [--py3-plus]
5 args: [--py36-plus]
66 - repo: https://github.com/python/black
7 rev: 19.10b0
7 rev: 20.8b1
88 hooks:
99 - id: black
1010 language_version: python3
1111 - repo: https://gitlab.com/pycqa/flake8
12 rev: 3.8.1
12 rev: 3.9.0
1313 hooks:
1414 - id: flake8
15 additional_dependencies: [flake8-bugbear==20.1.4]
15 additional_dependencies: [flake8-bugbear==21.4.3]
1616 - repo: https://github.com/asottile/blacken-docs
17 rev: v1.7.0
17 rev: v1.10.0
1818 hooks:
1919 - id: blacken-docs
20 additional_dependencies: [black==19.10b0]
20 additional_dependencies: [black==20.8b1]
6262 - Ashutosh Chaudhary `@codeasashu <https://github.com/codeasashu>`_
6363 - Fedor Fominykh `@fedorfo <https://github.com/fedorfo>`_
6464 - Colin Bounouar `@Colin-b <https://github.com/Colin-b>`_
65 - Mikko Kortelainen `@kortsi <https://github.com/kortsi>`_
66 - David Bishop `@teancom <https://github.com/teancom>`_
67 - Andrea Ghensi `@sanzoghenzo <https://github.com/sanzoghenzo>`_
68 - `@timsilvers <https://github.com/timsilvers>`_
69 - Kangwook Lee `@pbzweihander <https://github.com/pbzweihander>`_
70 - Martijn Pieters `@mjpieters <https://github.com/mjpieters>`_
71 - Duncan Booth `@kupuguy <https://github.com/kupuguy>`_
72 - Luke Whitehorn `<https://github.com/Anti-Distinctlyminty>`_
73 - François Magimel `<https://github.com/Linkid>`_
74 - Stefan van der Walt `<https://github.com/stefanv>`_
00 Changelog
11 ---------
2
3 5.1.1 (2021-09-27)
4 ******************
5
6 Bug fixes:
7
8 - Fix field ordering in "ordered" schema classes documentation (:issue:`714`).
9
10 Other changes:
11
12 * Don't build universal wheels. We don't support Python 2 anymore.
13 (:pr:`705`)
14 * Make the build reproducible (:pr:`#669`).
15
16 5.1.0 (2021-08-10)
17 ******************
18
19 Features:
20
21 - Add ``lazy`` option to component registration methods. This allows to add
22 components to the spec only if they are actually referenced. (:pr:`702`)
23 - Add ``BasePlugin.header_helper`` and ``MarshmallowPlugin.header_helper``
24 (:pr:`703`).
25
26 Bug fixes:
27
28 - Ensure plugin helpers get component copies. Avoids issues if a plugin helper
29 mutates its inputs. (:pr:`704`)
30
31 5.0.0 (2021-07-29)
32 ******************
33
34 Features:
35
36 - Rename ``doc_default`` to ``default``. Since schema metadata is namespaced in
37 a single ``metadata`` parameter, there is no name collision with ``default``
38 parameter anymore (:issue:`687`).
39 - Don't build schema component reference in
40 ``OpenAPIConverter.resolve_nested_schema``. This is done later in
41 ``Components`` (:pr:`700`).
42 - ``MarshmallowPlugin``: resolve schemas in ``allOf``, ``oneOf``, ``anyOf`` and
43 ``not`` (:pr:`701`). Thanks :user:`stefanv` for the initial work on this.
44
45 Other changes:
46
47 - Refactor ``Components`` methods to make them consistent. Use ``component_id``
48 rather than ``name``, remove ``**kwargs`` when unused. (:pr:`696`)
49
50 5.0.0b1 (2021-07-22)
51 ********************
52
53 Features:
54
55 - Resolve all component references in paths and components. All references must
56 be passed as strings, not as a ``{$ref: '...'}}`` dict (:pr:`671`).
57
58 Other changes:
59
60 - Don't use deprecated ``missing`` marshmallow field attribute but use
61 ``load_default`` instead (:pr:`692`).
62 - Refactor references resolution. ``get_ref`` method is moved from ``APISpec``
63 to ``Components`` (:pr:`655`). ``APISpec.clean_parameters`` and
64 ``APISpec.clean_parameters`` are now private methods (:pr:`695`).
65 - Drop support for marshmallow < 3.13.0 (:pr:`692`).
66
67 4.7.1 (2021-07-06)
68 ******************
69
70 Bug fixes:
71
72 - Correct spelling of ``'null'``: remove extra quotes (:issue:`689`).
73 Thanks :user:`mjpieters` for the PR.
74
75 4.7.0 (2021-06-28)
76 ******************
77
78 Features:
79
80 - Document ``deprecated`` property from field metadata (:pr:`686`).
81 Thanks :user:`greyli` for the PR.
82 - Document ``writeOnly`` and ``nullable`` properties from field metadata
83 (:pr:`684`). Thanks :user:`greyli` for the PR.
84
85 4.6.0 (2021-06-14)
86 ******************
87
88 Features:
89
90 - Support ``Pluck`` field (:pr:`677`). Thanks :user:`mjpieters` for the PR.
91 - Support ``TimeDelta`` field (:pr:`678`).
92
93 4.5.0 (2021-06-04)
94 ******************
95
96 Features:
97
98 - Support OpenAPI 3.1.0 (:issue:`579`).
99
100 Bug fixes:
101
102 - Fix ``get_fields`` to avoid crashing when a field is named ``fields``
103 (:issue:`673`). Thanks :user:`Reskov` for reporting.
104
105 Other changes:
106
107 - Don't pass field metadata as keyword arguments in the tests. This is
108 deprecated since marshmallow 3.10. apispec is still compatible with
109 marshmallow >=3,<3.10 but tests now require marshmallow >=3.10. (:pr:`675`)
110
111 4.4.2 (2021-05-24)
112 ******************
113
114 Bug fixes:
115
116 - Respect ``partial`` marshmallow schema parameter: don't document the field as
117 required. (:issue:`627`). Thanks :user:`Anti-Distinctlyminty` for the PR.
118
119 4.4.1 (2021-05-07)
120 ******************
121
122 Bug fixes:
123
124 - Don't set ``additionalProperties`` if ``Meta.unknown`` is ``EXCLUDE``
125 (:issue:`659`). Thanks :user:`kupuguy` for the PR.
126
127 4.4.0 (2021-03-31)
128 ******************
129
130 Features:
131
132 - Populate ``additionalProperties`` from ``Meta.unknown`` (:pr:`635`).
133 Thanks :user:`timsilvers` for the PR.
134 - Allow ``to_yaml`` to pass kwargs to ``yaml.dump`` (:pr:`648`).
135 - Resolve header references in responses (:pr:`650`).
136 - Resolve example references in parameters, request bodies and responses
137 (:pr:`651`).
138
139 4.3.0 (2021-02-10)
140 ******************
141
142 Features:
143
144 - Add `apispec.core.Components.header` to register header components
145 (:pr:`637`).
146
147 4.2.0 (2021-02-06)
148 ******************
149
150 Features:
151
152 - Make components public attributes of ``Components`` class (:pr:`634`).
153
154 4.1.0 (2021-01-26)
155 ******************
156
157 Features:
158
159 - Resolve schemas in callbacks (:pr:`544`). Thanks :user:`kortsi` for the PR.
160
161 Bug fixes:
162
163 - Fix docstrings documenting kwargs type as dict (:issue:`534`).
164 - Use ``x-minimum`` and ``x-maximum`` extensions to document ranges that are
165 not of number type (e.g. datetime) (:issue:`614`).
166
167 Other changes:
168
169 - Test against Python 3.9.
170
171 4.0.0 (2020-09-30)
172 ******************
173
174 Features:
175
176 - *Backwards-incompatible*: Automatically generate references for schemas
177 passed as strings in responses and request bodies. When using
178 ``MarshmallowPlugin``, if a schema is passed as string, the marshmallow
179 registry is looked up for this schema name and if none is found, the name is
180 assumed to be a reference to a manually created schema and a reference is
181 generated. No exception is raised anymore if the schema name can't be found
182 in the registry. (:pr:`554`)
183
184 4.0.0b1 (2020-09-06)
185 ********************
186
187 Features:
188
189 - *Backwards-incompatible*: Ignore ``location`` field metadata. This attribute
190 was used in webargs but it has now been dropped. A ``Schema`` can now only
191 have a single location. This simplifies the logic in ``OpenAPIConverter``
192 methods, where ``default_in`` argument now becomes ``location``. (:pr:`526`)
193 - *Backwards-incompatible*: Don't document ``int`` format as ``"int32"`` and
194 ``float`` format as ``"float"``, as those are platform-dependent (:pr:`595`).
195
196 Refactoring:
197
198 - ``OpenAPIConverter.field2parameters`` and
199 ``OpenAPIConverter.property2parameter`` are removed.
200 ``OpenAPIConverter.field2parameter`` becomes private. (:pr:`581`)
201
202 Other changes:
203
204 - Drop support for marshmallow 2. Marshmallow 3.x is required. (:pr:`583`)
205 - Drop support for Python 3.5. Python 3.6+ is required. (:pr:`582`)
206
207
208 3.3.2 (2020-08-29)
209 ******************
210
211 Bug fixes:
212
213 - Fix crash when field metadata contains non-string keys (:pr:`596`).
214 Thanks :user:`sanzoghenzo` for the fix.
2215
3216 3.3.1 (2020-06-06)
4217 ******************
8221 - Fix ``MarshmallowPlugin`` crash when ``resolve_schema_dict`` is passed a
9222 schema as string and ``schema_name_resolver`` returns ``None``
10223 (:issue:`566`). Thanks :user:`black3r` for reporting and thanks
11 :user:`Bangterm` for the PR.
224 :user:`Bangertm` for the PR.
12225
13226 3.3.0 (2020-02-14)
14227 ******************
39252
40253 Features:
41254
42 - Add `apispec.core.Components.example` for adding Example Objects
43 (:pr:`515`). Thanks :user:`codeasashu` for the PR.
255 - Add `apispec.core.Components.example` for adding Example Objects (:pr:`515`).
256 Thanks :user:`codeasashu` for the PR.
44257
45258 Support:
46259
113326
114327 - Add support for path level parameters (:issue:`453`).
115328 Thanks :user:`karec` for the PR.
116 - *Backwards-incompatible*: A `apispec.exceptions.DuplicateParameterError` is
329 - *Backwards-incompatible*: A ``apispec.exceptions.DuplicateParameterError`` is
117330 raised when two parameters with same name and location are passed to a path
118331 or an operation (:pr:`455`).
119 - *Backwards-incompatible*: A `apispec.exceptions.InvalidParameterError` is
332 - *Backwards-incompatible*: A ``apispec.exceptions.InvalidParameterError`` is
120333 raised when a parameter is missing required ``name`` and ``in`` attributes
121334 after helpers have been executed (:pr:`455`).
122335
123336 Other changes:
124337
125 - *Backwards-incompatible*: All plugin helpers must accept extra `**kwargs`
338 - *Backwards-incompatible*: All plugin helpers must accept extra ``**kwargs``
126339 (:issue:`453`).
127340 - *Backwards-incompatible*: Components must be referenced by ID, not full path
128341 (:issue:`463`).
147360
148361 Bug fixes:
149362
150 - Fix handling of `http.HTTPStatus` objects (:issue:`426`). Thanks
363 - Fix handling of ``http.HTTPStatus`` objects (:issue:`426`). Thanks
151364 :user:`DStape`.
152365 - [apispec.ext.marshmallow]: Ensure make_schema_key returns a unique key on
153366 unhashable iterables (:pr:`416`, :pr:`439`). Thanks :user:`zedrdave`.
223436
224437 Other changes:
225438
226 - *Backwards-incompatible*: Components properties are now passed as dictionaries rather than keyword arguments (:pr:`381`).
439 - *Backwards-incompatible*: Components properties are now passed as
440 dictionaries rather than keyword arguments (:pr:`381`).
227441
228442 .. code-block:: python
229443
1313 :target: https://apispec.readthedocs.io/
1414 :alt: Documentation
1515
16 .. image:: https://badgen.net/badge/marshmallow/2,3?list=1
16 .. image:: https://badgen.net/badge/marshmallow/3?list=1
1717 :target: https://marshmallow.readthedocs.io/en/latest/upgrading.html
18 :alt: marshmallow 2/3 compatible
18 :alt: marshmallow 3 only
1919
2020 .. image:: https://badgen.net/badge/OAS/2,3?list=1&color=cyan
2121 :target: https://github.com/OAI/OpenAPI-Specification
3434 - Framework-agnostic
3535 - Built-in support for `marshmallow <https://marshmallow.readthedocs.io/>`_
3636 - Utilities for parsing docstrings
37
38 Installation
39 ============
40
41 ::
42
43 $ pip install -U apispec
44
45 When using marshmallow pluging, ensure a compatible marshmallow version is used: ::
46
47 $ pip install -U apispec[marshmallow]
3748
3849 Example Application
3950 ===================
6677 name = fields.Str()
6778
6879
80 # Optional security scheme support
81 api_key_scheme = {"type": "apiKey", "in": "header", "name": "X-API-Key"}
82 spec.components.security_scheme("ApiKeyAuth", api_key_scheme)
83
84
6985 # Optional Flask support
7086 app = Flask(__name__)
7187
7692 ---
7793 get:
7894 description: Get a random pet
95 security:
96 - ApiKeyAuth: []
7997 responses:
8098 200:
8199 content:
104122 # "/random": {
105123 # "get": {
106124 # "description": "Get a random pet",
125 # "security": [
126 # {
127 # "ApiKeyAuth": []
128 # }
129 # ],
107130 # "responses": {
108131 # "200": {
109132 # "content": {
157180 # }
158181 # }
159182 # }
183 # "securitySchemes": {
184 # "ApiKeyAuth": {
185 # "type": "apiKey",
186 # "in": "header",
187 # "name": "X-API-Key"
188 # }
189 # }
160190 # }
161191 # }
162192 # }
179209 # type: array
180210 # name: {type: string}
181211 # type: object
212 # securitySchemes:
213 # ApiKeyAuth:
214 # in: header
215 # name: X-API-KEY
216 # type: apiKey
182217 # info: {title: Swagger Petstore, version: 1.0.0}
183218 # openapi: 3.0.2
184219 # paths:
190225 # content:
191226 # application/json:
192227 # schema: {$ref: '#/components/schemas/Pet'}
228 # security:
229 # - ApiKeyAuth: []
193230 # tags: []
194231
195232
2626 toxenvs:
2727 - lint
2828
29 - py35-marshmallow2
30 - py35-marshmallow3
29 - py36-marshmallow3
30 - py37-marshmallow3
31 - py38-marshmallow3
32 - py39-marshmallow3
3133
32 - py36-marshmallow3
33
34 - py37-marshmallow3
35
36 - py38-marshmallow2
37 - py38-marshmallow3
38
39 - py38-marshmallowdev
34 - py39-marshmallowdev
4035
4136 - docs
4237 os: linux
4338 - template: job--pypi-release.yml@sloria
4439 parameters:
45 python: "3.8"
40 python: "3.9"
4641 distributions: "sdist bdist_wheel"
4742 dependsOn:
4843 - tox_linux
0 apispec (5.1.1-0kali1) UNRELEASED; urgency=low
1
2 * New upstream release.
3
4 -- Kali Janitor <[email protected]> Sat, 20 Nov 2021 07:00:43 -0000
5
06 apispec (3.3.1-0kali2) kali-dev; urgency=medium
17
28 [ Sophie Brun ]
00 import datetime as dt
11 import os
22 import sys
3 import time
34
45 sys.path.insert(0, os.path.abspath(os.path.join("..", "src")))
56 import apispec # noqa: E402
2223
2324 issues_github_path = "marshmallow-code/apispec"
2425
26
27 # Use SOURCE_DATE_EPOCH for reproducible build output
28 # https://reproducible-builds.org/docs/source-date-epoch/
29 build_date = dt.datetime.utcfromtimestamp(
30 int(os.environ.get("SOURCE_DATE_EPOCH", time.time()))
31 )
32
2533 source_suffix = ".rst"
2634 master_doc = "index"
2735 project = "apispec"
28 copyright = "Steven Loria {:%Y}".format(dt.datetime.utcnow())
36 copyright = f"2014-{build_date:%Y}, Steven Loria and contributors"
2937
3038 version = release = apispec.__version__
3139
4646 name = fields.Str()
4747
4848
49 # Optional security scheme support
50 api_key_scheme = {"type": "apiKey", "in": "header", "name": "X-API-Key"}
51 spec.components.security_scheme("ApiKeyAuth", api_key_scheme)
52
53
4954 # Optional Flask support
5055 app = Flask(__name__)
5156
5661 ---
5762 get:
5863 description: Get a random pet
64 security:
65 - ApiKeyAuth: []
5966 responses:
6067 200:
6168 description: Return a pet
120127 # "type": "string"
121128 # }
122129 # }
123 # }
130 # },
131 # }
132 # },
133 # "securitySchemes": {
134 # "ApiKeyAuth": {
135 # "type": "apiKey",
136 # "in": "header",
137 # "name": "X-API-Key"
124138 # }
125139 # },
126140 # "paths": {
127141 # "/random": {
128142 # "get": {
129143 # "description": "Get a random pet",
144 # "security": [
145 # {
146 # "ApiKeyAuth": []
147 # }
148 # ],
130149 # "responses": {
131150 # "200": {
132151 # "description": "Return a pet",
170189 # name:
171190 # type: string
172191 # type: object
192 # securitySchemes:
193 # ApiKeyAuth:
194 # in: header
195 # name: X-API-KEY
196 # type: apiKey
173197 # paths:
174198 # /random:
175199 # get:
181205 # schema:
182206 # $ref: '#/components/schemas/Pet'
183207 # description: Return a pet
208 # security:
209 # - ApiKeyAuth: []
184210
185211 User Guide
186212 ==========
00 Install
11 =======
22
3 **apispec** requires Python >= 3.5.
3 **apispec** requires Python >= 3.6.
44
55 From the PyPI
66 -------------
5252 Documenting Top-level Components
5353 --------------------------------
5454
55 The ``APISpec`` object contains helpers to add top-level components.
55 The ``APISpec`` object contains helpers to add top-level components:
5656
57 To add a schema (f.k.a. "definition" in OAS v2), use
58 `spec.components.schema <apispec.core.Components.schema>`.
57 .. list-table::
58 :header-rows: 1
5959
60 Likewise, parameters and responses can be added using
61 `spec.components.parameter <apispec.core.Components.parameter>` and
62 `spec.components.response <apispec.core.Components.response>`.
60 * - Component type
61 - Helper method
62 - OpenAPI version
63 * - Schema (f.k.a. "definition" in OAS v2)
64 - `spec.components.schema <apispec.core.Components.schema>`
65 - 2, 3
66 * - Parameter
67 - `spec.components.parameter <apispec.core.Components.parameter>`
68 - 2, 3
69 * - Reponse
70 - `spec.components.response <apispec.core.Components.response>`
71 - 2, 3
72 * - Header
73 - `spec.components.response <apispec.core.Components.header>`
74 - 3
75 * - Example
76 - `spec.components.response <apispec.core.Components.example>`
77 - 3
78 * - Security scheme
79 - `spec.components.response <apispec.core.Components.security_scheme>`
80 - 2, 3
81
82 Most component registration methods provide a ``lazy`` keyword argument,
83 allowing to define a component but only publish it in the generated
84 documentation if it is actually referenced.
6385
6486 To add other top-level objects, pass them to the ``APISpec`` as keyword arguments.
6587
106128 validate_spec(spec)
107129
108130
109 When adding components, the main advantage of using dedicated methods over
110 passing them as kwargs is the ability to use plugin helpers. For instance,
111 `MarshmallowPlugin <apispec.ext.marshmallow.MarshmallowPlugin>` has helpers to
112 resolve schemas in parameters and responses.
113
114131 Documenting Security Schemes
115132 ----------------------------
116133
133150 pprint(spec.to_dict()["components"]["securitySchemes"], indent=2)
134151 # { 'api_key': {'in': 'header', 'name': 'X-API-Key', 'type': 'apiKey'},
135152 # 'jwt': {'bearerFormat': 'JWT', 'scheme': 'bearer', 'type': 'http'}}
153
154 Referencing Top-level Components
155 --------------------------------
156
157 On OpenAPI, top-level component are meant to be referenced using a ``$ref``,
158 as in ``{$ref: '#/components/schemas/Pet'}`` (OpenAPI v3) or
159 ``{$ref: '#/definitions/Pet'}`` (OpenAPI v2).
160
161 APISpec automatically resolves references in paths and in components themselves
162 when a string is provided while a dict is expected. Passing a fully-resolved
163 reference is not supported. In other words, rather than passing
164 ``{"schema": {$ref: '#/components/schemas/Pet'}}``, the user must pass
165 ``{"schema": "Pet"}``. APISpec assumes a schema reference named ``"Pet"`` has
166 been defined and builds the reference using the components location
167 corresponding to the OpenAPI version.
4141 First, ensure that ``apispec-webframeworks`` is installed: ::
4242
4343 $ pip install apispec-webframeworks
44
45 Also, ensure that a compatible ``marshmallow`` version is used: ::
46
47 $ pip install -U apispec[marshmallow]
4448
4549 We can now use the marshmallow and Flask plugins.
4650
279283 .. code-block:: python
280284
281285 def my_custom_field2properties(self, field, **kwargs):
282 """Add an OpenAPI extension flag to MyCustomField instances
283 """
286 """Add an OpenAPI extension flag to MyCustomField instances"""
284287 ret = {}
285288 if isinstance(field, MyCustomField):
286289 if self.openapi_version.major > 2:
2424 .. code-block:: python
2525
2626 from apispec import Path, BasePlugin
27 from apispec.utils import load_operations_from_docstring
27 from apispec.yaml_utils import load_operations_from_docstring
2828
2929
3030 class MyPlugin(BasePlugin):
31 def path_helper(self, path, func, **kwargs):
31 def path_helper(self, path, operations, func, **kwargs):
3232 """Path helper that parses docstrings for operations. Adds a
3333 ``func`` parameter to `apispec.APISpec.path`.
3434 """
35 operations = load_operations_from_docstring(func.__doc__)
36 return Path(path=path, operations=operations)
35 operations.update(load_operations_from_docstring(func.__doc__))
3736
3837
3938 All plugin helpers must accept extra `**kwargs`, allowing custom plugins to define new arguments if required.
5049
5150 class DeprecatedPlugin(BasePlugin):
5251 def operation_helper(self, path, operations, **kwargs):
53 """Operation helper that add `deprecated` flag if in `kwargs`
54 """
52 """Operation helper that add `deprecated` flag if in `kwargs`"""
5553 if kwargs.pop("deprecated", False) is True:
5654 for key, value in operations.items():
5755 value["deprecated"] = True
0 [tool.black]
1 line-length = 88
2 target-version = ['py36', 'py37', 'py38']
00 [metadata]
11 license_files = LICENSE
2
3 [bdist_wheel]
4 # This flag says that the code is written to work on both Python 2 and Python
5 # 3. If at all possible, it is good practice to do this. If you cannot, you
6 # will need to generate wheels for each Python version that you support.
7 universal=1
82
93 [flake8]
104 ignore = E203, E266, E501, W503
11 from setuptools import setup, find_packages
22
33 EXTRAS_REQUIRE = {
4 "marshmallow": ["marshmallow>=3.13.0"],
45 "yaml": ["PyYAML>=3.10"],
56 "validation": ["prance[osv]>=0.11"],
6 "lint": ["flake8==3.8.2", "flake8-bugbear==20.1.4", "pre-commit~=2.4"],
7 "lint": ["flake8==3.9.2", "flake8-bugbear==21.9.1", "pre-commit~=2.4"],
78 "docs": [
8 "marshmallow>=2.19.2",
9 "pyyaml==5.3.1",
10 "sphinx==3.0.4",
9 "marshmallow>=3.13.0",
10 "pyyaml==5.4.1",
11 "sphinx==4.2.0",
1112 "sphinx-issues==1.2.0",
12 "sphinx-rtd-theme==0.4.3",
13 "sphinx-rtd-theme==1.0.0",
1314 ],
1415 }
1516 EXTRAS_REQUIRE["tests"] = (
1617 EXTRAS_REQUIRE["yaml"]
1718 + EXTRAS_REQUIRE["validation"]
18 + ["marshmallow>=2.19.2", "pytest", "mock"]
19 + ["marshmallow>=3.13.0", "pytest", "mock"]
1920 )
2021 EXTRAS_REQUIRE["dev"] = EXTRAS_REQUIRE["tests"] + EXTRAS_REQUIRE["lint"] + ["tox"]
2122
5960 license="MIT",
6061 zip_safe=False,
6162 keywords="apispec swagger openapi specification oas documentation spec rest api",
62 python_requires=">=3.5",
63 python_requires=">=3.6",
6364 classifiers=[
6465 "License :: OSI Approved :: MIT License",
6566 "Programming Language :: Python :: 3",
66 "Programming Language :: Python :: 3.5",
6767 "Programming Language :: Python :: 3.6",
6868 "Programming Language :: Python :: 3.7",
6969 "Programming Language :: Python :: 3.8",
70 "Programming Language :: Python :: 3.9",
7071 "Programming Language :: Python :: 3 :: Only",
7172 ],
7273 test_suite="tests",
7374 project_urls={
7475 "Funding": "https://opencollective.com/marshmallow",
7576 "Issues": "https://github.com/marshmallow-code/apispec/issues",
76 "Tidelift": "https://tidelift.com/subscription/pkg/pypi-apispec?utm_source=pypi-apispec&utm_medium=pypi", # noqa: E501
77 "Tidelift": "https://tidelift.com/subscription/pkg/pypi-apispec?utm_source=pypi-apispec&utm_medium=pypi", # noqa: B950,E501
7778 },
7879 )
22 from .core import APISpec
33 from .plugin import BasePlugin
44
5 __version__ = "3.3.1"
5 __version__ = "5.1.1"
66 __all__ = ["APISpec", "BasePlugin"]
2828 def __init__(self, plugins, openapi_version):
2929 self._plugins = plugins
3030 self.openapi_version = openapi_version
31 self._schemas = {}
32 self._responses = {}
33 self._parameters = {}
34 self._examples = {}
35 self._security_schemes = {}
31 self.schemas = {}
32 self.responses = {}
33 self.parameters = {}
34 self.headers = {}
35 self.examples = {}
36 self.security_schemes = {}
37 self.schemas_lazy = {}
38 self.responses_lazy = {}
39 self.parameters_lazy = {}
40 self.headers_lazy = {}
41 self.examples_lazy = {}
42
43 self._subsections = {
44 "schema": self.schemas,
45 "response": self.responses,
46 "parameter": self.parameters,
47 "header": self.headers,
48 "example": self.examples,
49 "security_scheme": self.security_schemes,
50 }
51 self._subsections_lazy = {
52 "schema": self.schemas_lazy,
53 "response": self.responses_lazy,
54 "parameter": self.parameters_lazy,
55 "header": self.headers_lazy,
56 "example": self.examples_lazy,
57 }
3658
3759 def to_dict(self):
38 subsections = {
39 "schema": self._schemas,
40 "response": self._responses,
41 "parameter": self._parameters,
42 "example": self._examples,
43 "security_scheme": self._security_schemes,
44 }
4560 return {
4661 COMPONENT_SUBSECTIONS[self.openapi_version.major][k]: v
47 for k, v in subsections.items()
62 for k, v in self._subsections.items()
4863 if v != {}
4964 }
5065
51 def schema(self, name, component=None, **kwargs):
66 def _register_component(self, obj_type, component_id, component, *, lazy=False):
67 subsection = (self._subsections if lazy is False else self._subsections_lazy)[
68 obj_type
69 ]
70 subsection[component_id] = component
71
72 def _do_register_lazy_component(self, obj_type, component_id):
73 component_buffer = self._subsections_lazy[obj_type]
74 # If component was lazy registered, register it for real
75 if component_id in component_buffer:
76 self._subsections[obj_type][component_id] = component_buffer.pop(
77 component_id
78 )
79
80 def get_ref(self, obj_type, obj_or_component_id):
81 """Return object or reference
82
83 If obj is a dict, it is assumed to be a complete description and it is returned as is.
84 Otherwise, it is assumed to be a reference name as string and the corresponding $ref
85 string is returned.
86
87 :param str subsection: "schema", "parameter", "response" or "security_scheme"
88 :param dict|str obj: object in dict form or as ref_id string
89 """
90 if isinstance(obj_or_component_id, dict):
91 return obj_or_component_id
92 # Register the component if it was lazy registered
93 self._do_register_lazy_component(obj_type, obj_or_component_id)
94 return build_reference(
95 obj_type, self.openapi_version.major, obj_or_component_id
96 )
97
98 def schema(self, component_id, component=None, *, lazy=False, **kwargs):
5299 """Add a new schema to the spec.
53100
54 :param str name: identifier by which schema may be referenced.
55 :param dict component: schema definition.
101 :param str component_id: identifier by which schema may be referenced
102 :param dict component: schema definition
103 :param bool lazy: register component only when referenced in the spec
104 :param kwargs: plugin-specific arguments
56105
57106 .. note::
58107
63112
64113 status = fields.String(
65114 required=True,
66 enum=['open', 'closed'],
67 description='Status (open or closed)',
115 metadata={
116 "description": "Status (open or closed)",
117 "enum": ["open", "closed"],
118 },
68119 )
69120
70121 https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#schemaObject
71122 """
72 if name in self._schemas:
123 if component_id in self.schemas:
73124 raise DuplicateComponentNameError(
74 'Another schema with name "{}" is already registered.'.format(name)
75 )
76 component = component or {}
77 ret = component.copy()
125 f'Another schema with name "{component_id}" is already registered.'
126 )
127 ret = deepcopy(component) or {}
78128 # Execute all helpers from plugins
79129 for plugin in self._plugins:
80130 try:
81 ret.update(plugin.schema_helper(name, component, **kwargs) or {})
131 ret.update(plugin.schema_helper(component_id, ret, **kwargs) or {})
82132 except PluginMethodNotImplementedError:
83133 continue
84 self._schemas[name] = ret
85 return self
86
87 def response(self, component_id, component=None, **kwargs):
134 self._resolve_refs_in_schema(ret)
135 self._register_component("schema", component_id, ret, lazy=lazy)
136 return self
137
138 def response(self, component_id, component=None, *, lazy=False, **kwargs):
88139 """Add a response which can be referenced.
89140
90141 :param str component_id: ref_id to use as reference
91142 :param dict component: response fields
92 :param dict kwargs: plugin-specific arguments
93 """
94 if component_id in self._responses:
143 :param bool lazy: register component only when referenced in the spec
144 :param kwargs: plugin-specific arguments
145 """
146 if component_id in self.responses:
95147 raise DuplicateComponentNameError(
96 'Another response with name "{}" is already registered.'.format(
97 component_id
98 )
99 )
100 component = component or {}
101 ret = component.copy()
148 f'Another response with name "{component_id}" is already registered.'
149 )
150 ret = deepcopy(component) or {}
102151 # Execute all helpers from plugins
103152 for plugin in self._plugins:
104153 try:
105 ret.update(plugin.response_helper(component, **kwargs) or {})
154 ret.update(plugin.response_helper(ret, **kwargs) or {})
106155 except PluginMethodNotImplementedError:
107156 continue
108 self._responses[component_id] = ret
109 return self
110
111 def parameter(self, component_id, location, component=None, **kwargs):
112 """ Add a parameter which can be referenced.
113
114 :param str param_id: identifier by which parameter may be referenced.
115 :param str location: location of the parameter.
116 :param dict component: parameter fields.
117 :param dict kwargs: plugin-specific arguments
118 """
119 if component_id in self._parameters:
157 self._resolve_refs_in_response(ret)
158 self._register_component("response", component_id, ret, lazy=lazy)
159 return self
160
161 def parameter(
162 self, component_id, location, component=None, *, lazy=False, **kwargs
163 ):
164 """Add a parameter which can be referenced.
165
166 :param str component_id: identifier by which parameter may be referenced
167 :param str location: location of the parameter
168 :param dict component: parameter fields
169 :param bool lazy: register component only when referenced in the spec
170 :param kwargs: plugin-specific arguments
171 """
172 if component_id in self.parameters:
120173 raise DuplicateComponentNameError(
121 'Another parameter with name "{}" is already registered.'.format(
122 component_id
123 )
124 )
125 component = component or {}
126 ret = component.copy()
174 f'Another parameter with name "{component_id}" is already registered.'
175 )
176 ret = deepcopy(component) or {}
127177 ret.setdefault("name", component_id)
128178 ret["in"] = location
129179
134184 # Execute all helpers from plugins
135185 for plugin in self._plugins:
136186 try:
137 ret.update(plugin.parameter_helper(component, **kwargs) or {})
187 ret.update(plugin.parameter_helper(ret, **kwargs) or {})
138188 except PluginMethodNotImplementedError:
139189 continue
140 self._parameters[component_id] = ret
141 return self
142
143 def example(self, name, component, **kwargs):
190 self._resolve_refs_in_parameter_or_header(ret)
191 self._register_component("parameter", component_id, ret, lazy=lazy)
192 return self
193
194 def header(self, component_id, component, *, lazy=False, **kwargs):
195 """Add a header which can be referenced.
196
197 :param str component_id: identifier by which header may be referenced
198 :param dict component: header fields
199 :param bool lazy: register component only when referenced in the spec
200 :param kwargs: plugin-specific arguments
201
202 https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.1.md#headerObject
203 """
204 ret = deepcopy(component) or {}
205 if component_id in self.headers:
206 raise DuplicateComponentNameError(
207 f'Another header with name "{component_id}" is already registered.'
208 )
209 # Execute all helpers from plugins
210 for plugin in self._plugins:
211 try:
212 ret.update(plugin.header_helper(ret, **kwargs) or {})
213 except PluginMethodNotImplementedError:
214 continue
215 self._resolve_refs_in_parameter_or_header(ret)
216 self._register_component("header", component_id, ret, lazy=lazy)
217 return self
218
219 def example(self, component_id, component, *, lazy=False):
144220 """Add an example which can be referenced
145221
146 :param str name: identifier by which example may be referenced.
147 :param dict component: example fields.
222 :param str component_id: identifier by which example may be referenced
223 :param dict component: example fields
224 :param bool lazy: register component only when referenced in the spec
148225
149226 https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.1.md#exampleObject
150227 """
151 if name in self._examples:
228 if component_id in self.examples:
152229 raise DuplicateComponentNameError(
153 'Another example with name "{}" is already registered.'.format(name)
154 )
155 self._examples[name] = component
230 f'Another example with name "{component_id}" is already registered.'
231 )
232 self._register_component("example", component_id, component, lazy=lazy)
156233 return self
157234
158235 def security_scheme(self, component_id, component):
159236 """Add a security scheme which can be referenced.
160237
161238 :param str component_id: component_id to use as reference
162 :param dict kwargs: security scheme fields
163 """
164 if component_id in self._security_schemes:
239 :param dict component: security scheme fields
240 """
241 if component_id in self.security_schemes:
165242 raise DuplicateComponentNameError(
166 'Another security scheme with name "{}" is already registered.'.format(
167 component_id
243 f'Another security scheme with name "{component_id}" is already registered.'
244 )
245 self._register_component("security_scheme", component_id, component)
246 return self
247
248 def _resolve_schema(self, obj):
249 """Replace schema reference as string with a $ref if needed
250
251 Also resolve references in the schema
252 """
253 if "schema" in obj:
254 obj["schema"] = self.get_ref("schema", obj["schema"])
255 self._resolve_refs_in_schema(obj["schema"])
256
257 def _resolve_examples(self, obj):
258 """Replace example reference as string with a $ref"""
259 for name, example in obj.get("examples", {}).items():
260 obj["examples"][name] = self.get_ref("example", example)
261
262 def _resolve_refs_in_schema(self, schema):
263 if "properties" in schema:
264 for key in schema["properties"]:
265 schema["properties"][key] = self.get_ref(
266 "schema", schema["properties"][key]
168267 )
169 )
170 self._security_schemes[component_id] = component
171 return self
268 self._resolve_refs_in_schema(schema["properties"][key])
269 if "items" in schema:
270 schema["items"] = self.get_ref("schema", schema["items"])
271 self._resolve_refs_in_schema(schema["items"])
272 for key in ("allOf", "oneOf", "anyOf"):
273 if key in schema:
274 schema[key] = [self.get_ref("schema", s) for s in schema[key]]
275 for sch in schema[key]:
276 self._resolve_refs_in_schema(sch)
277 if "not" in schema:
278 schema["not"] = self.get_ref("schema", schema["not"])
279 self._resolve_refs_in_schema(schema["not"])
280
281 def _resolve_refs_in_parameter_or_header(self, parameter_or_header):
282 self._resolve_schema(parameter_or_header)
283 self._resolve_examples(parameter_or_header)
284
285 def _resolve_refs_in_request_body(self, request_body):
286 # requestBody is OpenAPI v3+
287 for media_type in request_body["content"].values():
288 self._resolve_schema(media_type)
289 self._resolve_examples(media_type)
290
291 def _resolve_refs_in_response(self, response):
292 if self.openapi_version.major < 3:
293 self._resolve_schema(response)
294 else:
295 for media_type in response.get("content", {}).values():
296 self._resolve_schema(media_type)
297 self._resolve_examples(media_type)
298 for name, header in response.get("headers", {}).items():
299 response["headers"][name] = self.get_ref("header", header)
300 self._resolve_refs_in_parameter_or_header(response["headers"][name])
301 # TODO: Resolve link refs when Components supports links
302
303 def _resolve_refs_in_operation(self, operation):
304 if "parameters" in operation:
305 parameters = []
306 for parameter in operation["parameters"]:
307 parameter = self.get_ref("parameter", parameter)
308 self._resolve_refs_in_parameter_or_header(parameter)
309 parameters.append(parameter)
310 operation["parameters"] = parameters
311 if "requestBody" in operation:
312 self._resolve_refs_in_request_body(operation["requestBody"])
313 if "responses" in operation:
314 responses = OrderedDict()
315 for code, response in operation["responses"].items():
316 response = self.get_ref("response", response)
317 self._resolve_refs_in_response(response)
318 responses[code] = response
319 operation["responses"] = responses
320
321 def resolve_refs_in_path(self, path):
322 if "parameters" in path:
323 parameters = []
324 for parameter in path["parameters"]:
325 parameter = self.get_ref("parameter", parameter)
326 self._resolve_refs_in_parameter_or_header(parameter)
327 parameters.append(parameter)
328 path["parameters"] = parameters
329 for method in (
330 "get",
331 "put",
332 "post",
333 "delete",
334 "options",
335 "head",
336 "patch",
337 "trace",
338 ):
339 if method in path:
340 self._resolve_refs_in_operation(path[method])
172341
173342
174343 class APISpec:
180349 See https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#infoObject
181350 :param str|OpenAPIVersion openapi_version: OpenAPI Specification version.
182351 Should be in the form '2.x' or '3.x.x' to comply with the OpenAPI standard.
183 :param dict options: Optional top-level keys
352 :param options: Optional top-level keys
184353 See https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#openapi-object
185354 """
186355
220389 ret = deepupdate(ret, self.options)
221390 return ret
222391
223 def to_yaml(self):
224 """Render the spec to YAML. Requires PyYAML to be installed."""
392 def to_yaml(self, yaml_dump_kwargs=None):
393 """Render the spec to YAML. Requires PyYAML to be installed.
394
395 :param dict yaml_dump_kwargs: Additional keyword arguments to pass to `yaml.dump`
396 """
225397 from .yaml_utils import dict_to_yaml
226398
227 return dict_to_yaml(self.to_dict())
399 return dict_to_yaml(self.to_dict(), yaml_dump_kwargs)
228400
229401 def tag(self, tag):
230 """ Store information about a tag.
402 """Store information about a tag.
231403
232404 :param dict tag: the dictionary storing information about the tag.
233405 """
242414 summary=None,
243415 description=None,
244416 parameters=None,
245 **kwargs
417 **kwargs,
246418 ):
247419 """Add a new path object to the spec.
248420
253425 :param str summary: short summary relevant to all operations in this path
254426 :param str description: long description relevant to all operations in this path
255427 :param list|None parameters: list of parameters relevant to all operations in this path
256 :param dict kwargs: parameters used by any path helpers see :meth:`register_path_helper`
428 :param kwargs: parameters used by any path helpers see :meth:`register_path_helper`
257429 """
258430 # operations and parameters must be deepcopied because they are mutated
259 # in clean_operations and operation helpers and path may be called twice
431 # in _clean_operations and operation helpers and path may be called twice
260432 operations = deepcopy(operations) or OrderedDict()
261433 parameters = deepcopy(parameters) or []
262434
280452 except PluginMethodNotImplementedError:
281453 continue
282454
283 self.clean_operations(operations)
455 self._clean_operations(operations)
284456
285457 self._paths.setdefault(path, operations).update(operations)
286458 if summary is not None:
288460 if description is not None:
289461 self._paths[path]["description"] = description
290462 if parameters:
291 parameters = self.clean_parameters(parameters)
463 parameters = self._clean_parameters(parameters)
292464 self._paths[path]["parameters"] = parameters
293 return self
294
295 def get_ref(self, obj_type, obj):
296 """Return object or reference
297
298 If obj is a dict, it is assumed to be a complete description and it is returned as is.
299 Otherwise, it is assumed to be a reference name as string and the corresponding $ref
300 string is returned.
301
302 :param str obj_type: "parameter" or "response"
303 :param dict|str obj: parameter or response in dict form or as ref_id string
304 """
305 if isinstance(obj, dict):
306 return obj
307 return build_reference(obj_type, self.openapi_version.major, obj)
308
309 def clean_parameters(self, parameters):
465
466 self.components.resolve_refs_in_path(self._paths[path])
467
468 return self
469
470 def _clean_parameters(self, parameters):
310471 """Ensure that all parameters with "in" equal to "path" are also required
311472 as required by the OpenAPI specification, as well as normalizing any
312473 references to global parameters and checking for duplicates parameters
322483 missing_attrs = [attr for attr in ("name", "in") if attr not in parameter]
323484 if missing_attrs:
324485 raise InvalidParameterError(
325 "Missing keys {} for parameter".format(missing_attrs)
486 f"Missing keys {missing_attrs} for parameter"
326487 )
327488
328489 # OpenAPI Spec 3 and 2 don't allow for duplicated parameters
340501 if parameter["in"] == "path":
341502 parameter["required"] = True
342503
343 return [self.get_ref("parameter", p) for p in parameters]
344
345 def clean_operations(self, operations):
504 return parameters
505
506 def _clean_operations(self, operations):
346507 """Ensure that all parameters with "in" equal to "path" are also required
347508 as required by the OpenAPI specification, as well as normalizing any
348509 references to global parameters. Also checks for invalid HTTP methods.
363524
364525 for operation in (operations or {}).values():
365526 if "parameters" in operation:
366 operation["parameters"] = self.clean_parameters(operation["parameters"])
527 operation["parameters"] = self._clean_parameters(
528 operation["parameters"]
529 )
367530 if "responses" in operation:
368531 responses = OrderedDict()
369532 for code, response in operation["responses"].items():
372535 except (TypeError, ValueError):
373536 if self.openapi_version.major < 3 and code != "default":
374537 warnings.warn("Non-integer code not allowed in OpenAPI < 3")
375
376 responses[str(code)] = self.get_ref("response", response)
538 responses[str(code)] = response
377539 operation["responses"] = responses
44 (for response and headers schemas) and
55 `spec.path <apispec.APISpec.path>` (for responses and response headers).
66
7 Requires marshmallow>=2.15.2.
7 Requires marshmallow>=3.13.0.
88
99 ``MarshmallowPlugin`` maps marshmallow ``Field`` classes with OpenAPI types and
1010 formats.
2020
2121 .. warning::
2222
23 ``MarshmallowPlugin`` infers the ``default`` property from the ``missing``
24 attribute of the ``Field`` (unless ``missing`` is a callable).
25 In marshmallow 3, default values are entered in deserialized form,
26 so the value is serialized by the ``Field`` instance.
23 ``MarshmallowPlugin`` infers the ``default`` property from the
24 ``load_default`` attribute of the ``Field`` (unless ``load_default`` is a
25 callable). Since default values are entered in deserialized form,
26 the value displayed in the doc is serialized by the ``Field`` instance.
2727 This may lead to inaccurate documentation in very specific cases.
2828 The default value to display in the documentation can be
29 specified explicitly by passing ``doc_default`` as metadata.
29 specified explicitly by passing ``default`` as field metadata.
3030
3131 ::
3232
4747
4848 class UserSchema(Schema):
4949 id = fields.Int(dump_only=True)
50 name = fields.Str(description="The user's name")
50 name = fields.Str(metadata={"description": "The user's name"})
5151 created = fields.DateTime(
52 dump_only=True, default=dt.datetime.utcnow, doc_default="The current datetime"
52 dump_only=True,
53 dump_default=dt.datetime.utcnow,
54 metadata={"default": "The current datetime"}
5355 )
5456
5557
5961 # 'format': 'date-time',
6062 # 'readOnly': True,
6163 # 'type': 'string'},
62 # 'id': {'format': 'int32',
63 # 'readOnly': True,
64 # 'id': {'readOnly': True,
6465 # 'type': 'integer'},
6566 # 'name': {'description': "The user's name",
6667 # 'type': 'string'}},
139140 class MyCustomField(Integer):
140141 # ...
141142
142 @ma_plugin.map_to_openapi_type(Integer) # will map to ('integer', 'int32')
143 @ma_plugin.map_to_openapi_type(Integer) # will map to ('integer', None)
143144 class MyCustomFieldThatsKindaLikeAnInteger(Integer):
144145 # ...
145146 """
172173 :param dict parameter: parameter fields. May contain a marshmallow
173174 Schema class or instance.
174175 """
175 # In OpenAPIv3, this only works when using the complex form using "content"
176176 self.resolver.resolve_schema(parameter)
177177 return parameter
178178
186186 self.resolver.resolve_response(response)
187187 return response
188188
189 def header_helper(self, header, **kwargs):
190 """Header component helper that allows using a marshmallow
191 :class:`Schema <marshmallow.Schema>` in header definition.
192
193 :param dict header: header fields. May contain a marshmallow
194 Schema class or instance.
195 """
196 self.resolver.resolve_schema(header)
197 return header
198
189199 def operation_helper(self, operations, **kwargs):
190 for operation in operations.values():
191 if not isinstance(operation, dict):
192 continue
193 if "parameters" in operation:
194 operation["parameters"] = self.resolver.resolve_parameters(
195 operation["parameters"]
196 )
197 if self.openapi_version.major >= 3:
198 if "requestBody" in operation:
199 self.resolver.resolve_schema(operation["requestBody"])
200 for response in operation.get("responses", {}).values():
201 self.resolver.resolve_response(response)
200 self.resolver.resolve_operations(operations)
202201
203202 def warn_if_schema_already_in_spec(self, schema_key):
204203 """Method to warn the user if the schema has already been added to the
1919 return schema()
2020 if isinstance(schema, marshmallow.Schema):
2121 return schema
22 try:
23 return marshmallow.class_registry.get_class(schema)()
24 except marshmallow.exceptions.RegistryError:
25 raise ValueError(
26 "{!r} is not a marshmallow.Schema subclass or instance and has not"
27 " been registered in the marshmallow class registry.".format(schema)
28 )
22 return marshmallow.class_registry.get_class(schema)()
2923
3024
3125 def resolve_schema_cls(schema):
3832 return schema
3933 if isinstance(schema, marshmallow.Schema):
4034 return type(schema)
41 try:
42 return marshmallow.class_registry.get_class(schema)
43 except marshmallow.exceptions.RegistryError:
44 raise ValueError(
45 "{!r} is not a marshmallow.Schema subclass or instance and has not"
46 " been registered in the marshmallow class registry.".format(schema)
47 )
35 return marshmallow.class_registry.get_class(schema)
4836
4937
5038 def get_fields(schema, *, exclude_dump_only=False):
5442 :param bool exclude_dump_only: whether to filter fields in Meta.dump_only
5543 :rtype: dict, of field name field object pairs
5644 """
57 if hasattr(schema, "fields"):
45 if isinstance(schema, marshmallow.Schema):
5846 fields = schema.fields
59 elif hasattr(schema, "_declared_fields"):
47 elif isinstance(schema, type) and issubclass(schema, marshmallow.Schema):
6048 fields = copy.deepcopy(schema._declared_fields)
6149 else:
62 raise ValueError(
63 "{!r} doesn't have either `fields` or `_declared_fields`.".format(schema)
64 )
50 raise ValueError(f"{schema!r} is neither a Schema class nor a Schema instance.")
6551 Meta = getattr(schema, "Meta", None)
6652 warn_if_fields_defined_in_meta(fields, Meta)
6753 return filter_excluded_fields(fields, Meta, exclude_dump_only=exclude_dump_only)
9076
9177 :param dict fields: A dictionary of fields name field object pairs
9278 :param Meta: the schema's Meta class
93 :param bool exclude_dump_only: whether to filter fields in Meta.dump_only
79 :param bool exclude_dump_only: whether to filter dump_only fields
9480 """
9581 exclude = list(getattr(Meta, "exclude", []))
9682 if exclude_dump_only:
9783 exclude.extend(getattr(Meta, "dump_only", []))
9884
9985 filtered_fields = OrderedDict(
100 (key, value) for key, value in fields.items() if key not in exclude
86 (key, value)
87 for key, value in fields.items()
88 if key not in exclude and not (exclude_dump_only and value.dump_only)
10189 )
10290
10391 return filtered_fields
132120 :param int counter: the counter of the number of recursions
133121 :return: the unique name
134122 """
135 if name not in components._schemas:
123 if name not in components.schemas:
136124 return name
137125 if not counter: # first time through recursion
138126 warnings.warn(
1616
1717 RegexType = type(re.compile(""))
1818
19 MARSHMALLOW_VERSION_INFO = tuple(
20 [int(part) for part in marshmallow.__version__.split(".") if part.isdigit()]
21 )
22
23
2419 # marshmallow field => (JSON Schema type, format)
2520 DEFAULT_FIELD_MAPPING = {
26 marshmallow.fields.Integer: ("integer", "int32"),
21 marshmallow.fields.Integer: ("integer", None),
2722 marshmallow.fields.Number: ("number", None),
28 marshmallow.fields.Float: ("number", "float"),
23 marshmallow.fields.Float: ("number", None),
2924 marshmallow.fields.Decimal: ("number", None),
3025 marshmallow.fields.String: ("string", None),
3126 marshmallow.fields.Boolean: ("boolean", None),
3328 marshmallow.fields.DateTime: ("string", "date-time"),
3429 marshmallow.fields.Date: ("string", "date"),
3530 marshmallow.fields.Time: ("string", None),
31 marshmallow.fields.TimeDelta: ("integer", None),
3632 marshmallow.fields.Email: ("string", "email"),
3733 marshmallow.fields.URL: ("string", "url"),
3834 marshmallow.fields.Dict: ("object", None),
6864 "type",
6965 "items",
7066 "allOf",
67 "oneOf",
68 "anyOf",
69 "not",
7170 "properties",
7271 "additionalProperties",
7372 "readOnly",
73 "writeOnly",
7474 "xml",
7575 "externalDocs",
7676 "example",
77 "nullable",
78 "deprecated",
7779 }
7880
7981
8789
8890 def init_attribute_functions(self):
8991 self.attribute_functions = [
92 # self.field2type_and_format should run first
93 # as other functions may rely on its output
9094 self.field2type_and_format,
9195 self.field2default,
9296 self.field2choices,
98102 self.field2pattern,
99103 self.metadata2properties,
100104 self.nested2properties,
105 self.pluck2properties,
101106 self.list2properties,
102107 self.dict2properties,
108 self.timedelta2properties,
103109 ]
104110
105111 def map_to_openapi_type(self, *args):
198204 def field2default(self, field, **kwargs):
199205 """Return the dictionary containing the field's default value.
200206
201 Will first look for a `doc_default` key in the field's metadata and then
207 Will first look for a `default` key in the field's metadata and then
202208 fall back on the field's `missing` parameter. A callable passed to the
203209 field's missing parameter will be ignored.
204210
206212 :rtype: dict
207213 """
208214 ret = {}
209 if "doc_default" in field.metadata:
210 ret["default"] = field.metadata["doc_default"]
215 if "default" in field.metadata:
216 ret["default"] = field.metadata["default"]
211217 else:
212 default = field.missing
218 default = field.load_default
213219 if default is not marshmallow.missing and not callable(default):
214 if MARSHMALLOW_VERSION_INFO[0] >= 3:
215 default = field._serialize(default, None, None)
220 default = field._serialize(default, None, None)
216221 ret["default"] = default
217222 return ret
218223
264269 attributes["writeOnly"] = True
265270 return attributes
266271
267 def field2nullable(self, field, **kwargs):
272 def field2nullable(self, field, ret):
268273 """Return the dictionary of OpenAPI field attributes for a nullable field.
269274
270275 :param Field field: A marshmallow field.
272277 """
273278 attributes = {}
274279 if field.allow_none:
275 attributes[
276 "x-nullable" if self.openapi_version.major < 3 else "nullable"
277 ] = True
280 if self.openapi_version.major < 3:
281 attributes["x-nullable"] = True
282 elif self.openapi_version.minor < 1:
283 attributes["nullable"] = True
284 else:
285 attributes["type"] = [*make_type_list(ret.get("type")), "null"]
278286 return attributes
279287
280 def field2range(self, field, **kwargs):
288 def field2range(self, field, ret):
281289 """Return the dictionary of OpenAPI field attributes for a set of
282290 :class:`Range <marshmallow.validators.Range>` validators.
283291
294302 )
295303 ]
296304
297 attributes = {}
298 for validator in validators:
299 if validator.min is not None:
300 if hasattr(attributes, "minimum"):
301 attributes["minimum"] = max(attributes["minimum"], validator.min)
302 else:
303 attributes["minimum"] = validator.min
304 if validator.max is not None:
305 if hasattr(attributes, "maximum"):
306 attributes["maximum"] = min(attributes["maximum"], validator.max)
307 else:
308 attributes["maximum"] = validator.max
309 return attributes
305 min_attr, max_attr = (
306 ("minimum", "maximum")
307 if set(make_type_list(ret.get("type"))) & {"number", "integer"}
308 else ("x-minimum", "x-maximum")
309 )
310 return make_min_max_attributes(validators, min_attr, max_attr)
310311
311312 def field2length(self, field, **kwargs):
312313 """Return the dictionary of OpenAPI field attributes for a set of
315316 :param Field field: A marshmallow field.
316317 :rtype: dict
317318 """
318 attributes = {}
319
320319 validators = [
321320 validator
322321 for validator in field.validators
333332 min_attr = "minItems" if is_array else "minLength"
334333 max_attr = "maxItems" if is_array else "maxLength"
335334
336 for validator in validators:
337 if validator.min is not None:
338 if hasattr(attributes, min_attr):
339 attributes[min_attr] = max(attributes[min_attr], validator.min)
340 else:
341 attributes[min_attr] = validator.min
342 if validator.max is not None:
343 if hasattr(attributes, max_attr):
344 attributes[max_attr] = min(attributes[max_attr], validator.max)
345 else:
346 attributes[max_attr] = validator.max
347
348 for validator in validators:
349 if validator.equal is not None:
350 attributes[min_attr] = validator.equal
351 attributes[max_attr] = validator.equal
352 return attributes
335 equal_list = [
336 validator.equal for validator in validators if validator.equal is not None
337 ]
338 if equal_list:
339 return {min_attr: equal_list[0], max_attr: equal_list[0]}
340
341 return make_min_max_attributes(validators, min_attr, max_attr)
353342
354343 def field2pattern(self, field, **kwargs):
355 """Return the dictionary of OpenAPI field attributes for a set of
356 :class:`Range <marshmallow.validators.Regexp>` validators.
344 """Return the dictionary of OpenAPI field attributes for a
345 :class:`Regexp <marshmallow.validators.Regexp>` validator.
346
347 If there is more than one such validator, only the first
348 is used in the output spec.
357349
358350 :param Field field: A marshmallow field.
359351 :rtype: dict
396388 metadata = {
397389 key.replace("_", "-") if key.startswith("x_") else key: value
398390 for key, value in field.metadata.items()
391 if isinstance(key, str)
399392 }
400393
401394 # Avoid validation error with "Additional properties not allowed"
417410 :param Field field: A marshmallow field.
418411 :rtype: dict
419412 """
420 if isinstance(field, marshmallow.fields.Nested):
413 # Pluck is a subclass of Nested but is in essence a single field; it
414 # is treated separately by pluck2properties.
415 if isinstance(field, marshmallow.fields.Nested) and not isinstance(
416 field, marshmallow.fields.Pluck
417 ):
421418 schema_dict = self.resolve_nested_schema(field.schema)
422419 if ret and "$ref" in schema_dict:
423420 ret.update({"allOf": [schema_dict]})
425422 ret.update(schema_dict)
426423 return ret
427424
425 def pluck2properties(self, field, **kwargs):
426 """Return a dictionary of properties from :class:`Pluck <marshmallow.fields.Pluck` fields.
427
428 Pluck effectively trans-includes a field from another schema into this,
429 possibly wrapped in an array (`many=True`).
430
431 :param Field field: A marshmallow field.
432 :rtype: dict
433 """
434 if isinstance(field, marshmallow.fields.Pluck):
435 plucked_field = field.schema.fields[field.field_name]
436 ret = self.field2property(plucked_field)
437 return {"type": "array", "items": ret} if field.many else ret
438 return {}
439
428440 def list2properties(self, field, **kwargs):
429441 """Return a dictionary of properties from :class:`List <marshmallow.fields.List>` fields.
430442
435447 """
436448 ret = {}
437449 if isinstance(field, marshmallow.fields.List):
438 inner_field = (
439 field.inner if MARSHMALLOW_VERSION_INFO[0] >= 3 else field.container
440 )
441 ret["items"] = self.field2property(inner_field)
450 ret["items"] = self.field2property(field.inner)
442451 return ret
443452
444453 def dict2properties(self, field, **kwargs):
452461 """
453462 ret = {}
454463 if isinstance(field, marshmallow.fields.Dict):
455 if MARSHMALLOW_VERSION_INFO[0] >= 3:
456 value_field = field.value_field
457 if value_field:
458 ret["additionalProperties"] = self.field2property(value_field)
459 return ret
464 value_field = field.value_field
465 if value_field:
466 ret["additionalProperties"] = self.field2property(value_field)
467 return ret
468
469 def timedelta2properties(self, field, **kwargs):
470 """Return a dictionary of properties from :class:`TimeDelta <marshmallow.fields.TimeDelta>` fields.
471
472 Adds a `x-unit` vendor property based on the field's `precision` attribute
473
474 :param Field field: A marshmallow field.
475 :rtype: dict
476 """
477 ret = {}
478 if isinstance(field, marshmallow.fields.TimeDelta):
479 ret["x-unit"] = field.precision
480 return ret
481
482
483 def make_type_list(types):
484 """Return a list of types from a type attribute
485
486 Since OpenAPI 3.1.0, "type" can be a single type as string or a list of
487 types, including 'null'. This function takes a "type" attribute as input
488 and returns it as a list, be it an empty or single-element list.
489 This is useful to factorize type-conditional code or code adding a type.
490 """
491 if types is None:
492 return []
493 if isinstance(types, str):
494 return [types]
495 return types
496
497
498 def make_min_max_attributes(validators, min_attr, max_attr):
499 """Return a dictionary of minimum and maximum attributes based on a list
500 of validators. If either minimum or maximum values are not present in any
501 of the validator objects that attribute will be omitted.
502
503 :param validators list: A list of `Marshmallow` validator objects. Each
504 objct is inspected for a minimum and maximum values
505 :param min_attr string: The OpenAPI attribute for the minimum value
506 :param max_attr string: The OpenAPI attribute for the maximum value
507 """
508 attributes = {}
509 min_list = [validator.min for validator in validators if validator.min is not None]
510 max_list = [validator.max for validator in validators if validator.max is not None]
511 if min_list:
512 attributes[min_attr] = max(min_list)
513 if max_list:
514 attributes[max_attr] = min(max_list)
515 return attributes
1010 import marshmallow
1111 from marshmallow.utils import is_collection
1212
13 from apispec.utils import OpenAPIVersion, build_reference
13 from apispec.utils import OpenAPIVersion
1414 from apispec.exceptions import APISpecError
1515 from .field_converter import FieldConverterMixin
1616 from .common import (
1818 make_schema_key,
1919 resolve_schema_instance,
2020 get_unique_schema_name,
21 )
22
23
24 MARSHMALLOW_VERSION_INFO = tuple(
25 [int(part) for part in marshmallow.__version__.split(".") if part.isdigit()]
2621 )
2722
2823
5954 # Schema references
6055 self.refs = {}
6156
62 @staticmethod
63 def _observed_name(field, name):
64 """Adjust field name to reflect `dump_to` and `load_from` attributes.
65
66 :param Field field: A marshmallow field.
67 :param str name: Field name
68 :rtype: str
69 """
70 if MARSHMALLOW_VERSION_INFO[0] < 3:
71 # use getattr in case we're running against older versions of marshmallow.
72 dump_to = getattr(field, "dump_to", None)
73 load_from = getattr(field, "load_from", None)
74 return dump_to or load_from or name
75 return field.data_key or name
76
7757 def resolve_nested_schema(self, schema):
7858 """Return the OpenAPI representation of a marshmallow Schema.
7959
8666
8767 :param schema: schema to add to the spec
8868 """
89 schema_instance = resolve_schema_instance(schema)
69 try:
70 schema_instance = resolve_schema_instance(schema)
71 # If schema is a string and is not found in registry,
72 # assume it is a schema reference
73 except marshmallow.exceptions.RegistryError:
74 return schema
9075 schema_key = make_schema_key(schema_instance)
9176 if schema_key not in self.refs:
9277 name = self.schema_name_resolver(schema)
10994 return self.get_ref_dict(schema_instance)
11095
11196 def schema2parameters(
112 self,
113 schema,
114 *,
115 default_in="body",
116 name="body",
117 required=False,
118 description=None
97 self, schema, *, location, name="body", required=False, description=None
11998 ):
12099 """Return an array of OpenAPI parameters given a given marshmallow
121 :class:`Schema <marshmallow.Schema>`. If `default_in` is "body", then return an array
100 :class:`Schema <marshmallow.Schema>`. If `location` is "body", then return an array
122101 of a single parameter; else return an array of a parameter for each included field in
123102 the :class:`Schema <marshmallow.Schema>`.
124103
104 In OpenAPI 3, only "query", "header", "path" or "cookie" are allowed for the location
105 of parameters. "requestBody" is used when fields are in the body.
106
125107 https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#parameterObject
126108 """
127 openapi_default_in = __location_map__.get(default_in, default_in)
128 if self.openapi_version.major < 3 and openapi_default_in == "body":
129 prop = self.resolve_nested_schema(schema)
130
109 location = __location_map__.get(location, location)
110 # OAS 2 body parameter
111 if location == "body":
131112 param = {
132 "in": openapi_default_in,
113 "in": location,
133114 "required": required,
134115 "name": name,
135 "schema": prop,
116 "schema": self.resolve_nested_schema(schema),
136117 }
137
138118 if description:
139119 param["description"] = description
140
141120 return [param]
142121
143122 assert not getattr(
146125
147126 fields = get_fields(schema, exclude_dump_only=True)
148127
149 return self.fields2parameters(fields, default_in=default_in)
150
151 def fields2parameters(self, fields, *, default_in):
152 """Return an array of OpenAPI parameters given a mapping between field names and
153 :class:`Field <marshmallow.Field>` objects. If `default_in` is "body", then return an array
154 of a single parameter; else return an array of a parameter for each included field in
155 the :class:`Schema <marshmallow.Schema>`.
156
157 In OpenAPI3, only "query", "header", "path" or "cookie" are allowed for the location
158 of parameters. In OpenAPI 3, "requestBody" is used when fields are in the body.
159
160 This function always returns a list, with a parameter
161 for each included field in the :class:`Schema <marshmallow.Schema>`.
162
163 https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#parameterObject
164 """
165 parameters = []
166 body_param = None
167 for field_name, field_obj in fields.items():
168 if field_obj.dump_only:
169 continue
170 param = self.field2parameter(
128 return [
129 self._field2parameter(
171130 field_obj,
172 name=self._observed_name(field_obj, field_name),
173 default_in=default_in,
131 name=field_obj.data_key or field_name,
132 location=location,
174133 )
175 if (
176 self.openapi_version.major < 3
177 and param["in"] == "body"
178 and body_param is not None
179 ):
180 body_param["schema"]["properties"].update(param["schema"]["properties"])
181 required_fields = param["schema"].get("required", [])
182 if required_fields:
183 body_param["schema"].setdefault("required", []).extend(
184 required_fields
185 )
186 else:
187 if self.openapi_version.major < 3 and param["in"] == "body":
188 body_param = param
189 parameters.append(param)
190 return parameters
191
192 def field2parameter(self, field, *, name, default_in):
134 for field_name, field_obj in fields.items()
135 ]
136
137 def _field2parameter(self, field, *, name, location):
193138 """Return an OpenAPI parameter as a `dict`, given a marshmallow
194139 :class:`Field <marshmallow.Field>`.
195140
196141 https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#parameterObject
197142 """
198 location = field.metadata.get("location", None)
143 ret = {"in": location, "name": name}
144
145 partial = getattr(field.parent, "partial", False)
146 ret["required"] = field.required and (
147 not partial or (is_collection(partial) and field.name not in partial)
148 )
149
199150 prop = self.field2property(field)
200 return self.property2parameter(
201 prop,
202 name=name,
203 required=field.required,
204 multiple=isinstance(field, marshmallow.fields.List),
205 location=location,
206 default_in=default_in,
207 )
208
209 def property2parameter(
210 self, prop, *, name, required, multiple, location, default_in
211 ):
212 """Return the Parameter Object definition for a JSON Schema property.
213
214 https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#parameterObject
215
216 :param dict prop: JSON Schema property
217 :param str name: Field name
218 :param bool required: Parameter is required
219 :param bool multiple: Parameter is repeated
220 :param str location: Location to look for ``name``
221 :param str default_in: Default location to look for ``name``
222 :raise: TranslationError if arg object cannot be translated to a Parameter Object schema.
223 :rtype: dict, a Parameter Object
224 """
225 openapi_default_in = __location_map__.get(default_in, default_in)
226 openapi_location = __location_map__.get(location, openapi_default_in)
227 ret = {"in": openapi_location, "name": name}
228
229 if openapi_location == "body":
230 ret["required"] = False
231 ret["name"] = "body"
232 ret["schema"] = {"type": "object", "properties": {name: prop}}
233 if required:
234 ret["schema"]["required"] = [name]
151 multiple = isinstance(field, marshmallow.fields.List)
152
153 if self.openapi_version.major < 3:
154 if multiple:
155 ret["collectionFormat"] = "multi"
156 ret.update(prop)
235157 else:
236 ret["required"] = required
237 if self.openapi_version.major < 3:
238 if multiple:
239 ret["collectionFormat"] = "multi"
240 ret.update(prop)
241 else:
242 if multiple:
243 ret["explode"] = True
244 ret["style"] = "form"
245 if prop.get("description", None):
246 ret["description"] = prop.pop("description")
247 ret["schema"] = prop
158 if multiple:
159 ret["explode"] = True
160 ret["style"] = "form"
161 if prop.get("description", None):
162 ret["description"] = prop.pop("description")
163 ret["schema"] = prop
248164 return ret
249165
250166 def schema2jsonschema(self, schema):
260176 fields = get_fields(schema)
261177 Meta = getattr(schema, "Meta", None)
262178 partial = getattr(schema, "partial", None)
263 ordered = getattr(schema, "ordered", False)
179 ordered = getattr(Meta, "ordered", False)
264180
265181 jsonschema = self.fields2jsonschema(fields, partial=partial, ordered=ordered)
266182
268184 jsonschema["title"] = Meta.title
269185 if hasattr(Meta, "description"):
270186 jsonschema["description"] = Meta.description
187 if hasattr(Meta, "unknown") and Meta.unknown != marshmallow.EXCLUDE:
188 jsonschema["additionalProperties"] = Meta.unknown == marshmallow.INCLUDE
271189
272190 return jsonschema
273191
285203 jsonschema = {"type": "object", "properties": OrderedDict() if ordered else {}}
286204
287205 for field_name, field_obj in fields.items():
288 observed_field_name = self._observed_name(field_obj, field_name)
289 property = self.field2property(field_obj)
290 jsonschema["properties"][observed_field_name] = property
206 observed_field_name = field_obj.data_key or field_name
207 prop = self.field2property(field_obj)
208 jsonschema["properties"][observed_field_name] = prop
291209
292210 if field_obj.required:
293211 if not partial or (
305223 schema in the spec
306224 """
307225 schema_key = make_schema_key(schema)
308 ref_schema = build_reference(
309 "schema", self.openapi_version.major, self.refs[schema_key]
310 )
226 ref_schema = self.spec.components.get_ref("schema", self.refs[schema_key])
311227 if getattr(schema, "many", False):
312228 return {"type": "array", "items": ref_schema}
313229 return ref_schema
1414 self.openapi_version = openapi_version
1515 self.converter = converter
1616
17 def resolve_operations(self, operations, **kwargs):
18 """Resolve marshmallow Schemas in a dict mapping operation to OpenApi `Operation Object
19 https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#operationObject`_"""
20
21 for operation in operations.values():
22 if not isinstance(operation, dict):
23 continue
24 if "parameters" in operation:
25 operation["parameters"] = self.resolve_parameters(
26 operation["parameters"]
27 )
28 if self.openapi_version.major >= 3:
29 self.resolve_callback(operation.get("callbacks", {}))
30 if "requestBody" in operation:
31 self.resolve_schema(operation["requestBody"])
32 for response in operation.get("responses", {}).values():
33 self.resolve_response(response)
34
35 def resolve_callback(self, callbacks):
36 """Resolve marshmallow Schemas in a dict mapping callback name to OpenApi `Callback Object
37 https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#callbackObject`_.
38
39 This is done recursively, so it is possible to define callbacks in your callbacks.
40
41 Example: ::
42
43 #Input
44 {
45 "userEvent": {
46 "https://my.example/user-callback": {
47 "post": {
48 "requestBody": {
49 "content": {
50 "application/json": {
51 "schema": UserSchema
52 }
53 }
54 }
55 },
56 }
57 }
58 }
59
60 #Output
61 {
62 "userEvent": {
63 "https://my.example/user-callback": {
64 "post": {
65 "requestBody": {
66 "content": {
67 "application/json": {
68 "schema": {
69 "$ref": "#/components/schemas/User"
70 }
71 }
72 }
73 }
74 },
75 }
76 }
77 }
78
79
80 """
81 for callback in callbacks.values():
82 if isinstance(callback, dict):
83 for path in callback.values():
84 self.resolve_operations(path)
85
1786 def resolve_parameters(self, parameters):
1887 """Resolve marshmallow Schemas in a list of OpenAPI `Parameter Objects
1988 <https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#parameter-object>`_.
37106
38107 #Output
39108 [
40 {"in": "query", "name": "id", "required": False, "schema": {"type": "integer", "format": "int32"}},
109 {"in": "query", "name": "id", "required": False, "schema": {"type": "integer"}},
41110 {"in": "query", "name": "name", "required": False, "schema": {"type": "string"}}
42111 ]
43112
75144 ):
76145 schema_instance = resolve_schema_instance(parameter.pop("schema"))
77146 resolved += self.converter.schema2parameters(
78 schema_instance, default_in=parameter.pop("in"), **parameter
147 schema_instance, location=parameter.pop("in"), **parameter
79148 )
80149 else:
81150 self.resolve_schema(parameter)
149218 if not isinstance(data, dict):
150219 return
151220
152 # OAS 2 component or OAS 3 header
221 # OAS 2 component or OAS 3 parameter or header
153222 if "schema" in data:
154223 data["schema"] = self.resolve_schema_dict(data["schema"])
155224 # OAS 3 component except header
161230
162231 def resolve_schema_dict(self, schema):
163232 """Resolve a marshmallow Schema class, object, or a string that resolves
164 to a Schema class or an OpenAPI Schema Object containing one of the above
165 to an OpenAPI Schema Object or Reference Object.
233 to a Schema class or a schema reference or an OpenAPI Schema Object
234 containing one of the above to an OpenAPI Schema Object or Reference Object.
166235
167236 If the input is a marshmallow Schema class, object or a string that resolves
168237 to a Schema class the Schema will be translated to an OpenAPI Schema Object
212281 k: self.resolve_schema_dict(v)
213282 for k, v in schema["properties"].items()
214283 }
284 for keyword in ("oneOf", "anyOf", "allOf"):
285 if keyword in schema:
286 schema[keyword] = [
287 self.resolve_schema_dict(s) for s in schema[keyword]
288 ]
289 if "not" in schema:
290 schema["not"] = self.resolve_schema_dict(schema["not"])
215291 return schema
216292
217293 return self.converter.resolve_nested_schema(schema)
1717
1818 :param str name: Identifier by which schema may be referenced
1919 :param dict definition: Schema definition
20 :param dict kwargs: All additional keywords arguments sent to `APISpec.schema()`
20 :param kwargs: All additional keywords arguments sent to `APISpec.schema()`
2121 """
2222 raise PluginMethodNotImplementedError
2323
2525 """May return response component description as a dict.
2626
2727 :param dict response: Response fields
28 :param dict kwargs: All additional keywords arguments sent to `APISpec.response()`
28 :param kwargs: All additional keywords arguments sent to `APISpec.response()`
2929 """
3030 raise PluginMethodNotImplementedError
3131
3333 """May return parameter component description as a dict.
3434
3535 :param dict parameter: Parameter fields
36 :param dict kwargs: All additional keywords arguments sent to `APISpec.parameter()`
36 :param kwargs: All additional keywords arguments sent to `APISpec.parameter()`
37 """
38 raise PluginMethodNotImplementedError
39
40 def header_helper(self, header, **kwargs):
41 """May return header component description as a dict.
42
43 :param dict header: Header fields
44 :param kwargs: All additional keywords arguments sent to `APISpec.header()`
3745 """
3846 raise PluginMethodNotImplementedError
3947
4654 :param list parameters: A `list` of parameters objects or references for the path. See
4755 https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#parameterObject
4856 and https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#referenceObject
49 :param dict kwargs: All additional keywords arguments sent to `APISpec.path()`
57 :param kwargs: All additional keywords arguments sent to `APISpec.path()`
5058
5159 Return value should be a string or None. If a string is returned, it
5260 is set as the path.
6371 :param str path: Path to the resource
6472 :param dict operations: A `dict` mapping HTTP methods to operation object.
6573 See https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#operationObject
66 :param dict kwargs: All additional keywords arguments sent to `APISpec.path()`
74 :param kwargs: All additional keywords arguments sent to `APISpec.path()`
6775 """
6876 raise PluginMethodNotImplementedError
1919 "schema": "schemas",
2020 "response": "responses",
2121 "parameter": "parameters",
22 "header": "headers",
2223 "example": "examples",
2324 "security_scheme": "securitySchemes",
2425 },
102103 < self.MAX_EXCLUSIVE_VERSION
103104 ):
104105 raise exceptions.APISpecError(
105 "Not a valid OpenAPI version number: {}".format(openapi_version)
106 f"Not a valid OpenAPI version number: {openapi_version}"
106107 )
107108 super().__init__(openapi_version)
108109
1414 yaml.add_representer(OrderedDict, YAMLDumper._represent_dict, Dumper=YAMLDumper)
1515
1616
17 def dict_to_yaml(dic):
18 return yaml.dump(dic, Dumper=YAMLDumper)
17 def dict_to_yaml(dic, yaml_dump_kwargs=None):
18 if yaml_dump_kwargs is None:
19 yaml_dump_kwargs = {}
20 return yaml.dump(dic, Dumper=YAMLDumper, **yaml_dump_kwargs)
1921
2022
2123 def load_yaml_from_docstring(docstring):
+0
-0
tests/plugins/__init__.py less more
(Empty file)
+0
-2
tests/plugins/dummy_plugin.py less more
0 def setup(spec):
1 spec.old_plugins["tests.plugins.dummy_plugin"]["foo"] = 42
+0
-0
tests/plugins/dummy_plugin_no_setup.py less more
(Empty file)
00 from marshmallow import Schema, fields
1
2 from apispec.ext.marshmallow.openapi import MARSHMALLOW_VERSION_INFO
31
42
53 class PetSchema(Schema):
64 description = dict(id="Pet id", name="Pet name", password="Password")
7 id = fields.Int(dump_only=True, description=description["id"])
5 id = fields.Int(dump_only=True, metadata={"description": description["id"]})
86 name = fields.Str(
97 required=True,
10 deprecated=False,
11 allowEmptyValue=False,
12 description=description["name"],
8 metadata={
9 "description": description["name"],
10 "deprecated": False,
11 "allowEmptyValue": False,
12 },
1313 )
14 password = fields.Str(load_only=True, description=description["password"])
14 password = fields.Str(
15 load_only=True, metadata={"description": description["password"]}
16 )
1517
1618
1719 class SampleSchema(Schema):
3335
3436
3537 class PatternedObjectSchema(Schema):
36 count = fields.Int(dump_only=True, **{"x-count": 1})
37 count2 = fields.Int(dump_only=True, x_count2=2)
38 count = fields.Int(dump_only=True, metadata={"x-count": 1})
39 count2 = fields.Int(dump_only=True, metadata={"x_count2": 2})
3840
3941
4042 class SelfReferencingSchema(Schema):
4143 id = fields.Int()
42 if MARSHMALLOW_VERSION_INFO[0] < 3:
43 single = fields.Nested("self")
44 many = fields.Nested("self", many=True)
45 else:
46 single = fields.Nested(lambda: SelfReferencingSchema())
47 many = fields.Nested(lambda: SelfReferencingSchema(many=True))
44 single = fields.Nested(lambda: SelfReferencingSchema())
45 many = fields.Nested(lambda: SelfReferencingSchema(many=True))
4846
4947
5048 class OrderedSchema(Schema):
5957
6058
6159 class DefaultValuesSchema(Schema):
62 number_auto_default = fields.Int(missing=12)
63 number_manual_default = fields.Int(missing=12, doc_default=42)
64 string_callable_default = fields.Str(missing=lambda: "Callable")
65 string_manual_default = fields.Str(missing=lambda: "Callable", doc_default="Manual")
66 numbers = fields.List(fields.Int, missing=list)
60 number_auto_default = fields.Int(load_default=12)
61 number_manual_default = fields.Int(load_default=12, metadata={"default": 42})
62 string_callable_default = fields.Str(load_default=lambda: "Callable")
63 string_manual_default = fields.Str(
64 load_default=lambda: "Callable", metadata={"default": "Manual"}
65 )
66 numbers = fields.List(fields.Int, load_default=list)
6767
6868
6969 class CategorySchema(Schema):
0 import copy
01 from collections import OrderedDict
12 from http import HTTPStatus
23
1617 get_examples,
1718 get_paths,
1819 get_parameters,
20 get_headers,
1921 get_responses,
2022 get_security_schemes,
2123 build_ref,
2628 'about Swagger at <a href="http://swagger.wordnik.com">http://swagger.wordnik.com</a> '
2729 "or on irc.freenode.net, #swagger. For this sample, you can use the api "
2830 'key "special-key" to test the authorization filters'
31
32
33 class RefsSchemaTestMixin:
34 REFS_SCHEMA = {
35 "properties": {
36 "nested": "NestedSchema",
37 "deep_nested": {"properties": {"nested": "NestedSchema"}},
38 "nested_list": {"items": "DeepNestedSchema"},
39 "deep_nested_list": {
40 "items": {"properties": {"nested": "DeepNestedSchema"}}
41 },
42 "allof": {
43 "allOf": [
44 "AllOfSchema",
45 {"properties": {"nested": "AllOfSchema"}},
46 ]
47 },
48 "oneof": {
49 "oneOf": [
50 "OneOfSchema",
51 {"properties": {"nested": "OneOfSchema"}},
52 ]
53 },
54 "anyof": {
55 "anyOf": [
56 "AnyOfSchema",
57 {"properties": {"nested": "AnyOfSchema"}},
58 ]
59 },
60 "not": "NotSchema",
61 "deep_not": {"properties": {"nested": "DeepNotSchema"}},
62 }
63 }
64
65 @staticmethod
66 def assert_schema_refs(spec, schema):
67 props = schema["properties"]
68 assert props["nested"] == build_ref(spec, "schema", "NestedSchema")
69 assert props["deep_nested"]["properties"]["nested"] == build_ref(
70 spec, "schema", "NestedSchema"
71 )
72 assert props["nested_list"]["items"] == build_ref(
73 spec, "schema", "DeepNestedSchema"
74 )
75 assert props["deep_nested_list"]["items"]["properties"]["nested"] == build_ref(
76 spec, "schema", "DeepNestedSchema"
77 )
78 assert props["allof"]["allOf"][0] == build_ref(spec, "schema", "AllOfSchema")
79 assert props["allof"]["allOf"][1]["properties"]["nested"] == build_ref(
80 spec, "schema", "AllOfSchema"
81 )
82 assert props["oneof"]["oneOf"][0] == build_ref(spec, "schema", "OneOfSchema")
83 assert props["oneof"]["oneOf"][1]["properties"]["nested"] == build_ref(
84 spec, "schema", "OneOfSchema"
85 )
86 assert props["anyof"]["anyOf"][0] == build_ref(spec, "schema", "AnyOfSchema")
87 assert props["anyof"]["anyOf"][1]["properties"]["nested"] == build_ref(
88 spec, "schema", "AnyOfSchema"
89 )
90 assert props["not"] == build_ref(spec, "schema", "NotSchema")
91 assert props["deep_not"]["properties"]["nested"] == build_ref(
92 spec, "schema", "DeepNotSchema"
93 )
2994
3095
3196 @pytest.fixture(params=("2.0", "3.0.0"))
59124 version="1.0.0",
60125 openapi_version=openapi_version,
61126 info={"description": description},
62 **security_kwargs
127 **security_kwargs,
63128 )
64129
65130
130195 assert spec.to_dict()["tags"] == [{"name": "tag1"}, {"name": "tag2"}]
131196
132197
133 class TestComponents:
198 class TestComponents(RefsSchemaTestMixin):
134199
135200 properties = {
136201 "id": {"type": "integer", "format": "int64"},
201266 spec.components.response("test_response")
202267
203268 def test_parameter(self, spec):
269 # Note: this is an OpenAPI v2 parameter header
270 # but is does the job for the test even for OpenAPI v3
204271 parameter = {"format": "int64", "type": "integer"}
205272 spec.components.parameter("PetId", "path", parameter)
206273 params = get_parameters(spec)
226293 ):
227294 spec.components.parameter("test_parameter", "path")
228295
296 # Referenced headers are only supported in OAS 3.x
297 @pytest.mark.parametrize("spec", ("3.0.0",), indirect=True)
298 def test_header(self, spec):
299 header = {"schema": {"type": "string"}}
300 spec.components.header("test_header", header.copy())
301 headers = get_headers(spec)
302 assert headers["test_header"] == header
303
304 # Referenced headers are only supported in OAS 3.x
305 @pytest.mark.parametrize("spec", ("3.0.0",), indirect=True)
306 def test_header_is_chainable(self, spec):
307 header = {"schema": {"type": "string"}}
308 spec.components.header("header1", header).header("header2", header)
309 headers = get_headers(spec)
310 assert "header1" in headers
311 assert "header2" in headers
312
313 # Referenced headers are only supported in OAS 3.x
314 @pytest.mark.parametrize("spec", ("3.0.0",), indirect=True)
315 def test_header_duplicate_name(self, spec):
316 spec.components.header("test_header", {"schema": {"type": "string"}})
317 with pytest.raises(
318 DuplicateComponentNameError,
319 match='Another header with name "test_header" is already registered.',
320 ):
321 spec.components.header("test_header", {"schema": {"type": "integer"}})
322
229323 # Referenced examples are only supported in OAS 3.x
230324 @pytest.mark.parametrize("spec", ("3.0.0",), indirect=True)
231325 def test_example(self, spec):
290384 "TestSchema": {"properties": {"key": {"type": "string"}}, "type": "object"}
291385 }
292386
293
294 class TestPath:
387 def test_components_resolve_refs_in_schema(self, spec):
388 spec.components.schema("refs_schema", copy.deepcopy(self.REFS_SCHEMA))
389 self.assert_schema_refs(spec, get_schemas(spec)["refs_schema"])
390
391 def test_components_resolve_response_schema(self, spec):
392 schema = {"schema": "PetSchema"}
393 if spec.openapi_version.major >= 3:
394 schema = {"content": {"application/json": schema}}
395 spec.components.response("Response", schema)
396 resp = get_responses(spec)["Response"]
397 if spec.openapi_version.major < 3:
398 schema = resp["schema"]
399 else:
400 schema = resp["content"]["application/json"]["schema"]
401 assert schema == build_ref(spec, "schema", "PetSchema")
402
403 # "headers" components section only exists in OAS 3
404 @pytest.mark.parametrize("spec", ("3.0.0",), indirect=True)
405 def test_components_resolve_response_header(self, spec):
406 response = {"headers": {"header_1": "Header_1"}}
407 spec.components.response("Response", response)
408 resp = get_responses(spec)["Response"]
409 header_1 = resp["headers"]["header_1"]
410 assert header_1 == build_ref(spec, "header", "Header_1")
411
412 # "headers" components section only exists in OAS 3
413 @pytest.mark.parametrize("spec", ("3.0.0",), indirect=True)
414 def test_components_resolve_response_header_schema(self, spec):
415 response = {"headers": {"header_1": {"name": "Pet", "schema": "PetSchema"}}}
416 spec.components.response("Response", response)
417 resp = get_responses(spec)["Response"]
418 header_1 = resp["headers"]["header_1"]
419 assert header_1["schema"] == build_ref(spec, "schema", "PetSchema")
420
421 # "headers" components section only exists in OAS 3
422 @pytest.mark.parametrize("spec", ("3.0.0",), indirect=True)
423 def test_components_resolve_response_header_examples(self, spec):
424 response = {
425 "headers": {
426 "header_1": {"name": "Pet", "examples": {"example_1": "Example_1"}}
427 }
428 }
429 spec.components.response("Response", response)
430 resp = get_responses(spec)["Response"]
431 header_1 = resp["headers"]["header_1"]
432 assert header_1["examples"]["example_1"] == build_ref(
433 spec, "example", "Example_1"
434 )
435
436 # "examples" components section only exists in OAS 3
437 @pytest.mark.parametrize("spec", ("3.0.0",), indirect=True)
438 def test_components_resolve_response_examples(self, spec):
439 response = {
440 "content": {"application/json": {"examples": {"example_1": "Example_1"}}}
441 }
442 spec.components.response("Response", response)
443 resp = get_responses(spec)["Response"]
444 example_1 = resp["content"]["application/json"]["examples"]["example_1"]
445 assert example_1 == build_ref(spec, "example", "Example_1")
446
447 def test_components_resolve_refs_in_response_schema(self, spec):
448 schema = copy.deepcopy(self.REFS_SCHEMA)
449 if spec.openapi_version.major >= 3:
450 response = {"content": {"application/json": {"schema": schema}}}
451 else:
452 response = {"schema": schema}
453 spec.components.response("Response", response)
454 resp = get_responses(spec)["Response"]
455 if spec.openapi_version.major < 3:
456 schema = resp["schema"]
457 else:
458 schema = resp["content"]["application/json"]["schema"]
459 self.assert_schema_refs(spec, schema)
460
461 # "headers" components section only exists in OAS 3
462 @pytest.mark.parametrize("spec", ("3.0.0",), indirect=True)
463 def test_components_resolve_refs_in_response_header_schema(self, spec):
464 header = {"schema": copy.deepcopy(self.REFS_SCHEMA)}
465 response = {"headers": {"header": header}}
466 spec.components.response("Response", response)
467 resp = get_responses(spec)["Response"]
468 self.assert_schema_refs(spec, resp["headers"]["header"]["schema"])
469
470 # "examples" components section only exists in OAS 3
471 @pytest.mark.parametrize("spec", ("3.0.0",), indirect=True)
472 def test_components_resolve_parameter_examples(self, spec):
473 parameter = {
474 "examples": {"example_1": "Example_1"},
475 }
476 spec.components.parameter("param", "path", parameter)
477 param = get_parameters(spec)["param"]
478 example_1 = param["examples"]["example_1"]
479 assert example_1 == build_ref(spec, "example", "Example_1")
480
481 def test_components_resolve_parameter_schemas(self, spec):
482 parameter = {"schema": "PetSchema"}
483 spec.components.parameter("param", "path", parameter)
484 param = get_parameters(spec)["param"]
485 assert param["schema"] == build_ref(spec, "schema", "PetSchema")
486
487 def test_components_resolve_refs_in_parameter_schema(self, spec):
488 parameter = {"schema": copy.deepcopy(self.REFS_SCHEMA)}
489 spec.components.parameter("param", "path", parameter)
490 self.assert_schema_refs(spec, get_parameters(spec)["param"]["schema"])
491
492 # "headers" components section only exists in OAS 3
493 @pytest.mark.parametrize("spec", ("3.0.0",), indirect=True)
494 def test_components_resolve_header_schema(self, spec):
495 header = {"name": "Pet", "schema": "PetSchema"}
496 spec.components.header("header", header)
497 header = get_headers(spec)["header"]
498 assert header["schema"] == build_ref(spec, "schema", "PetSchema")
499
500 # "headers" components section only exists in OAS 3
501 @pytest.mark.parametrize("spec", ("3.0.0",), indirect=True)
502 def test_components_resolve_header_examples(self, spec):
503 header = {"name": "Pet", "examples": {"example_1": "Example_1"}}
504 spec.components.header("header", header)
505 header = get_headers(spec)["header"]
506 assert header["examples"]["example_1"] == build_ref(
507 spec, "example", "Example_1"
508 )
509
510 # "headers" components section only exists in OAS 3
511 @pytest.mark.parametrize("spec", ("3.0.0",), indirect=True)
512 def test_components_resolve_refs_in_header_schema(self, spec):
513 header = {"schema": copy.deepcopy(self.REFS_SCHEMA)}
514 spec.components.header("header", header)
515 self.assert_schema_refs(spec, get_headers(spec)["header"]["schema"])
516
517 def test_schema_lazy(self, spec):
518 spec.components.schema("Pet_1", {"properties": self.properties}, lazy=False)
519 spec.components.schema("Pet_2", {"properties": self.properties}, lazy=True)
520 schemas = get_schemas(spec)
521 assert "Pet_1" in schemas
522 assert "Pet_2" not in schemas
523 spec.components.schema("PetFriend", {"oneOf": ["Pet_1", "Pet_2"]})
524 schemas = get_schemas(spec)
525 assert "Pet_2" in schemas
526 assert schemas["Pet_2"]["properties"] == self.properties
527
528 def test_response_lazy(self, spec):
529 response_1 = {"description": "Reponse 1"}
530 response_2 = {"description": "Reponse 2"}
531 spec.components.response("Response_1", response_1, lazy=False)
532 spec.components.response("Response_2", response_2, lazy=True)
533 responses = get_responses(spec)
534 assert "Response_1" in responses
535 assert "Response_2" not in responses
536 spec.path("/path", operations={"get": {"responses": {"200": "Response_2"}}})
537 responses = get_responses(spec)
538 assert "Response_2" in responses
539
540 def test_parameter_lazy(self, spec):
541 parameter = {"format": "int64", "type": "integer"}
542 spec.components.parameter("Param_1", "path", parameter, lazy=False)
543 spec.components.parameter("Param_2", "path", parameter, lazy=True)
544 params = get_parameters(spec)
545 assert "Param_1" in params
546 assert "Param_2" not in params
547 spec.path("/path", operations={"get": {"parameters": ["Param_1", "Param_2"]}})
548 assert "Param_2" in params
549
550 # Referenced headers are only supported in OAS 3.x
551 @pytest.mark.parametrize("spec", ("3.0.0",), indirect=True)
552 def test_header_lazy(self, spec):
553 header = {"schema": {"type": "string"}}
554 spec.components.header("Header_1", header, lazy=False)
555 spec.components.header("Header_2", header, lazy=True)
556 headers = get_headers(spec)
557 assert "Header_1" in headers
558 assert "Header_2" not in headers
559 spec.path(
560 "/path",
561 operations={
562 "get": {"responses": {"200": {"headers": {"header_2": "Header_2"}}}}
563 },
564 )
565 assert "Header_2" in headers
566
567 # Referenced examples are only supported in OAS 3.x
568 @pytest.mark.parametrize("spec", ("3.0.0",), indirect=True)
569 def test_example_lazy(self, spec):
570 spec.components.example("Example_1", {"value": {"a": "b"}}, lazy=False)
571 spec.components.example("Example_2", {"value": {"a": "b"}}, lazy=True)
572 examples = get_examples(spec)
573 assert "Example_1" in examples
574 assert "Example_2" not in examples
575 spec.path(
576 "/path",
577 operations={
578 "get": {
579 "responses": {
580 "200": {
581 "content": {
582 "application/json": {
583 "examples": {"example_2": "Example_2"}
584 }
585 }
586 }
587 }
588 }
589 },
590 )
591 assert "Example_2" in examples
592
593
594 class TestPath(RefsSchemaTestMixin):
295595 paths = {
296596 "/pet/{petId}": {
297597 "get": {
306606 }
307607 ],
308608 "responses": {
309 "200": {"schema": "Pet", "description": "successful operation"},
609 "200": {"description": "successful operation"},
310610 "400": {"description": "Invalid ID supplied"},
311611 "404": {"description": "Pet not found"},
312612 },
359659 "/path4",
360660 ]
361661
362 def test_paths_is_chainable(self, spec):
662 def test_path_is_chainable(self, spec):
363663 spec.path(path="/path1").path("/path2")
364664 assert list(spec.to_dict()["paths"].keys()) == ["/path1", "/path2"]
365665
366 def test_methods_maintain_order(self, spec):
666 def test_path_methods_maintain_order(self, spec):
367667 methods = ["get", "post", "put", "patch", "delete", "head", "options"]
368668 for method in methods:
369669 spec.path(path="/path", operations=OrderedDict({method: {}}))
461761 assert p["summary"] == summary
462762 assert p["description"] == description
463763
464 def test_parameter(self, spec):
764 def test_path_resolves_parameter(self, spec):
465765 route_spec = self.paths["/pet/{petId}"]["get"]
466
467766 spec.components.parameter("test_parameter", "path", route_spec["parameters"][0])
468
469767 spec.path(
470768 path="/pet/{petId}", operations={"get": {"parameters": ["test_parameter"]}}
471769 )
472
473 metadata = spec.to_dict()
474770 p = get_paths(spec)["/pet/{petId}"]["get"]
475
476771 assert p["parameters"][0] == build_ref(spec, "parameter", "test_parameter")
477 if spec.openapi_version.major < 3:
478 assert (
479 route_spec["parameters"][0] == metadata["parameters"]["test_parameter"]
480 )
481 else:
482 assert (
483 route_spec["parameters"][0]
484 == metadata["components"]["parameters"]["test_parameter"]
485 )
486772
487773 @pytest.mark.parametrize(
488774 "parameters",
489775 ([{"name": "petId"}], [{"in": "path"}]), # missing "in" # missing "name"
490776 )
491 def test_invalid_parameter(self, spec, parameters):
777 def test_path_invalid_parameter(self, spec, parameters):
492778 path = "/pet/{petId}"
493779
494780 with pytest.raises(InvalidParameterError):
563849 ],
564850 )
565851
566 def test_response(self, spec):
852 def test_path_resolves_response(self, spec):
567853 route_spec = self.paths["/pet/{petId}"]["get"]
568
569854 spec.components.response("test_response", route_spec["responses"]["200"])
570
571855 spec.path(
572856 path="/pet/{petId}",
573857 operations={"get": {"responses": {"200": "test_response"}}},
574858 )
575
576 metadata = spec.to_dict()
577859 p = get_paths(spec)["/pet/{petId}"]["get"]
578
579860 assert p["responses"]["200"] == build_ref(spec, "response", "test_response")
580 if spec.openapi_version.major < 3:
581 assert (
582 route_spec["responses"]["200"] == metadata["responses"]["test_response"]
583 )
584 else:
585 assert (
586 route_spec["responses"]["200"]
587 == metadata["components"]["responses"]["test_response"]
588 )
589
590 def test_response_with_HTTPStatus_code(self, spec):
861
862 def test_path_response_with_HTTPStatus_code(self, spec):
591863 code = HTTPStatus(200)
592864 spec.path(
593865 path="/pet/{petId}",
596868
597869 assert "200" in get_paths(spec)["/pet/{petId}"]["get"]["responses"]
598870
599 def test_response_with_status_code_range(self, spec, recwarn):
871 def test_path_response_with_status_code_range(self, spec, recwarn):
600872 status_code = "2XX"
601873
602874 spec.path(
617889 with pytest.raises(APISpecError, match=message):
618890 spec.path("/pet/{petId}", operations={"dummy": {}})
619891
892 def test_path_resolve_response_schema(self, spec):
893 schema = {"schema": "PetSchema"}
894 if spec.openapi_version.major >= 3:
895 schema = {"content": {"application/json": schema}}
896 spec.path("/pet/{petId}", operations={"get": {"responses": {"200": schema}}})
897 resp = get_paths(spec)["/pet/{petId}"]["get"]["responses"]["200"]
898 if spec.openapi_version.major < 3:
899 schema = resp["schema"]
900 else:
901 schema = resp["content"]["application/json"]["schema"]
902 assert schema == build_ref(spec, "schema", "PetSchema")
903
904 # requestBody only exists in OAS 3
905 @pytest.mark.parametrize("spec", ("3.0.0",), indirect=True)
906 def test_path_resolve_request_body(self, spec):
907 spec.path(
908 "/pet/{petId}",
909 operations={
910 "get": {
911 "requestBody": {
912 "content": {"application/json": {"schema": "PetSchema"}}
913 }
914 }
915 },
916 )
917 assert get_paths(spec)["/pet/{petId}"]["get"]["requestBody"]["content"][
918 "application/json"
919 ]["schema"] == build_ref(spec, "schema", "PetSchema")
920
921 # "headers" components section only exists in OAS 3
922 @pytest.mark.parametrize("spec", ("3.0.0",), indirect=True)
923 def test_path_resolve_response_header(self, spec):
924 response = {"headers": {"header_1": "Header_1"}}
925 spec.path("/pet/{petId}", operations={"get": {"responses": {"200": response}}})
926 resp = get_paths(spec)["/pet/{petId}"]["get"]["responses"]["200"]
927 header_1 = resp["headers"]["header_1"]
928 assert header_1 == build_ref(spec, "header", "Header_1")
929
930 # "headers" components section only exists in OAS 3
931 @pytest.mark.parametrize("spec", ("3.0.0",), indirect=True)
932 def test_path_resolve_response_header_schema(self, spec):
933 response = {"headers": {"header_1": {"name": "Pet", "schema": "PetSchema"}}}
934 spec.path("/pet/{petId}", operations={"get": {"responses": {"200": response}}})
935 resp = get_paths(spec)["/pet/{petId}"]["get"]["responses"]["200"]
936 header_1 = resp["headers"]["header_1"]
937 assert header_1["schema"] == build_ref(spec, "schema", "PetSchema")
938
939 # "headers" components section only exists in OAS 3
940 @pytest.mark.parametrize("spec", ("3.0.0",), indirect=True)
941 def test_path_resolve_response_header_examples(self, spec):
942 response = {
943 "headers": {
944 "header_1": {"name": "Pet", "examples": {"example_1": "Example_1"}}
945 }
946 }
947 spec.path("/pet/{petId}", operations={"get": {"responses": {"200": response}}})
948 resp = get_paths(spec)["/pet/{petId}"]["get"]["responses"]["200"]
949 header_1 = resp["headers"]["header_1"]
950 assert header_1["examples"]["example_1"] == build_ref(
951 spec, "example", "Example_1"
952 )
953
954 # "examples" components section only exists in OAS 3
955 @pytest.mark.parametrize("spec", ("3.0.0",), indirect=True)
956 def test_path_resolve_response_examples(self, spec):
957 response = {
958 "content": {"application/json": {"examples": {"example_1": "Example_1"}}}
959 }
960 spec.path("/pet/{petId}", operations={"get": {"responses": {"200": response}}})
961 resp = get_paths(spec)["/pet/{petId}"]["get"]["responses"]["200"]
962 example_1 = resp["content"]["application/json"]["examples"]["example_1"]
963 assert example_1 == build_ref(spec, "example", "Example_1")
964
965 # "examples" components section only exists in OAS 3
966 @pytest.mark.parametrize("spec", ("3.0.0",), indirect=True)
967 def test_path_resolve_request_body_examples(self, spec):
968 request_body = {
969 "content": {"application/json": {"examples": {"example_1": "Example_1"}}}
970 }
971 spec.path("/pet/{petId}", operations={"get": {"requestBody": request_body}})
972 reqbdy = get_paths(spec)["/pet/{petId}"]["get"]["requestBody"]
973 example_1 = reqbdy["content"]["application/json"]["examples"]["example_1"]
974 assert example_1 == build_ref(spec, "example", "Example_1")
975
976 # "examples" components section only exists in OAS 3
977 @pytest.mark.parametrize("spec", ("3.0.0",), indirect=True)
978 def test_path_resolve_parameter_examples(self, spec):
979 parameter = {
980 "name": "test",
981 "in": "query",
982 "examples": {"example_1": "Example_1"},
983 }
984 spec.path("/pet/{petId}", operations={"get": {"parameters": [parameter]}})
985 param = get_paths(spec)["/pet/{petId}"]["get"]["parameters"][0]
986 example_1 = param["examples"]["example_1"]
987 assert example_1 == build_ref(spec, "example", "Example_1")
988
989 def test_path_resolve_parameter_schemas(self, spec):
990 parameter = {"name": "test", "in": "query", "schema": "PetSchema"}
991 spec.path("/pet/{petId}", operations={"get": {"parameters": [parameter]}})
992 param = get_paths(spec)["/pet/{petId}"]["get"]["parameters"][0]
993 assert param["schema"] == build_ref(spec, "schema", "PetSchema")
994
995 def test_path_resolve_refs_in_response_schema(self, spec):
996 if spec.openapi_version.major >= 3:
997 schema = {"content": {"application/json": {"schema": self.REFS_SCHEMA}}}
998 else:
999 schema = {"schema": self.REFS_SCHEMA}
1000 spec.path("/pet/{petId}", operations={"get": {"responses": {"200": schema}}})
1001 resp = get_paths(spec)["/pet/{petId}"]["get"]["responses"]["200"]
1002 if spec.openapi_version.major < 3:
1003 schema = resp["schema"]
1004 else:
1005 schema = resp["content"]["application/json"]["schema"]
1006 self.assert_schema_refs(spec, schema)
1007
1008 def test_path_resolve_refs_in_parameter_schema(self, spec):
1009 schema = copy.copy({"schema": self.REFS_SCHEMA})
1010 schema["in"] = "query"
1011 schema["name"] = "test"
1012 spec.path("/pet/{petId}", operations={"get": {"parameters": [schema]}})
1013 schema = get_paths(spec)["/pet/{petId}"]["get"]["parameters"][0]["schema"]
1014 self.assert_schema_refs(spec, schema)
1015
1016 # requestBody only exists in OAS 3
1017 @pytest.mark.parametrize("spec", ("3.0.0",), indirect=True)
1018 def test_path_resolve_refs_in_request_body_schema(self, spec):
1019 schema = {"content": {"application/json": {"schema": self.REFS_SCHEMA}}}
1020 spec.path("/pet/{petId}", operations={"get": {"responses": {"200": schema}}})
1021 resp = get_paths(spec)["/pet/{petId}"]["get"]["responses"]["200"]
1022 schema = resp["content"]["application/json"]["schema"]
1023 self.assert_schema_refs(spec, schema)
1024
6201025
6211026 class TestPlugins:
6221027 @staticmethod
6231028 def test_plugin_factory(return_none=False):
6241029 class TestPlugin(BasePlugin):
1030 """Test Plugin
1031
1032 return_none allows to check plugin helpers returning ``None``
1033 Inputs are mutated to allow testing only a copy is passed.
1034 """
1035
6251036 def schema_helper(self, name, definition, **kwargs):
1037 definition.pop("dummy", None)
6261038 if not return_none:
6271039 return {"properties": {"name": {"type": "string"}}}
6281040
6291041 def parameter_helper(self, parameter, **kwargs):
1042 parameter.pop("dummy", None)
6301043 if not return_none:
6311044 return {"description": "some parameter"}
6321045
6331046 def response_helper(self, response, **kwargs):
1047 response.pop("dummy", None)
6341048 if not return_none:
6351049 return {"description": "42"}
1050
1051 def header_helper(self, header, **kwargs):
1052 header.pop("dummy", None)
1053 if not return_none:
1054 return {"description": "some header"}
6361055
6371056 def path_helper(self, path, operations, parameters, **kwargs):
6381057 if not return_none:
6561075 openapi_version=openapi_version,
6571076 plugins=(self.test_plugin_factory(return_none),),
6581077 )
659 spec.components.schema("Pet")
1078 schema = {"dummy": "dummy"}
1079 spec.components.schema("Pet", schema)
6601080 definitions = get_schemas(spec)
6611081 if return_none:
6621082 assert definitions["Pet"] == {}
6631083 else:
6641084 assert definitions["Pet"] == {"properties": {"name": {"type": "string"}}}
1085 # Check original schema is not modified
1086 assert schema == {"dummy": "dummy"}
6651087
6661088 @pytest.mark.parametrize("openapi_version", ("2.0", "3.0.0"))
6671089 @pytest.mark.parametrize("return_none", (True, False))
6721094 openapi_version=openapi_version,
6731095 plugins=(self.test_plugin_factory(return_none),),
6741096 )
675 spec.components.parameter("Pet", "body", {})
1097 parameter = {"dummy": "dummy"}
1098 spec.components.parameter("Pet", "body", parameter)
6761099 parameters = get_parameters(spec)
6771100 if return_none:
6781101 assert parameters["Pet"] == {"in": "body", "name": "Pet"}
6821105 "name": "Pet",
6831106 "description": "some parameter",
6841107 }
1108 # Check original parameter is not modified
1109 assert parameter == {"dummy": "dummy"}
6851110
6861111 @pytest.mark.parametrize("openapi_version", ("2.0", "3.0.0"))
6871112 @pytest.mark.parametrize("return_none", (True, False))
6921117 openapi_version=openapi_version,
6931118 plugins=(self.test_plugin_factory(return_none),),
6941119 )
695 spec.components.response("Pet", {})
1120 response = {"dummy": "dummy"}
1121 spec.components.response("Pet", response)
6961122 responses = get_responses(spec)
6971123 if return_none:
6981124 assert responses["Pet"] == {}
6991125 else:
7001126 assert responses["Pet"] == {"description": "42"}
1127 # Check original response is not modified
1128 assert response == {"dummy": "dummy"}
1129
1130 @pytest.mark.parametrize("openapi_version", ("3.0.0",))
1131 @pytest.mark.parametrize("return_none", (True, False))
1132 def test_plugin_header_helper_is_used(self, openapi_version, return_none):
1133 spec = APISpec(
1134 title="Swagger Petstore",
1135 version="1.0.0",
1136 openapi_version=openapi_version,
1137 plugins=(self.test_plugin_factory(return_none),),
1138 )
1139 header = {"dummy": "dummy"}
1140 spec.components.header("Pet", header)
1141 headers = get_headers(spec)
1142 if return_none:
1143 assert headers["Pet"] == {}
1144 else:
1145 assert headers["Pet"] == {
1146 "description": "some header",
1147 }
1148 # Check original header is not modified
1149 assert header == {"dummy": "dummy"}
7011150
7021151 @pytest.mark.parametrize("openapi_version", ("2.0", "3.0.0"))
7031152 @pytest.mark.parametrize("return_none", (True, False))
7411190 self.output = output
7421191
7431192 def path_helper(self, path, operations, **kwargs):
744 self.output.append("plugin_{}_path".format(self.index))
1193 self.output.append(f"plugin_{self.index}_path")
7451194
7461195 def operation_helper(self, path, operations, **kwargs):
747 self.output.append("plugin_{}_operations".format(self.index))
1196 self.output.append(f"plugin_{self.index}_operations")
7481197
7491198 def test_plugins_order(self):
7501199 """Test plugins execution order in APISpec.path
11
22 import pytest
33
4 from marshmallow.fields import Field, DateTime, Dict, String, Nested, List
4 from marshmallow.fields import Field, DateTime, Dict, String, Nested, List, TimeDelta
55 from marshmallow import Schema
66
77 from apispec import APISpec
88 from apispec.ext.marshmallow import MarshmallowPlugin
9 from apispec.ext.marshmallow.openapi import MARSHMALLOW_VERSION_INFO
109 from apispec.ext.marshmallow import common
1110 from apispec.exceptions import APISpecError
1211 from .schemas import (
1312 PetSchema,
13 SampleSchema,
1414 AnalysisSchema,
1515 RunSchema,
1616 SelfReferencingSchema,
2020 AnalysisWithListSchema,
2121 )
2222
23 from .utils import get_schemas, get_parameters, get_responses, get_paths, build_ref
23 from .utils import (
24 get_schemas,
25 get_parameters,
26 get_responses,
27 get_headers,
28 get_paths,
29 build_ref,
30 )
2431
2532
2633 class TestDefinitionHelper:
219226 class TestComponentParameterHelper:
220227 @pytest.mark.parametrize("schema", [PetSchema, PetSchema()])
221228 def test_can_use_schema_in_parameter(self, spec, schema):
222 if spec.openapi_version.major < 3:
223 param = {"schema": schema}
224 else:
225 param = {"content": {"application/json": {"schema": schema}}}
229 param = {"schema": schema}
226230 spec.components.parameter("Pet", "body", param)
227231 parameter = get_parameters(spec)["Pet"]
228232 assert parameter["in"] == "body"
229 if spec.openapi_version.major < 3:
230 reference = parameter["schema"]
231 else:
232 reference = parameter["content"]["application/json"]["schema"]
233 reference = parameter["schema"]
233234 assert reference == build_ref(spec, "schema", "Pet")
234235
235 resolved_schema = spec.components._schemas["Pet"]
236 resolved_schema = spec.components.schemas["Pet"]
237 assert resolved_schema["properties"]["name"]["type"] == "string"
238 assert resolved_schema["properties"]["password"]["type"] == "string"
239 assert resolved_schema["properties"]["id"]["type"] == "integer"
240
241 @pytest.mark.parametrize("spec", ("3.0.0",), indirect=True)
242 @pytest.mark.parametrize("schema", [PetSchema, PetSchema()])
243 def test_can_use_schema_in_parameter_with_content(self, spec, schema):
244 param = {"content": {"application/json": {"schema": schema}}}
245 spec.components.parameter("Pet", "body", param)
246 parameter = get_parameters(spec)["Pet"]
247 assert parameter["in"] == "body"
248 reference = parameter["content"]["application/json"]["schema"]
249 assert reference == build_ref(spec, "schema", "Pet")
250
251 resolved_schema = spec.components.schemas["Pet"]
236252 assert resolved_schema["properties"]["name"]["type"] == "string"
237253 assert resolved_schema["properties"]["password"]["type"] == "string"
238254 assert resolved_schema["properties"]["id"]["type"] == "integer"
253269 reference = response["content"]["application/json"]["schema"]
254270 assert reference == build_ref(spec, "schema", "Pet")
255271
256 resolved_schema = spec.components._schemas["Pet"]
272 resolved_schema = spec.components.schemas["Pet"]
257273 assert resolved_schema["properties"]["id"]["type"] == "integer"
258274 assert resolved_schema["properties"]["name"]["type"] == "string"
259275 assert resolved_schema["properties"]["password"]["type"] == "string"
266282 reference = response["headers"]["PetHeader"]["schema"]
267283 assert reference == build_ref(spec, "schema", "Pet")
268284
269 resolved_schema = spec.components._schemas["Pet"]
285 resolved_schema = spec.components.schemas["Pet"]
270286 assert resolved_schema["properties"]["id"]["type"] == "integer"
271287 assert resolved_schema["properties"]["name"]["type"] == "string"
272288 assert resolved_schema["properties"]["password"]["type"] == "string"
279295 assert response == resp
280296
281297
298 class TestComponentHeaderHelper:
299 @pytest.mark.parametrize("spec", ("3.0.0",), indirect=True)
300 @pytest.mark.parametrize("schema", [PetSchema, PetSchema()])
301 def test_can_use_schema_in_header(self, spec, schema):
302 param = {"schema": schema}
303 spec.components.header("Pet", param)
304 header = get_headers(spec)["Pet"]
305 reference = header["schema"]
306 assert reference == build_ref(spec, "schema", "Pet")
307
308 resolved_schema = spec.components.schemas["Pet"]
309 assert resolved_schema["properties"]["name"]["type"] == "string"
310 assert resolved_schema["properties"]["password"]["type"] == "string"
311 assert resolved_schema["properties"]["id"]["type"] == "integer"
312
313
282314 class TestCustomField:
283315 def test_can_use_custom_field_decorator(self, spec_fixture):
284316 @spec_fixture.marshmallow_plugin.map_to_openapi_type(DateTime)
327359
328360
329361 class TestOperationHelper:
362 @pytest.fixture
363 def make_pet_callback_spec(self, spec_fixture):
364 def _make_pet_spec(operations):
365 spec_fixture.spec.path(
366 path="/pet",
367 operations={
368 "post": {"callbacks": {"petEvent": {"petCallbackUrl": operations}}}
369 },
370 )
371 return spec_fixture
372
373 return _make_pet_spec
374
330375 @pytest.mark.parametrize(
331376 "pet_schema",
332377 (PetSchema, PetSchema(), PetSchema(many=True), "tests.schemas.PetSchema"),
363408 header_reference = get["responses"]["200"]["headers"]["PetHeader"]["schema"]
364409 assert schema_reference == build_ref(spec_fixture.spec, "schema", "Pet")
365410 assert header_reference == build_ref(spec_fixture.spec, "schema", "Pet")
366 assert len(spec_fixture.spec.components._schemas) == 1
367 resolved_schema = spec_fixture.spec.components._schemas["Pet"]
411 assert len(spec_fixture.spec.components.schemas) == 1
412 resolved_schema = spec_fixture.spec.components.schemas["Pet"]
368413 assert resolved_schema == spec_fixture.openapi.schema2jsonschema(PetSchema)
369414 assert get["responses"]["200"]["description"] == "successful operation"
370415
412457
413458 assert schema_reference == build_ref(spec_fixture.spec, "schema", "Pet")
414459 assert header_reference == build_ref(spec_fixture.spec, "schema", "Pet")
415 assert len(spec_fixture.spec.components._schemas) == 1
416 resolved_schema = spec_fixture.spec.components._schemas["Pet"]
460 assert len(spec_fixture.spec.components.schemas) == 1
461 resolved_schema = spec_fixture.spec.components.schemas["Pet"]
462 assert resolved_schema == spec_fixture.openapi.schema2jsonschema(PetSchema)
463 assert get["responses"]["200"]["description"] == "successful operation"
464
465 @pytest.mark.parametrize(
466 "pet_schema",
467 (PetSchema, PetSchema(), PetSchema(many=True), "tests.schemas.PetSchema"),
468 )
469 @pytest.mark.parametrize("spec_fixture", ("3.0.0",), indirect=True)
470 def test_callback_schema_v3(self, make_pet_callback_spec, pet_schema):
471 spec_fixture = make_pet_callback_spec(
472 {
473 "get": {
474 "responses": {
475 "200": {
476 "content": {"application/json": {"schema": pet_schema}},
477 "description": "successful operation",
478 "headers": {"PetHeader": {"schema": pet_schema}},
479 }
480 }
481 }
482 }
483 )
484 p = get_paths(spec_fixture.spec)["/pet"]
485 c = p["post"]["callbacks"]["petEvent"]["petCallbackUrl"]
486 get = c["get"]
487 if isinstance(pet_schema, Schema) and pet_schema.many is True:
488 assert (
489 get["responses"]["200"]["content"]["application/json"]["schema"]["type"]
490 == "array"
491 )
492 schema_reference = get["responses"]["200"]["content"]["application/json"][
493 "schema"
494 ]["items"]
495 assert (
496 get["responses"]["200"]["headers"]["PetHeader"]["schema"]["type"]
497 == "array"
498 )
499 header_reference = get["responses"]["200"]["headers"]["PetHeader"][
500 "schema"
501 ]["items"]
502 else:
503 schema_reference = get["responses"]["200"]["content"]["application/json"][
504 "schema"
505 ]
506 header_reference = get["responses"]["200"]["headers"]["PetHeader"]["schema"]
507
508 assert schema_reference == build_ref(spec_fixture.spec, "schema", "Pet")
509 assert header_reference == build_ref(spec_fixture.spec, "schema", "Pet")
510 assert len(spec_fixture.spec.components.schemas) == 1
511 resolved_schema = spec_fixture.spec.components.schemas["Pet"]
417512 assert resolved_schema == spec_fixture.openapi.schema2jsonschema(PetSchema)
418513 assert get["responses"]["200"]["description"] == "successful operation"
419514
439534 p = get_paths(spec_fixture.spec)["/pet"]
440535 get = p["get"]
441536 assert get["parameters"] == spec_fixture.openapi.schema2parameters(
442 PetSchema(), default_in="query"
537 PetSchema(), location="query"
443538 )
444539 post = p["post"]
445540 assert post["parameters"] == spec_fixture.openapi.schema2parameters(
446541 PetSchema,
447 default_in="body",
542 location="body",
448543 required=True,
449544 name="pet",
450545 description="a pet schema",
468563 p = get_paths(spec_fixture.spec)["/pet"]
469564 get = p["get"]
470565 assert get["parameters"] == spec_fixture.openapi.schema2parameters(
471 PetSchema(), default_in="query"
566 PetSchema(), location="query"
472567 )
473568 for parameter in get["parameters"]:
474569 description = parameter.get("description", False)
485580 assert post["requestBody"]["description"] == "a pet schema"
486581 assert post["requestBody"]["required"]
487582
583 @pytest.mark.parametrize("spec_fixture", ("3.0.0",), indirect=True)
584 def test_callback_schema_expand_parameters_v3(self, make_pet_callback_spec):
585 spec_fixture = make_pet_callback_spec(
586 {
587 "get": {"parameters": [{"in": "query", "schema": PetSchema}]},
588 "post": {
589 "requestBody": {
590 "description": "a pet schema",
591 "required": True,
592 "content": {"application/json": {"schema": PetSchema}},
593 }
594 },
595 }
596 )
597 p = get_paths(spec_fixture.spec)["/pet"]
598 c = p["post"]["callbacks"]["petEvent"]["petCallbackUrl"]
599 get = c["get"]
600 assert get["parameters"] == spec_fixture.openapi.schema2parameters(
601 PetSchema(), location="query"
602 )
603 for parameter in get["parameters"]:
604 description = parameter.get("description", False)
605 assert description
606 name = parameter["name"]
607 assert description == PetSchema.description[name]
608 post = c["post"]
609 post_schema = spec_fixture.marshmallow_plugin.resolver.resolve_schema_dict(
610 PetSchema
611 )
612 assert (
613 post["requestBody"]["content"]["application/json"]["schema"] == post_schema
614 )
615 assert post["requestBody"]["description"] == "a pet schema"
616 assert post["requestBody"]["required"]
617
488618 @pytest.mark.parametrize("spec_fixture", ("2.0",), indirect=True)
489619 def test_schema_uses_ref_if_available_v2(self, spec_fixture):
490620 spec_fixture.spec.components.schema("Pet", schema=PetSchema)
510640 },
511641 )
512642 get = get_paths(spec_fixture.spec)["/pet"]["get"]
643 assert get["responses"]["200"]["content"]["application/json"][
644 "schema"
645 ] == build_ref(spec_fixture.spec, "schema", "Pet")
646
647 @pytest.mark.parametrize("spec_fixture", ("3.0.0",), indirect=True)
648 def test_callback_schema_uses_ref_if_available_v3(self, make_pet_callback_spec):
649 spec_fixture = make_pet_callback_spec(
650 {
651 "get": {
652 "responses": {
653 "200": {"content": {"application/json": {"schema": PetSchema}}}
654 }
655 }
656 }
657 )
658 p = get_paths(spec_fixture.spec)["/pet"]
659 c = p["post"]["callbacks"]["petEvent"]["petCallbackUrl"]
660 get = c["get"]
513661 assert get["responses"]["200"]["content"]["application/json"][
514662 "schema"
515663 ] == build_ref(spec_fixture.spec, "schema", "Pet")
557705 "schema"
558706 ] == build_ref(spec, "schema", "Pet")
559707
708 @pytest.mark.parametrize("spec_fixture", ("2.0",), indirect=True)
709 def test_schema_resolver_allof_v2(self, spec_fixture):
710 spec_fixture.spec.components.schema("Pet", schema=PetSchema)
711 spec_fixture.spec.components.schema("Sample", schema=SampleSchema)
712 spec_fixture.spec.path(
713 path="/pet",
714 operations={
715 "get": {
716 "responses": {200: {"schema": {"allOf": [PetSchema, SampleSchema]}}}
717 }
718 },
719 )
720 get = get_paths(spec_fixture.spec)["/pet"]["get"]
721 assert get["responses"]["200"]["schema"] == {
722 "allOf": [
723 build_ref(spec_fixture.spec, "schema", "Pet"),
724 build_ref(spec_fixture.spec, "schema", "Sample"),
725 ]
726 }
727
728 @pytest.mark.parametrize("spec_fixture", ("3.0.0",), indirect=True)
729 @pytest.mark.parametrize("combinator", ["oneOf", "anyOf", "allOf"])
730 def test_schema_resolver_oneof_anyof_allof_v3(self, spec_fixture, combinator):
731 spec_fixture.spec.components.schema("Pet", schema=PetSchema)
732 spec_fixture.spec.path(
733 path="/pet",
734 operations={
735 "get": {
736 "responses": {
737 200: {
738 "content": {
739 "application/json": {
740 "schema": {combinator: [PetSchema, SampleSchema]}
741 }
742 }
743 }
744 }
745 }
746 },
747 )
748 get = get_paths(spec_fixture.spec)["/pet"]["get"]
749 assert get["responses"]["200"]["content"]["application/json"]["schema"] == {
750 combinator: [
751 build_ref(spec_fixture.spec, "schema", "Pet"),
752 build_ref(spec_fixture.spec, "schema", "Sample"),
753 ]
754 }
755
756 @pytest.mark.parametrize("spec_fixture", ("2.0",), indirect=True)
757 def test_schema_resolver_not_v2(self, spec_fixture):
758 spec_fixture.spec.components.schema("Pet", schema=PetSchema)
759 spec_fixture.spec.path(
760 path="/pet",
761 operations={"get": {"responses": {200: {"schema": {"not": PetSchema}}}}},
762 )
763 get = get_paths(spec_fixture.spec)["/pet"]["get"]
764 assert get["responses"]["200"]["schema"] == {
765 "not": build_ref(spec_fixture.spec, "schema", "Pet"),
766 }
767
768 @pytest.mark.parametrize("spec_fixture", ("3.0.0",), indirect=True)
769 def test_schema_resolver_not_v3(self, spec_fixture):
770 spec_fixture.spec.components.schema("Pet", schema=PetSchema)
771 spec_fixture.spec.path(
772 path="/pet",
773 operations={
774 "get": {
775 "responses": {
776 200: {
777 "content": {
778 "application/json": {"schema": {"not": PetSchema}}
779 }
780 }
781 }
782 }
783 },
784 )
785 get = get_paths(spec_fixture.spec)["/pet"]["get"]
786 assert get["responses"]["200"]["content"]["application/json"]["schema"] == {
787 "not": build_ref(spec_fixture.spec, "schema", "Pet"),
788 }
789
560790 @pytest.mark.parametrize(
561 "pet_schema", (PetSchema, PetSchema(), "tests.schemas.PetSchema"),
791 "pet_schema",
792 (PetSchema, PetSchema(), "tests.schemas.PetSchema"),
562793 )
563794 def test_schema_name_resolver_returns_none_v2(self, pet_schema):
564795 def resolver(schema):
578809 assert "properties" in get["responses"]["200"]["schema"]
579810
580811 @pytest.mark.parametrize(
581 "pet_schema", (PetSchema, PetSchema(), "tests.schemas.PetSchema"),
812 "pet_schema",
813 (PetSchema, PetSchema(), "tests.schemas.PetSchema"),
582814 )
583815 def test_schema_name_resolver_returns_none_v3(self, pet_schema):
584816 def resolver(schema):
605837 "properties"
606838 in get["responses"]["200"]["content"]["application/json"]["schema"]
607839 )
840
841 def test_callback_schema_uses_ref_if_available_name_resolver_returns_none_v3(self):
842 def resolver(schema):
843 return None
844
845 spec = APISpec(
846 title="Test auto-reference",
847 version="0.1",
848 openapi_version="3.0.0",
849 plugins=(MarshmallowPlugin(schema_name_resolver=resolver),),
850 )
851 spec.components.schema("Pet", schema=PetSchema)
852 spec.path(
853 path="/pet",
854 operations={
855 "post": {
856 "callbacks": {
857 "petEvent": {
858 "petCallbackUrl": {
859 "get": {
860 "responses": {
861 "200": {
862 "content": {
863 "application/json": {
864 "schema": PetSchema
865 }
866 }
867 }
868 }
869 }
870 }
871 }
872 }
873 }
874 },
875 )
876 p = get_paths(spec)["/pet"]
877 c = p["post"]["callbacks"]["petEvent"]["petCallbackUrl"]
878 get = c["get"]
879 assert get["responses"]["200"]["content"]["application/json"][
880 "schema"
881 ] == build_ref(spec, "schema", "Pet")
608882
609883 @pytest.mark.parametrize("spec_fixture", ("2.0",), indirect=True)
610884 def test_schema_uses_ref_in_parameters_and_request_body_if_available_v2(
645919 p = get_paths(spec_fixture.spec)["/pet"]
646920 assert "schema" in p["get"]["parameters"][0]
647921 post = p["post"]
922 schema_ref = post["requestBody"]["content"]["application/json"]["schema"]
923 assert schema_ref == build_ref(spec_fixture.spec, "schema", "Pet")
924
925 @pytest.mark.parametrize("spec_fixture", ("3.0.0",), indirect=True)
926 def test_callback_schema_uses_ref_in_parameters_and_request_body_if_available_v3(
927 self, make_pet_callback_spec
928 ):
929 spec_fixture = make_pet_callback_spec(
930 {
931 "get": {"parameters": [{"in": "query", "schema": PetSchema}]},
932 "post": {
933 "requestBody": {
934 "content": {"application/json": {"schema": PetSchema}}
935 }
936 },
937 }
938 )
939 p = get_paths(spec_fixture.spec)["/pet"]
940 c = p["post"]["callbacks"]["petEvent"]["petCallbackUrl"]
941 assert "schema" in c["get"]["parameters"][0]
942 post = c["post"]
648943 schema_ref = post["requestBody"]["content"]["application/json"]["schema"]
649944 assert schema_ref == build_ref(spec_fixture.spec, "schema", "Pet")
650945
7201015 ]
7211016 assert response_schema == resolved_schema
7221017
1018 @pytest.mark.parametrize("spec_fixture", ("3.0.0",), indirect=True)
1019 def test_callback_schema_array_uses_ref_if_available_v3(
1020 self, make_pet_callback_spec
1021 ):
1022 spec_fixture = make_pet_callback_spec(
1023 {
1024 "get": {
1025 "parameters": [
1026 {
1027 "name": "Pet",
1028 "in": "query",
1029 "content": {
1030 "application/json": {
1031 "schema": {"type": "array", "items": PetSchema}
1032 }
1033 },
1034 }
1035 ],
1036 "responses": {
1037 "200": {
1038 "content": {
1039 "application/json": {
1040 "schema": {"type": "array", "items": PetSchema}
1041 }
1042 }
1043 }
1044 },
1045 }
1046 }
1047 )
1048 p = get_paths(spec_fixture.spec)["/pet"]
1049 c = p["post"]["callbacks"]["petEvent"]["petCallbackUrl"]
1050 get = c["get"]
1051 assert len(get["parameters"]) == 1
1052 resolved_schema = {
1053 "type": "array",
1054 "items": build_ref(spec_fixture.spec, "schema", "Pet"),
1055 }
1056 request_schema = get["parameters"][0]["content"]["application/json"]["schema"]
1057 assert request_schema == resolved_schema
1058 response_schema = get["responses"]["200"]["content"]["application/json"][
1059 "schema"
1060 ]
1061 assert response_schema == resolved_schema
1062
7231063 @pytest.mark.parametrize("spec_fixture", ("2.0",), indirect=True)
7241064 def test_schema_partially_v2(self, spec_fixture):
7251065 spec_fixture.spec.components.schema("Pet", schema=PetSchema)
7841124 },
7851125 }
7861126
1127 @pytest.mark.parametrize("spec_fixture", ("3.0.0",), indirect=True)
1128 def test_callback_schema_partially_v3(self, make_pet_callback_spec):
1129 spec_fixture = make_pet_callback_spec(
1130 {
1131 "get": {
1132 "responses": {
1133 "200": {
1134 "content": {
1135 "application/json": {
1136 "schema": {
1137 "type": "object",
1138 "properties": {
1139 "mother": PetSchema,
1140 "father": PetSchema,
1141 },
1142 }
1143 }
1144 }
1145 }
1146 }
1147 }
1148 }
1149 )
1150 p = get_paths(spec_fixture.spec)["/pet"]
1151 c = p["post"]["callbacks"]["petEvent"]["petCallbackUrl"]
1152 get = c["get"]
1153 assert get["responses"]["200"]["content"]["application/json"]["schema"] == {
1154 "type": "object",
1155 "properties": {
1156 "mother": build_ref(spec_fixture.spec, "schema", "Pet"),
1157 "father": build_ref(spec_fixture.spec, "schema", "Pet"),
1158 },
1159 }
1160
7871161 def test_parameter_reference(self, spec_fixture):
7881162 if spec_fixture.spec.openapi_version.major < 3:
7891163 param = {"schema": PetSchema}
8181192
8191193 def test_schema_global_state_untouched_2parameters(self, spec_fixture):
8201194 assert get_nested_schema(RunSchema, "sample") is None
821 data = spec_fixture.openapi.schema2parameters(RunSchema)
1195 data = spec_fixture.openapi.schema2parameters(RunSchema, location="json")
8221196 json.dumps(data)
8231197 assert get_nested_schema(RunSchema, "sample") is None
1198
1199 def test_resolve_schema_dict_ref_as_string(self, spec):
1200 """Test schema ref passed as string"""
1201 # The case tested here is a reference passed as string, not a
1202 # marshmallow Schema passed by name as string. We want to ensure the
1203 # MarshmallowPlugin does not interfere with the feature interpreting
1204 # strings as references. Therefore, we use a specific name to ensure
1205 # there is no Schema with that name in the marshmallow registry from
1206 # somewhere else in the tests.
1207 # e.g. PetSchema is in the registry already so it wouldn't work.
1208 schema = {"schema": "SomeSpecificPetSchema"}
1209 if spec.openapi_version.major >= 3:
1210 schema = {"content": {"application/json": schema}}
1211 spec.path("/pet/{petId}", operations={"get": {"responses": {"200": schema}}})
1212 resp = get_paths(spec)["/pet/{petId}"]["get"]["responses"]["200"]
1213 if spec.openapi_version.major < 3:
1214 schema = resp["schema"]
1215 else:
1216 schema = resp["content"]["application/json"]["schema"]
1217 assert schema == build_ref(spec, "schema", "SomeSpecificPetSchema")
8241218
8251219
8261220 class TestCircularReference:
8821276 assert "default" not in props["numbers"]
8831277
8841278
885 @pytest.mark.skipif(
886 MARSHMALLOW_VERSION_INFO[0] < 3, reason="Values ignored in marshmallow 2"
887 )
8881279 class TestDictValues:
8891280 def test_dict_values_resolve_to_additional_properties(self, spec):
8901281 class SchemaWithDict(Schema):
9281319
9291320 result = get_schemas(spec)["SchemaWithList"]["properties"]["list_field"]
9301321 assert result == {"items": build_ref(spec, "schema", "Pet"), "type": "array"}
1322
1323
1324 class TestTimeDelta:
1325 def test_timedelta_x_unit(self, spec):
1326 class SchemaWithTimeDelta(Schema):
1327 sec = TimeDelta("seconds")
1328 day = TimeDelta("days")
1329
1330 spec.components.schema("SchemaWithTimeDelta", schema=SchemaWithTimeDelta)
1331
1332 assert (
1333 get_schemas(spec)["SchemaWithTimeDelta"]["properties"]["sec"]["x-unit"]
1334 == "seconds"
1335 )
1336 assert (
1337 get_schemas(spec)["SchemaWithTimeDelta"]["properties"]["day"]["x-unit"]
1338 == "days"
1339 )
7878 assert list(get_fields(ExcludeSchema, exclude_dump_only=True).keys()) == [
7979 "field5"
8080 ]
81
82 # regression test for https://github.com/marshmallow-code/apispec/issues/673
83 def test_schema_with_field_named_fields(self):
84 class TestSchema(Schema):
85 fields = fields.Int()
86
87 schema_fields = get_fields(TestSchema)
88 assert list(schema_fields.keys()) == ["fields"]
89 assert isinstance(schema_fields["fields"], fields.Int)
33 import pytest
44 from marshmallow import fields, validate
55
6 from apispec.ext.marshmallow.openapi import MARSHMALLOW_VERSION_INFO
7
86 from .schemas import CategorySchema, CustomList, CustomStringField, CustomIntegerField
9 from .utils import build_ref
7 from .utils import build_ref, get_schemas
108
119
1210 def test_field2choices_preserving_order(openapi):
2927 (fields.DateTime, "string"),
3028 (fields.Date, "string"),
3129 (fields.Time, "string"),
30 (fields.TimeDelta, "integer"),
3231 (fields.Email, "string"),
3332 (fields.URL, "string"),
3433 # Custom fields inherit types from their parents
6059 @pytest.mark.parametrize(
6160 ("FieldClass", "expected_format"),
6261 [
63 (fields.Integer, "int32"),
64 (fields.Float, "float"),
6562 (fields.UUID, "uuid"),
6663 (fields.DateTime, "date-time"),
6764 (fields.Date, "date"),
7673
7774
7875 def test_field_with_description(spec_fixture):
79 field = fields.Str(description="a username")
76 field = fields.Str(metadata={"description": "a username"})
8077 res = spec_fixture.openapi.field2property(field)
8178 assert res["description"] == "a username"
8279
8380
84 def test_field_with_missing(spec_fixture):
85 field = fields.Str(default="foo", missing="bar")
81 def test_field_with_load_default(spec_fixture):
82 field = fields.Str(dump_default="foo", load_default="bar")
8683 res = spec_fixture.openapi.field2property(field)
8784 assert res["default"] == "bar"
8885
8986
90 def test_boolean_field_with_false_missing(spec_fixture):
91 field = fields.Boolean(default=None, missing=False)
87 def test_boolean_field_with_false_load_default(spec_fixture):
88 field = fields.Boolean(dump_default=None, load_default=False)
9289 res = spec_fixture.openapi.field2property(field)
9390 assert res["default"] is False
9491
9592
96 def test_datetime_field_with_missing(spec_fixture):
97 if MARSHMALLOW_VERSION_INFO[0] < 3:
98 field = fields.Date(missing=dt.date(2014, 7, 18).isoformat())
99 else:
100 field = fields.Date(missing=dt.date(2014, 7, 18))
93 def test_datetime_field_with_load_default(spec_fixture):
94 field = fields.Date(load_default=dt.date(2014, 7, 18))
10195 res = spec_fixture.openapi.field2property(field)
10296 assert res["default"] == dt.date(2014, 7, 18).isoformat()
10397
10498
105 def test_field_with_missing_callable(spec_fixture):
106 field = fields.Str(missing=lambda: "dummy")
99 def test_field_with_load_default_callable(spec_fixture):
100 field = fields.Str(load_default=lambda: "dummy")
107101 res = spec_fixture.openapi.field2property(field)
108102 assert "default" not in res
109103
110104
111 def test_field_with_doc_default(spec_fixture):
112 field = fields.Str(doc_default="Manual default")
105 def test_field_with_default(spec_fixture):
106 field = fields.Str(metadata={"default": "Manual default"})
113107 res = spec_fixture.openapi.field2property(field)
114108 assert res["default"] == "Manual default"
115109
116110
117 def test_field_with_doc_default_and_missing(spec_fixture):
118 field = fields.Int(doc_default=42, missing=12)
111 def test_field_with_default_and_load_default(spec_fixture):
112 field = fields.Int(load_default=12, metadata={"default": 42})
119113 res = spec_fixture.openapi.field2property(field)
120114 assert res["default"] == 42
121115
134128
135129 def test_only_allows_valid_properties_in_metadata(spec_fixture):
136130 field = fields.Str(
137 missing="foo",
138 description="foo",
139 enum=["red", "blue"],
140 allOf=["bar"],
141 not_valid="lol",
142 )
143 res = spec_fixture.openapi.field2property(field)
144 assert res["default"] == field.missing
131 load_default="foo",
132 metadata={
133 "description": "foo",
134 "not_valid": "lol",
135 "allOf": ["bar"],
136 "enum": ["red", "blue"],
137 },
138 )
139 res = spec_fixture.openapi.field2property(field)
140 assert res["default"] == field.load_default
145141 assert "description" in res
146142 assert "enum" in res
147143 assert "allOf" in res
160156
161157
162158 def test_field_with_additional_metadata(spec_fixture):
163 field = fields.Str(minLength=6, maxLength=100)
159 field = fields.Str(metadata={"minLength": 6, "maxLength": 100})
164160 res = spec_fixture.openapi.field2property(field)
165161 assert res["maxLength"] == 100
166162 assert res["minLength"] == 6
167163
168164
165 @pytest.mark.parametrize("spec_fixture", ("2.0", "3.0.0", "3.1.0"), indirect=True)
169166 def test_field_with_allow_none(spec_fixture):
170167 field = fields.Str(allow_none=True)
171168 res = spec_fixture.openapi.field2property(field)
172169 if spec_fixture.openapi.openapi_version.major < 3:
173170 assert res["x-nullable"] is True
171 elif spec_fixture.openapi.openapi_version.minor < 1:
172 assert res["nullable"] is True
174173 else:
175 assert res["nullable"] is True
174 assert "nullable" not in res
175 assert res["type"] == ["string", "null"]
176
177
178 def test_field_with_dump_only(spec_fixture):
179 field = fields.Str(dump_only=True)
180 res = spec_fixture.openapi.field2property(field)
181 assert res["readOnly"] is True
182
183
184 @pytest.mark.parametrize("spec_fixture", ("2.0", "3.0.0", "3.1.0"), indirect=True)
185 def test_field_with_load_only(spec_fixture):
186 field = fields.Str(load_only=True)
187 res = spec_fixture.openapi.field2property(field)
188 if spec_fixture.openapi.openapi_version.major < 3:
189 assert "writeOnly" not in res
190 else:
191 assert res["writeOnly"] is True
192
193
194 def test_field_with_range_no_type(spec_fixture):
195 field = fields.Field(validate=validate.Range(min=1, max=10))
196 res = spec_fixture.openapi.field2property(field)
197 assert res["x-minimum"] == 1
198 assert res["x-maximum"] == 10
199 assert "type" not in res
200
201
202 @pytest.mark.parametrize("field", (fields.Number, fields.Integer))
203 def test_field_with_range_string_type(spec_fixture, field):
204 field = field(validate=validate.Range(min=1, max=10))
205 res = spec_fixture.openapi.field2property(field)
206 assert res["minimum"] == 1
207 assert res["maximum"] == 10
208 assert isinstance(res["type"], str)
209
210
211 @pytest.mark.parametrize("spec_fixture", ("3.1.0",), indirect=True)
212 def test_field_with_range_type_list_with_number(spec_fixture):
213 @spec_fixture.openapi.map_to_openapi_type(["integer", "null"], None)
214 class NullableInteger(fields.Field):
215 """Nullable integer"""
216
217 field = NullableInteger(validate=validate.Range(min=1, max=10))
218 res = spec_fixture.openapi.field2property(field)
219 assert res["minimum"] == 1
220 assert res["maximum"] == 10
221 assert res["type"] == ["integer", "null"]
222
223
224 @pytest.mark.parametrize("spec_fixture", ("3.1.0",), indirect=True)
225 def test_field_with_range_type_list_without_number(spec_fixture):
226 @spec_fixture.openapi.map_to_openapi_type(["string", "null"], None)
227 class NullableInteger(fields.Field):
228 """Nullable integer"""
229
230 field = NullableInteger(validate=validate.Range(min=1, max=10))
231 res = spec_fixture.openapi.field2property(field)
232 assert res["x-minimum"] == 1
233 assert res["x-maximum"] == 10
234 assert res["type"] == ["string", "null"]
176235
177236
178237 def test_field_with_str_regex(spec_fixture):
207266 spec_fixture.spec.components.schema("Category", schema=CategorySchema)
208267 category = fields.Nested(
209268 CategorySchema,
210 description="A category",
211 invalid_property="not in the result",
212 x_extension="A great extension",
269 metadata={
270 "description": "A category",
271 "invalid_property": "not in the result",
272 "x_extension": "A great extension",
273 },
213274 )
214275 result = spec_fixture.openapi.field2property(category)
215276 assert result == {
273334 }
274335
275336
337 class TestField2PropertyPluck:
338 @pytest.fixture(autouse=True)
339 def _setup(self, spec_fixture):
340 self.field2property = spec_fixture.openapi.field2property
341
342 self.spec = spec_fixture.spec
343 self.spec.components.schema("Category", schema=CategorySchema)
344 self.unplucked = get_schemas(self.spec)["Category"]["properties"]["breed"]
345
346 def test_spec(self, spec_fixture):
347 breed = fields.Pluck(CategorySchema, "breed")
348 assert self.field2property(breed) == self.unplucked
349
350 def test_with_property(self):
351 breed = fields.Pluck(CategorySchema, "breed", dump_only=True)
352 assert self.field2property(breed) == {**self.unplucked, "readOnly": True}
353
354 def test_metadata(self):
355 breed = fields.Pluck(
356 CategorySchema,
357 "breed",
358 metadata={
359 "description": "Category breed",
360 "invalid_property": "not in the result",
361 "x_extension": "A great extension",
362 },
363 )
364 assert self.field2property(breed) == {
365 **self.unplucked,
366 "description": "Category breed",
367 "x-extension": "A great extension",
368 }
369
370 def test_many(self):
371 breed = fields.Pluck(CategorySchema, "breed", many=True)
372 assert self.field2property(breed) == {"type": "array", "items": self.unplucked}
373
374 def test_many_with_property(self):
375 breed = fields.Pluck(CategorySchema, "breed", many=True, dump_only=True)
376 assert self.field2property(breed) == {
377 "items": self.unplucked,
378 "type": "array",
379 "readOnly": True,
380 }
381
382
276383 def test_custom_properties_for_custom_fields(spec_fixture):
277384 def custom_string2properties(self, field, **kwargs):
278385 ret = {}
292399 assert properties["x-customString"] == (
293400 spec_fixture.openapi.openapi_version == "2.0"
294401 )
402
403
404 def test_field2property_with_non_string_metadata_keys(spec_fixture):
405 class _DesertSentinel:
406 pass
407
408 field = fields.Boolean(metadata={"description": "A description"})
409 field.metadata[_DesertSentinel()] = "to be ignored"
410 result = spec_fixture.openapi.field2property(field)
411 assert result == {"description": "A description", "type": "boolean"}
00 import pytest
1
2 from marshmallow import fields, Schema, validate
1 from collections import OrderedDict
2 from datetime import datetime
3
4 from marshmallow import EXCLUDE, fields, INCLUDE, RAISE, Schema, validate
35
46 from apispec.ext.marshmallow import MarshmallowPlugin
5 from apispec.ext.marshmallow.openapi import MARSHMALLOW_VERSION_INFO
67 from apispec import exceptions, utils, APISpec
78
89 from .schemas import CustomList, CustomStringField
1011
1112
1213 class TestMarshmallowFieldToOpenAPI:
13 def test_fields_with_missing_load(self, openapi):
14 field_dict = {"field": fields.Str(default="foo", missing="bar")}
15 res = openapi.fields2parameters(field_dict, default_in="query")
14 def test_fields_with_load_default_load(self, openapi):
15 class MySchema(Schema):
16 field = fields.Str(dump_default="foo", load_default="bar")
17
18 res = openapi.schema2parameters(MySchema, location="query")
1619 if openapi.openapi_version.major < 3:
1720 assert res[0]["default"] == "bar"
1821 else:
1922 assert res[0]["schema"]["default"] == "bar"
20
21 def test_fields_with_location(self, openapi):
22 field_dict = {"field": fields.Str(location="querystring")}
23 res = openapi.fields2parameters(field_dict, default_in="headers")
24 assert res[0]["in"] == "query"
25
26 # json/body is invalid for OpenAPI 3
27 @pytest.mark.parametrize("openapi", ("2.0",), indirect=True)
28 def test_fields_with_multiple_json_locations(self, openapi):
29 field_dict = {
30 "field1": fields.Str(location="json", required=True),
31 "field2": fields.Str(location="json", required=True),
32 "field3": fields.Str(location="json"),
33 }
34 res = openapi.fields2parameters(field_dict, default_in=None)
35 assert len(res) == 1
36 assert res[0]["in"] == "body"
37 assert res[0]["required"] is False
38 assert "field1" in res[0]["schema"]["properties"]
39 assert "field2" in res[0]["schema"]["properties"]
40 assert "field3" in res[0]["schema"]["properties"]
41 assert "required" in res[0]["schema"]
42 assert len(res[0]["schema"]["required"]) == 2
43 assert "field1" in res[0]["schema"]["required"]
44 assert "field2" in res[0]["schema"]["required"]
45
46 def test_fields2parameters_does_not_modify_metadata(self, openapi):
47 field_dict = {"field": fields.Str(location="querystring")}
48 res = openapi.fields2parameters(field_dict, default_in="headers")
49 assert res[0]["in"] == "query"
50
51 res = openapi.fields2parameters(field_dict, default_in="headers")
52 assert res[0]["in"] == "query"
53
54 def test_fields_location_mapping(self, openapi):
55 field_dict = {"field": fields.Str(location="cookies")}
56 res = openapi.fields2parameters(field_dict, default_in="headers")
57 assert res[0]["in"] == "cookie"
58
59 def test_fields_default_location_mapping(self, openapi):
60 field_dict = {"field": fields.Str()}
61 res = openapi.fields2parameters(field_dict, default_in="headers")
62 assert res[0]["in"] == "header"
6323
6424 # json/body is invalid for OpenAPI 3
6525 @pytest.mark.parametrize("openapi", ("2.0",), indirect=True)
6828 id = fields.Int()
6929
7030 schema = ExampleSchema(many=True)
71 res = openapi.schema2parameters(schema=schema, default_in="json")
31 res = openapi.schema2parameters(schema=schema, location="json")
7232 assert res[0]["in"] == "body"
7333
7434 def test_fields_with_dump_only(self, openapi):
7535 class UserSchema(Schema):
7636 name = fields.Str(dump_only=True)
7737
78 res = openapi.fields2parameters(UserSchema._declared_fields, default_in="query")
38 res = openapi.schema2parameters(schema=UserSchema(), location="query")
7939 assert len(res) == 0
80 res = openapi.fields2parameters(UserSchema().fields, default_in="query")
40
41 class UserSchema(Schema):
42 name = fields.Str()
43
44 class Meta:
45 dump_only = ("name",)
46
47 res = openapi.schema2parameters(schema=UserSchema(), location="query")
8148 assert len(res) == 0
8249
83 class UserSchema(Schema):
84 name = fields.Str()
85
86 class Meta:
87 dump_only = ("name",)
88
89 res = openapi.schema2parameters(schema=UserSchema, default_in="query")
90 assert len(res) == 0
91
9250
9351 class TestMarshmallowSchemaToModelDefinition:
94 def test_invalid_schema(self, openapi):
95 with pytest.raises(ValueError):
96 openapi.schema2jsonschema(None)
97
9852 def test_schema2jsonschema_with_explicit_fields(self, openapi):
9953 class UserSchema(Schema):
10054 _id = fields.Int()
101 email = fields.Email(description="email address of the user")
55 email = fields.Email(metadata={"description": "email address of the user"})
10256 name = fields.Str()
10357
10458 class Meta:
11367 assert props["email"]["format"] == "email"
11468 assert props["email"]["description"] == "email address of the user"
11569
116 @pytest.mark.skipif(
117 MARSHMALLOW_VERSION_INFO[0] >= 3, reason="Behaviour changed in marshmallow 3"
118 )
119 def test_schema2jsonschema_override_name_ma2(self, openapi):
120 class ExampleSchema(Schema):
121 _id = fields.Int(load_from="id", dump_to="id")
122 _dt = fields.Int(load_from="lf_no_match", dump_to="dt")
123 _lf = fields.Int(load_from="lf")
124 _global = fields.Int(load_from="global", dump_to="global")
125
126 class Meta:
127 exclude = ("_global",)
128
129 res = openapi.schema2jsonschema(ExampleSchema)
130 assert res["type"] == "object"
131 props = res["properties"]
132 # `_id` renamed to `id`
133 assert "_id" not in props and props["id"]["type"] == "integer"
134 # `load_from` and `dump_to` do not match, `dump_to` is used
135 assert "lf_no_match" not in props
136 assert props["dt"]["type"] == "integer"
137 # `load_from` and no `dump_to`, `load_from` is used
138 assert props["lf"]["type"] == "integer"
139 # `_global` excluded correctly
140 assert "_global" not in props and "global" not in props
141
142 @pytest.mark.skipif(
143 MARSHMALLOW_VERSION_INFO[0] < 3, reason="Behaviour changed in marshmallow 3"
144 )
145 def test_schema2jsonschema_override_name_ma3(self, openapi):
70 def test_schema2jsonschema_override_name(self, openapi):
14671 class ExampleSchema(Schema):
14772 _id = fields.Int(data_key="id")
14873 _global = fields.Int(data_key="global")
177102 res = openapi.schema2jsonschema(BandSchema(partial=("drummer",)))
178103 assert res["required"] == ["bassist"]
179104
105 @pytest.mark.parametrize("ordered_schema", (True, False))
106 def test_ordered(self, openapi, ordered_schema):
107 class BandSchema(Schema):
108 class Meta:
109 ordered = ordered_schema
110
111 drummer = fields.Str()
112 bassist = fields.Str()
113
114 res = openapi.schema2jsonschema(BandSchema)
115 assert isinstance(res["properties"], OrderedDict) == ordered_schema
116
117 res = openapi.schema2jsonschema(BandSchema())
118 assert isinstance(res["properties"], OrderedDict) == ordered_schema
119
180120 def test_no_required_fields(self, openapi):
181121 class BandSchema(Schema):
182122 drummer = fields.Str()
206146
207147 res = openapi.schema2jsonschema(WhiteStripesSchema)
208148 assert set(res["properties"].keys()) == {"guitarist", "drummer"}
149
150 def test_unknown_values_disallow(self, openapi):
151 class UnknownRaiseSchema(Schema):
152 class Meta:
153 unknown = RAISE
154
155 first = fields.Str()
156
157 res = openapi.schema2jsonschema(UnknownRaiseSchema)
158 assert res["additionalProperties"] is False
159
160 def test_unknown_values_allow(self, openapi):
161 class UnknownIncludeSchema(Schema):
162 class Meta:
163 unknown = INCLUDE
164
165 first = fields.Str()
166
167 res = openapi.schema2jsonschema(UnknownIncludeSchema)
168 assert res["additionalProperties"] is True
169
170 def test_unknown_values_ignore(self, openapi):
171 class UnknownExcludeSchema(Schema):
172 class Meta:
173 unknown = EXCLUDE
174
175 first = fields.Str()
176
177 res = openapi.schema2jsonschema(UnknownExcludeSchema)
178 assert "additionalProperties" not in res
209179
210180 def test_only_explicitly_declared_fields_are_translated(self, openapi):
211181 class UserSchema(Schema):
226196 assert "email" not in props
227197
228198 def test_observed_field_name_for_required_field(self, openapi):
229 if MARSHMALLOW_VERSION_INFO[0] < 3:
230 fields_dict = {
231 "user_id": fields.Int(load_from="id", dump_to="id", required=True)
232 }
233 else:
234 fields_dict = {"user_id": fields.Int(data_key="id", required=True)}
235
199 fields_dict = {"user_id": fields.Int(data_key="id", required=True)}
236200 res = openapi.fields2jsonschema(fields_dict)
237201 assert res["required"] == ["id"]
238202
250214 class NotASchema:
251215 pass
252216
253 expected_error = "{!r} doesn't have either `fields` or `_declared_fields`.".format(
254 NotASchema
217 expected_error = (
218 f"{NotASchema!r} is neither a Schema class nor a Schema instance."
255219 )
256220 with pytest.raises(ValueError, match=expected_error):
257221 openapi.schema2jsonschema(NotASchema)
260224 class TestMarshmallowSchemaToParameters:
261225 @pytest.mark.parametrize("ListClass", [fields.List, CustomList])
262226 def test_field_multiple(self, ListClass, openapi):
263 field = ListClass(fields.Str, location="querystring")
264 res = openapi.field2parameter(field, name="field", default_in=None)
227 field = ListClass(fields.Str)
228 res = openapi._field2parameter(field, name="field", location="query")
265229 assert res["in"] == "query"
266230 if openapi.openapi_version.major < 3:
267231 assert res["type"] == "array"
274238 assert res["explode"] is True
275239
276240 def test_field_required(self, openapi):
277 field = fields.Str(required=True, location="query")
278 res = openapi.field2parameter(field, name="field", default_in=None)
241 field = fields.Str(required=True)
242 res = openapi._field2parameter(field, name="field", location="query")
279243 assert res["required"] is True
280244
281 def test_invalid_schema(self, openapi):
282 with pytest.raises(ValueError):
283 openapi.schema2parameters(None)
245 def test_schema_partial(self, openapi):
246 class UserSchema(Schema):
247 field = fields.Str(required=True)
248
249 res_nodump = openapi.schema2parameters(
250 UserSchema(partial=True), location="query"
251 )
252
253 param = res_nodump[0]
254 assert param["required"] is False
255
256 def test_schema_partial_list(self, openapi):
257 class UserSchema(Schema):
258 field = fields.Str(required=True)
259 partial_field = fields.Str(required=True)
260
261 res_nodump = openapi.schema2parameters(
262 UserSchema(partial=("partial_field",)), location="query"
263 )
264
265 param = next(p for p in res_nodump if p["name"] == "field")
266 assert param["required"] is True
267 param = next(p for p in res_nodump if p["name"] == "partial_field")
268 assert param["required"] is False
284269
285270 # json/body is invalid for OpenAPI 3
286271 @pytest.mark.parametrize("openapi", ("2.0",), indirect=True)
289274 name = fields.Str()
290275 email = fields.Email()
291276
292 res = openapi.schema2parameters(UserSchema, default_in="body")
277 res = openapi.schema2parameters(UserSchema, location="body")
293278 assert len(res) == 1
294279 param = res[0]
295280 assert param["in"] == "body"
302287 name = fields.Str()
303288 email = fields.Email(dump_only=True)
304289
305 res_nodump = openapi.schema2parameters(UserSchema, default_in="body")
290 res_nodump = openapi.schema2parameters(UserSchema, location="body")
306291 assert len(res_nodump) == 1
307292 param = res_nodump[0]
308293 assert param["in"] == "body"
315300 name = fields.Str()
316301 email = fields.Email()
317302
318 res = openapi.schema2parameters(UserSchema(many=True), default_in="body")
303 res = openapi.schema2parameters(UserSchema(many=True), location="body")
319304 assert len(res) == 1
320305 param = res[0]
321306 assert param["in"] == "body"
327312 name = fields.Str()
328313 email = fields.Email()
329314
330 res = openapi.schema2parameters(UserSchema, default_in="query")
315 res = openapi.schema2parameters(UserSchema, location="query")
331316 assert len(res) == 2
332317 res.sort(key=lambda param: param["name"])
333318 assert res[0]["name"] == "email"
340325 name = fields.Str()
341326 email = fields.Email()
342327
343 res = openapi.schema2parameters(UserSchema(), default_in="query")
328 res = openapi.schema2parameters(UserSchema(), location="query")
344329 assert len(res) == 2
345330 res.sort(key=lambda param: param["name"])
346331 assert res[0]["name"] == "email"
354339 email = fields.Email()
355340
356341 with pytest.raises(AssertionError):
357 openapi.schema2parameters(UserSchema(many=True), default_in="query")
342 openapi.schema2parameters(UserSchema(many=True), location="query")
358343
359344 def test_fields_query(self, openapi):
360 field_dict = {"name": fields.Str(), "email": fields.Email()}
361 res = openapi.fields2parameters(field_dict, default_in="query")
345 class MySchema(Schema):
346 name = fields.Str()
347 email = fields.Email()
348
349 res = openapi.schema2parameters(MySchema, location="query")
362350 assert len(res) == 2
363351 res.sort(key=lambda param: param["name"])
364352 assert res[0]["name"] == "email"
370358 class NotASchema:
371359 pass
372360
373 expected_error = "{!r} doesn't have either `fields` or `_declared_fields`".format(
374 NotASchema
361 expected_error = (
362 f"{NotASchema!r} is neither a Schema class nor a Schema instance."
375363 )
376364 with pytest.raises(ValueError, match=expected_error):
377365 openapi.schema2jsonschema(NotASchema)
418406 assert ("i" in props) == (modifier == "only")
419407 assert ("j" not in props) == (modifier == "only")
420408
409 def test_schema2jsonschema_with_plucked_field(self, spec_fixture):
410 class PetSchema(Schema):
411 breed = fields.Pluck(CategorySchema, "breed")
412
413 category_schema = spec_fixture.openapi.schema2jsonschema(CategorySchema)
414 pet_schema = spec_fixture.openapi.schema2jsonschema(PetSchema)
415 assert (
416 pet_schema["properties"]["breed"] == category_schema["properties"]["breed"]
417 )
418
421419 def test_schema2jsonschema_with_nested_fields_with_adhoc_changes(
422420 self, spec_fixture
423421 ):
440438 assert props["Category"] == spec_fixture.openapi.schema2jsonschema(
441439 CategorySchema
442440 )
441
442 def test_schema2jsonschema_with_plucked_fields_with_adhoc_changes(
443 self, spec_fixture
444 ):
445 category_schema = CategorySchema()
446 category_schema.fields["breed"].dump_only = True
447
448 class PetSchema(Schema):
449 breed = fields.Pluck(category_schema, "breed", many=True)
450
451 spec_fixture.spec.components.schema("Pet", schema=PetSchema)
452 props = get_schemas(spec_fixture.spec)["Pet"]["properties"]
453
454 assert props["breed"]["items"]["readOnly"] is True
443455
444456 def test_schema2jsonschema_with_nested_excluded_fields(self, spec):
445457 category_schema = CategorySchema(exclude=("breed",))
476488 "required": True,
477489 "type": "string",
478490 },
479 openapi.field2parameter(
491 openapi._field2parameter(
480492 field=fields.List(
481493 fields.Str(),
482494 validate=validate.OneOf(["freddie", "roger"]),
483 location="querystring",
484495 ),
485 default_in=None,
496 location="query",
486497 name="body",
487498 ),
488499 ]
489 + openapi.schema2parameters(PageSchema, default_in="query"),
500 + openapi.schema2parameters(PageSchema, location="query"),
490501 "responses": {200: {"schema": PetSchema, "description": "A pet"}},
491502 },
492503 "post": {
499510 "type": "string",
500511 }
501512 ]
502 + openapi.schema2parameters(CategorySchema, default_in="body")
513 + openapi.schema2parameters(CategorySchema, location="body")
503514 ),
504515 "responses": {201: {"schema": PetSchema, "description": "A pet"}},
505516 },
534545 "required": True,
535546 "schema": {"type": "string"},
536547 },
537 openapi.field2parameter(
548 openapi._field2parameter(
538549 field=fields.List(
539550 fields.Str(),
540551 validate=validate.OneOf(["freddie", "roger"]),
541 location="querystring",
542552 ),
543 default_in=None,
553 location="query",
544554 name="body",
545555 ),
546556 ]
547 + openapi.schema2parameters(PageSchema, default_in="query"),
557 + openapi.schema2parameters(PageSchema, location="query"),
548558 "responses": {
549559 200: {
550560 "description": "success",
585595 class ValidationSchema(Schema):
586596 id = fields.Int(dump_only=True)
587597 range = fields.Int(validate=validate.Range(min=1, max=10))
598 range_no_upper = fields.Float(validate=validate.Range(min=1))
588599 multiple_ranges = fields.Int(
589600 validate=[
590601 validate.Range(min=1),
610621 equal_length = fields.Str(
611622 validate=[validate.Length(equal=5), validate.Length(min=1, max=10)]
612623 )
624 date_range = fields.DateTime(
625 validate=validate.Range(
626 min=datetime(1900, 1, 1),
627 )
628 )
613629
614630 @pytest.mark.parametrize(
615631 ("field", "properties"),
616632 [
617633 ("range", {"minimum": 1, "maximum": 10}),
634 ("range_no_upper", {"minimum": 1}),
618635 ("multiple_ranges", {"minimum": 3, "maximum": 7}),
619636 ("list_length", {"minItems": 1, "maxItems": 10}),
620637 ("custom_list_length", {"minItems": 1, "maxItems": 10}),
622639 ("custom_field_length", {"minLength": 1, "maxLength": 10}),
623640 ("multiple_lengths", {"minLength": 3, "maxLength": 7}),
624641 ("equal_length", {"minLength": 5, "maxLength": 5}),
642 ("date_range", {"x-minimum": datetime(1900, 1, 1)}),
625643 ],
626644 )
627645 def test_properties(self, field, properties, spec):
2525 @pytest.mark.parametrize("docstring", (None, "", "---"))
2626 def test_load_operations_from_docstring_empty_docstring(docstring):
2727 assert yaml_utils.load_operations_from_docstring(docstring) == {}
28
29
30 def test_dict_to_yaml_unicode():
31 assert yaml_utils.dict_to_yaml({"가": "나"}) == '"\\uAC00": "\\uB098"\n'
32 assert yaml_utils.dict_to_yaml({"가": "나"}, {"allow_unicode": True}) == "가: 나\n"
2020 return spec.to_dict()["components"]["parameters"]
2121
2222
23 def get_headers(spec):
24 if spec.openapi_version.major < 3:
25 return spec.to_dict()["headers"]
26 return spec.to_dict()["components"]["headers"]
27
28
2329 def get_examples(spec):
2430 return spec.to_dict()["components"]["examples"]
2531
00 [tox]
11 envlist=
22 lint
3 py{35,36,37,38}-marshmallow2
4 py{35,36,37,38}-marshmallow3
3 py{36,37,38,39}-marshmallow3
54 py38-marshmallowdev
65 docs
76
87 [testenv]
98 extras = tests
109 deps =
11 marshmallow2: marshmallow>=2.0.0,<3.0.0
12 marshmallow3: marshmallow>=3.0.0,<4.0.0
10 marshmallow3: marshmallow>=3.10.0,<4.0.0
1311 marshmallowdev: https://github.com/marshmallow-code/marshmallow/archive/dev.tar.gz
1412 commands = pytest {posargs}
1513
2725 [testenv:watch-docs]
2826 deps = sphinx-autobuild
2927 extras = docs
30 commands = sphinx-autobuild --open-browser docs/ docs/_build {posargs} -z src/apispec -s 2
28 commands = sphinx-autobuild --open-browser docs/ docs/_build {posargs} --watch src/apispec --delay 2
3129
3230 [testenv:watch-readme]
3331 deps = restview