New upstream release.
Kali Janitor
2 years ago
0 | version: 2 | |
1 | updates: | |
2 | - package-ecosystem: pip | |
3 | directory: "/" | |
4 | schedule: | |
5 | interval: daily | |
6 | open-pull-requests-limit: 10 |
0 | 0 | repos: |
1 | 1 | - repo: https://github.com/asottile/pyupgrade |
2 | rev: v2.4.1 | |
2 | rev: v2.11.0 | |
3 | 3 | hooks: |
4 | 4 | - id: pyupgrade |
5 | args: [--py3-plus] | |
5 | args: [--py36-plus] | |
6 | 6 | - repo: https://github.com/python/black |
7 | rev: 19.10b0 | |
7 | rev: 20.8b1 | |
8 | 8 | hooks: |
9 | 9 | - id: black |
10 | 10 | language_version: python3 |
11 | 11 | - repo: https://gitlab.com/pycqa/flake8 |
12 | rev: 3.8.1 | |
12 | rev: 3.9.0 | |
13 | 13 | hooks: |
14 | 14 | - id: flake8 |
15 | additional_dependencies: [flake8-bugbear==20.1.4] | |
15 | additional_dependencies: [flake8-bugbear==21.4.3] | |
16 | 16 | - repo: https://github.com/asottile/blacken-docs |
17 | rev: v1.7.0 | |
17 | rev: v1.10.0 | |
18 | 18 | hooks: |
19 | 19 | - id: blacken-docs |
20 | additional_dependencies: [black==19.10b0] | |
20 | additional_dependencies: [black==20.8b1] |
62 | 62 | - Ashutosh Chaudhary `@codeasashu <https://github.com/codeasashu>`_ |
63 | 63 | - Fedor Fominykh `@fedorfo <https://github.com/fedorfo>`_ |
64 | 64 | - 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>`_ |
0 | 0 | Changelog |
1 | 1 | --------- |
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. | |
2 | 215 | |
3 | 216 | 3.3.1 (2020-06-06) |
4 | 217 | ****************** |
8 | 221 | - Fix ``MarshmallowPlugin`` crash when ``resolve_schema_dict`` is passed a |
9 | 222 | schema as string and ``schema_name_resolver`` returns ``None`` |
10 | 223 | (:issue:`566`). Thanks :user:`black3r` for reporting and thanks |
11 | :user:`Bangterm` for the PR. | |
224 | :user:`Bangertm` for the PR. | |
12 | 225 | |
13 | 226 | 3.3.0 (2020-02-14) |
14 | 227 | ****************** |
39 | 252 | |
40 | 253 | Features: |
41 | 254 | |
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. | |
44 | 257 | |
45 | 258 | Support: |
46 | 259 | |
113 | 326 | |
114 | 327 | - Add support for path level parameters (:issue:`453`). |
115 | 328 | Thanks :user:`karec` for the PR. |
116 | - *Backwards-incompatible*: A `apispec.exceptions.DuplicateParameterError` is | |
329 | - *Backwards-incompatible*: A ``apispec.exceptions.DuplicateParameterError`` is | |
117 | 330 | raised when two parameters with same name and location are passed to a path |
118 | 331 | or an operation (:pr:`455`). |
119 | - *Backwards-incompatible*: A `apispec.exceptions.InvalidParameterError` is | |
332 | - *Backwards-incompatible*: A ``apispec.exceptions.InvalidParameterError`` is | |
120 | 333 | raised when a parameter is missing required ``name`` and ``in`` attributes |
121 | 334 | after helpers have been executed (:pr:`455`). |
122 | 335 | |
123 | 336 | Other changes: |
124 | 337 | |
125 | - *Backwards-incompatible*: All plugin helpers must accept extra `**kwargs` | |
338 | - *Backwards-incompatible*: All plugin helpers must accept extra ``**kwargs`` | |
126 | 339 | (:issue:`453`). |
127 | 340 | - *Backwards-incompatible*: Components must be referenced by ID, not full path |
128 | 341 | (:issue:`463`). |
147 | 360 | |
148 | 361 | Bug fixes: |
149 | 362 | |
150 | - Fix handling of `http.HTTPStatus` objects (:issue:`426`). Thanks | |
363 | - Fix handling of ``http.HTTPStatus`` objects (:issue:`426`). Thanks | |
151 | 364 | :user:`DStape`. |
152 | 365 | - [apispec.ext.marshmallow]: Ensure make_schema_key returns a unique key on |
153 | 366 | unhashable iterables (:pr:`416`, :pr:`439`). Thanks :user:`zedrdave`. |
223 | 436 | |
224 | 437 | Other changes: |
225 | 438 | |
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`). | |
227 | 441 | |
228 | 442 | .. code-block:: python |
229 | 443 |
13 | 13 | :target: https://apispec.readthedocs.io/ |
14 | 14 | :alt: Documentation |
15 | 15 | |
16 | .. image:: https://badgen.net/badge/marshmallow/2,3?list=1 | |
16 | .. image:: https://badgen.net/badge/marshmallow/3?list=1 | |
17 | 17 | :target: https://marshmallow.readthedocs.io/en/latest/upgrading.html |
18 | :alt: marshmallow 2/3 compatible | |
18 | :alt: marshmallow 3 only | |
19 | 19 | |
20 | 20 | .. image:: https://badgen.net/badge/OAS/2,3?list=1&color=cyan |
21 | 21 | :target: https://github.com/OAI/OpenAPI-Specification |
34 | 34 | - Framework-agnostic |
35 | 35 | - Built-in support for `marshmallow <https://marshmallow.readthedocs.io/>`_ |
36 | 36 | - 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] | |
37 | 48 | |
38 | 49 | Example Application |
39 | 50 | =================== |
66 | 77 | name = fields.Str() |
67 | 78 | |
68 | 79 | |
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 | ||
69 | 85 | # Optional Flask support |
70 | 86 | app = Flask(__name__) |
71 | 87 | |
76 | 92 | --- |
77 | 93 | get: |
78 | 94 | description: Get a random pet |
95 | security: | |
96 | - ApiKeyAuth: [] | |
79 | 97 | responses: |
80 | 98 | 200: |
81 | 99 | content: |
104 | 122 | # "/random": { |
105 | 123 | # "get": { |
106 | 124 | # "description": "Get a random pet", |
125 | # "security": [ | |
126 | # { | |
127 | # "ApiKeyAuth": [] | |
128 | # } | |
129 | # ], | |
107 | 130 | # "responses": { |
108 | 131 | # "200": { |
109 | 132 | # "content": { |
157 | 180 | # } |
158 | 181 | # } |
159 | 182 | # } |
183 | # "securitySchemes": { | |
184 | # "ApiKeyAuth": { | |
185 | # "type": "apiKey", | |
186 | # "in": "header", | |
187 | # "name": "X-API-Key" | |
188 | # } | |
189 | # } | |
160 | 190 | # } |
161 | 191 | # } |
162 | 192 | # } |
179 | 209 | # type: array |
180 | 210 | # name: {type: string} |
181 | 211 | # type: object |
212 | # securitySchemes: | |
213 | # ApiKeyAuth: | |
214 | # in: header | |
215 | # name: X-API-KEY | |
216 | # type: apiKey | |
182 | 217 | # info: {title: Swagger Petstore, version: 1.0.0} |
183 | 218 | # openapi: 3.0.2 |
184 | 219 | # paths: |
190 | 225 | # content: |
191 | 226 | # application/json: |
192 | 227 | # schema: {$ref: '#/components/schemas/Pet'} |
228 | # security: | |
229 | # - ApiKeyAuth: [] | |
193 | 230 | # tags: [] |
194 | 231 | |
195 | 232 |
26 | 26 | toxenvs: |
27 | 27 | - lint |
28 | 28 | |
29 | - py35-marshmallow2 | |
30 | - py35-marshmallow3 | |
29 | - py36-marshmallow3 | |
30 | - py37-marshmallow3 | |
31 | - py38-marshmallow3 | |
32 | - py39-marshmallow3 | |
31 | 33 | |
32 | - py36-marshmallow3 | |
33 | ||
34 | - py37-marshmallow3 | |
35 | ||
36 | - py38-marshmallow2 | |
37 | - py38-marshmallow3 | |
38 | ||
39 | - py38-marshmallowdev | |
34 | - py39-marshmallowdev | |
40 | 35 | |
41 | 36 | - docs |
42 | 37 | os: linux |
43 | 38 | - template: job--pypi-release.yml@sloria |
44 | 39 | parameters: |
45 | python: "3.8" | |
40 | python: "3.9" | |
46 | 41 | distributions: "sdist bdist_wheel" |
47 | 42 | dependsOn: |
48 | 43 | - tox_linux |
0 | apispec (5.1.1-0kali1) UNRELEASED; urgency=low | |
1 | ||
2 | * New upstream release. | |
3 | ||
4 | -- Kali Janitor <[email protected]> Mon, 17 Jan 2022 03:42:52 -0000 | |
5 | ||
0 | 6 | apispec (3.3.1-0kali2) kali-dev; urgency=medium |
1 | 7 | |
2 | 8 | [ Sophie Brun ] |
0 | 0 | import datetime as dt |
1 | 1 | import os |
2 | 2 | import sys |
3 | import time | |
3 | 4 | |
4 | 5 | sys.path.insert(0, os.path.abspath(os.path.join("..", "src"))) |
5 | 6 | import apispec # noqa: E402 |
22 | 23 | |
23 | 24 | issues_github_path = "marshmallow-code/apispec" |
24 | 25 | |
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 | ||
25 | 33 | source_suffix = ".rst" |
26 | 34 | master_doc = "index" |
27 | 35 | project = "apispec" |
28 | copyright = "Steven Loria {:%Y}".format(dt.datetime.utcnow()) | |
36 | copyright = f"2014-{build_date:%Y}, Steven Loria and contributors" | |
29 | 37 | |
30 | 38 | version = release = apispec.__version__ |
31 | 39 |
46 | 46 | name = fields.Str() |
47 | 47 | |
48 | 48 | |
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 | ||
49 | 54 | # Optional Flask support |
50 | 55 | app = Flask(__name__) |
51 | 56 | |
56 | 61 | --- |
57 | 62 | get: |
58 | 63 | description: Get a random pet |
64 | security: | |
65 | - ApiKeyAuth: [] | |
59 | 66 | responses: |
60 | 67 | 200: |
61 | 68 | description: Return a pet |
120 | 127 | # "type": "string" |
121 | 128 | # } |
122 | 129 | # } |
123 | # } | |
130 | # }, | |
131 | # } | |
132 | # }, | |
133 | # "securitySchemes": { | |
134 | # "ApiKeyAuth": { | |
135 | # "type": "apiKey", | |
136 | # "in": "header", | |
137 | # "name": "X-API-Key" | |
124 | 138 | # } |
125 | 139 | # }, |
126 | 140 | # "paths": { |
127 | 141 | # "/random": { |
128 | 142 | # "get": { |
129 | 143 | # "description": "Get a random pet", |
144 | # "security": [ | |
145 | # { | |
146 | # "ApiKeyAuth": [] | |
147 | # } | |
148 | # ], | |
130 | 149 | # "responses": { |
131 | 150 | # "200": { |
132 | 151 | # "description": "Return a pet", |
170 | 189 | # name: |
171 | 190 | # type: string |
172 | 191 | # type: object |
192 | # securitySchemes: | |
193 | # ApiKeyAuth: | |
194 | # in: header | |
195 | # name: X-API-KEY | |
196 | # type: apiKey | |
173 | 197 | # paths: |
174 | 198 | # /random: |
175 | 199 | # get: |
181 | 205 | # schema: |
182 | 206 | # $ref: '#/components/schemas/Pet' |
183 | 207 | # description: Return a pet |
208 | # security: | |
209 | # - ApiKeyAuth: [] | |
184 | 210 | |
185 | 211 | User Guide |
186 | 212 | ========== |
0 | 0 | Install |
1 | 1 | ======= |
2 | 2 | |
3 | **apispec** requires Python >= 3.5. | |
3 | **apispec** requires Python >= 3.6. | |
4 | 4 | |
5 | 5 | From the PyPI |
6 | 6 | ------------- |
52 | 52 | Documenting Top-level Components |
53 | 53 | -------------------------------- |
54 | 54 | |
55 | The ``APISpec`` object contains helpers to add top-level components. | |
55 | The ``APISpec`` object contains helpers to add top-level components: | |
56 | 56 | |
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 | |
59 | 59 | |
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. | |
63 | 85 | |
64 | 86 | To add other top-level objects, pass them to the ``APISpec`` as keyword arguments. |
65 | 87 | |
106 | 128 | validate_spec(spec) |
107 | 129 | |
108 | 130 | |
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 | ||
114 | 131 | Documenting Security Schemes |
115 | 132 | ---------------------------- |
116 | 133 | |
133 | 150 | pprint(spec.to_dict()["components"]["securitySchemes"], indent=2) |
134 | 151 | # { 'api_key': {'in': 'header', 'name': 'X-API-Key', 'type': 'apiKey'}, |
135 | 152 | # '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. |
41 | 41 | First, ensure that ``apispec-webframeworks`` is installed: :: |
42 | 42 | |
43 | 43 | $ pip install apispec-webframeworks |
44 | ||
45 | Also, ensure that a compatible ``marshmallow`` version is used: :: | |
46 | ||
47 | $ pip install -U apispec[marshmallow] | |
44 | 48 | |
45 | 49 | We can now use the marshmallow and Flask plugins. |
46 | 50 | |
279 | 283 | .. code-block:: python |
280 | 284 | |
281 | 285 | 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""" | |
284 | 287 | ret = {} |
285 | 288 | if isinstance(field, MyCustomField): |
286 | 289 | if self.openapi_version.major > 2: |
24 | 24 | .. code-block:: python |
25 | 25 | |
26 | 26 | from apispec import Path, BasePlugin |
27 | from apispec.utils import load_operations_from_docstring | |
27 | from apispec.yaml_utils import load_operations_from_docstring | |
28 | 28 | |
29 | 29 | |
30 | 30 | class MyPlugin(BasePlugin): |
31 | def path_helper(self, path, func, **kwargs): | |
31 | def path_helper(self, path, operations, func, **kwargs): | |
32 | 32 | """Path helper that parses docstrings for operations. Adds a |
33 | 33 | ``func`` parameter to `apispec.APISpec.path`. |
34 | 34 | """ |
35 | operations = load_operations_from_docstring(func.__doc__) | |
36 | return Path(path=path, operations=operations) | |
35 | operations.update(load_operations_from_docstring(func.__doc__)) | |
37 | 36 | |
38 | 37 | |
39 | 38 | All plugin helpers must accept extra `**kwargs`, allowing custom plugins to define new arguments if required. |
50 | 49 | |
51 | 50 | class DeprecatedPlugin(BasePlugin): |
52 | 51 | 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`""" | |
55 | 53 | if kwargs.pop("deprecated", False) is True: |
56 | 54 | for key, value in operations.items(): |
57 | 55 | value["deprecated"] = True |
0 | 0 | [metadata] |
1 | 1 | 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 | |
8 | 2 | |
9 | 3 | [flake8] |
10 | 4 | ignore = E203, E266, E501, W503 |
1 | 1 | from setuptools import setup, find_packages |
2 | 2 | |
3 | 3 | EXTRAS_REQUIRE = { |
4 | "marshmallow": ["marshmallow>=3.13.0"], | |
4 | 5 | "yaml": ["PyYAML>=3.10"], |
5 | 6 | "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"], | |
7 | 8 | "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", | |
11 | 12 | "sphinx-issues==1.2.0", |
12 | "sphinx-rtd-theme==0.4.3", | |
13 | "sphinx-rtd-theme==1.0.0", | |
13 | 14 | ], |
14 | 15 | } |
15 | 16 | EXTRAS_REQUIRE["tests"] = ( |
16 | 17 | EXTRAS_REQUIRE["yaml"] |
17 | 18 | + EXTRAS_REQUIRE["validation"] |
18 | + ["marshmallow>=2.19.2", "pytest", "mock"] | |
19 | + ["marshmallow>=3.13.0", "pytest", "mock"] | |
19 | 20 | ) |
20 | 21 | EXTRAS_REQUIRE["dev"] = EXTRAS_REQUIRE["tests"] + EXTRAS_REQUIRE["lint"] + ["tox"] |
21 | 22 | |
59 | 60 | license="MIT", |
60 | 61 | zip_safe=False, |
61 | 62 | keywords="apispec swagger openapi specification oas documentation spec rest api", |
62 | python_requires=">=3.5", | |
63 | python_requires=">=3.6", | |
63 | 64 | classifiers=[ |
64 | 65 | "License :: OSI Approved :: MIT License", |
65 | 66 | "Programming Language :: Python :: 3", |
66 | "Programming Language :: Python :: 3.5", | |
67 | 67 | "Programming Language :: Python :: 3.6", |
68 | 68 | "Programming Language :: Python :: 3.7", |
69 | 69 | "Programming Language :: Python :: 3.8", |
70 | "Programming Language :: Python :: 3.9", | |
70 | 71 | "Programming Language :: Python :: 3 :: Only", |
71 | 72 | ], |
72 | 73 | test_suite="tests", |
73 | 74 | project_urls={ |
74 | 75 | "Funding": "https://opencollective.com/marshmallow", |
75 | 76 | "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 | |
77 | 78 | }, |
78 | 79 | ) |
2 | 2 | from .core import APISpec |
3 | 3 | from .plugin import BasePlugin |
4 | 4 | |
5 | __version__ = "3.3.1" | |
5 | __version__ = "5.1.1" | |
6 | 6 | __all__ = ["APISpec", "BasePlugin"] |
28 | 28 | def __init__(self, plugins, openapi_version): |
29 | 29 | self._plugins = plugins |
30 | 30 | 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 | } | |
36 | 58 | |
37 | 59 | 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 | } | |
45 | 60 | return { |
46 | 61 | COMPONENT_SUBSECTIONS[self.openapi_version.major][k]: v |
47 | for k, v in subsections.items() | |
62 | for k, v in self._subsections.items() | |
48 | 63 | if v != {} |
49 | 64 | } |
50 | 65 | |
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): | |
52 | 99 | """Add a new schema to the spec. |
53 | 100 | |
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 | |
56 | 105 | |
57 | 106 | .. note:: |
58 | 107 | |
63 | 112 | |
64 | 113 | status = fields.String( |
65 | 114 | 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 | }, | |
68 | 119 | ) |
69 | 120 | |
70 | 121 | https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#schemaObject |
71 | 122 | """ |
72 | if name in self._schemas: | |
123 | if component_id in self.schemas: | |
73 | 124 | 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 {} | |
78 | 128 | # Execute all helpers from plugins |
79 | 129 | for plugin in self._plugins: |
80 | 130 | try: |
81 | ret.update(plugin.schema_helper(name, component, **kwargs) or {}) | |
131 | ret.update(plugin.schema_helper(component_id, ret, **kwargs) or {}) | |
82 | 132 | except PluginMethodNotImplementedError: |
83 | 133 | 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): | |
88 | 139 | """Add a response which can be referenced. |
89 | 140 | |
90 | 141 | :param str component_id: ref_id to use as reference |
91 | 142 | :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: | |
95 | 147 | 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 {} | |
102 | 151 | # Execute all helpers from plugins |
103 | 152 | for plugin in self._plugins: |
104 | 153 | try: |
105 | ret.update(plugin.response_helper(component, **kwargs) or {}) | |
154 | ret.update(plugin.response_helper(ret, **kwargs) or {}) | |
106 | 155 | except PluginMethodNotImplementedError: |
107 | 156 | 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: | |
120 | 173 | 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 {} | |
127 | 177 | ret.setdefault("name", component_id) |
128 | 178 | ret["in"] = location |
129 | 179 | |
134 | 184 | # Execute all helpers from plugins |
135 | 185 | for plugin in self._plugins: |
136 | 186 | try: |
137 | ret.update(plugin.parameter_helper(component, **kwargs) or {}) | |
187 | ret.update(plugin.parameter_helper(ret, **kwargs) or {}) | |
138 | 188 | except PluginMethodNotImplementedError: |
139 | 189 | 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): | |
144 | 220 | """Add an example which can be referenced |
145 | 221 | |
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 | |
148 | 225 | |
149 | 226 | https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.1.md#exampleObject |
150 | 227 | """ |
151 | if name in self._examples: | |
228 | if component_id in self.examples: | |
152 | 229 | 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) | |
156 | 233 | return self |
157 | 234 | |
158 | 235 | def security_scheme(self, component_id, component): |
159 | 236 | """Add a security scheme which can be referenced. |
160 | 237 | |
161 | 238 | :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: | |
165 | 242 | 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] | |
168 | 267 | ) |
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]) | |
172 | 341 | |
173 | 342 | |
174 | 343 | class APISpec: |
180 | 349 | See https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#infoObject |
181 | 350 | :param str|OpenAPIVersion openapi_version: OpenAPI Specification version. |
182 | 351 | 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 | |
184 | 353 | See https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#openapi-object |
185 | 354 | """ |
186 | 355 | |
220 | 389 | ret = deepupdate(ret, self.options) |
221 | 390 | return ret |
222 | 391 | |
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 | """ | |
225 | 397 | from .yaml_utils import dict_to_yaml |
226 | 398 | |
227 | return dict_to_yaml(self.to_dict()) | |
399 | return dict_to_yaml(self.to_dict(), yaml_dump_kwargs) | |
228 | 400 | |
229 | 401 | def tag(self, tag): |
230 | """ Store information about a tag. | |
402 | """Store information about a tag. | |
231 | 403 | |
232 | 404 | :param dict tag: the dictionary storing information about the tag. |
233 | 405 | """ |
242 | 414 | summary=None, |
243 | 415 | description=None, |
244 | 416 | parameters=None, |
245 | **kwargs | |
417 | **kwargs, | |
246 | 418 | ): |
247 | 419 | """Add a new path object to the spec. |
248 | 420 | |
253 | 425 | :param str summary: short summary relevant to all operations in this path |
254 | 426 | :param str description: long description relevant to all operations in this path |
255 | 427 | :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` | |
257 | 429 | """ |
258 | 430 | # 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 | |
260 | 432 | operations = deepcopy(operations) or OrderedDict() |
261 | 433 | parameters = deepcopy(parameters) or [] |
262 | 434 | |
280 | 452 | except PluginMethodNotImplementedError: |
281 | 453 | continue |
282 | 454 | |
283 | self.clean_operations(operations) | |
455 | self._clean_operations(operations) | |
284 | 456 | |
285 | 457 | self._paths.setdefault(path, operations).update(operations) |
286 | 458 | if summary is not None: |
288 | 460 | if description is not None: |
289 | 461 | self._paths[path]["description"] = description |
290 | 462 | if parameters: |
291 | parameters = self.clean_parameters(parameters) | |
463 | parameters = self._clean_parameters(parameters) | |
292 | 464 | 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): | |
310 | 471 | """Ensure that all parameters with "in" equal to "path" are also required |
311 | 472 | as required by the OpenAPI specification, as well as normalizing any |
312 | 473 | references to global parameters and checking for duplicates parameters |
322 | 483 | missing_attrs = [attr for attr in ("name", "in") if attr not in parameter] |
323 | 484 | if missing_attrs: |
324 | 485 | raise InvalidParameterError( |
325 | "Missing keys {} for parameter".format(missing_attrs) | |
486 | f"Missing keys {missing_attrs} for parameter" | |
326 | 487 | ) |
327 | 488 | |
328 | 489 | # OpenAPI Spec 3 and 2 don't allow for duplicated parameters |
340 | 501 | if parameter["in"] == "path": |
341 | 502 | parameter["required"] = True |
342 | 503 | |
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): | |
346 | 507 | """Ensure that all parameters with "in" equal to "path" are also required |
347 | 508 | as required by the OpenAPI specification, as well as normalizing any |
348 | 509 | references to global parameters. Also checks for invalid HTTP methods. |
363 | 524 | |
364 | 525 | for operation in (operations or {}).values(): |
365 | 526 | if "parameters" in operation: |
366 | operation["parameters"] = self.clean_parameters(operation["parameters"]) | |
527 | operation["parameters"] = self._clean_parameters( | |
528 | operation["parameters"] | |
529 | ) | |
367 | 530 | if "responses" in operation: |
368 | 531 | responses = OrderedDict() |
369 | 532 | for code, response in operation["responses"].items(): |
372 | 535 | except (TypeError, ValueError): |
373 | 536 | if self.openapi_version.major < 3 and code != "default": |
374 | 537 | 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 | |
377 | 539 | operation["responses"] = responses |
4 | 4 | (for response and headers schemas) and |
5 | 5 | `spec.path <apispec.APISpec.path>` (for responses and response headers). |
6 | 6 | |
7 | Requires marshmallow>=2.15.2. | |
7 | Requires marshmallow>=3.13.0. | |
8 | 8 | |
9 | 9 | ``MarshmallowPlugin`` maps marshmallow ``Field`` classes with OpenAPI types and |
10 | 10 | formats. |
20 | 20 | |
21 | 21 | .. warning:: |
22 | 22 | |
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. | |
27 | 27 | This may lead to inaccurate documentation in very specific cases. |
28 | 28 | 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. | |
30 | 30 | |
31 | 31 | :: |
32 | 32 | |
47 | 47 | |
48 | 48 | class UserSchema(Schema): |
49 | 49 | 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"}) | |
51 | 51 | 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"} | |
53 | 55 | ) |
54 | 56 | |
55 | 57 | |
59 | 61 | # 'format': 'date-time', |
60 | 62 | # 'readOnly': True, |
61 | 63 | # 'type': 'string'}, |
62 | # 'id': {'format': 'int32', | |
63 | # 'readOnly': True, | |
64 | # 'id': {'readOnly': True, | |
64 | 65 | # 'type': 'integer'}, |
65 | 66 | # 'name': {'description': "The user's name", |
66 | 67 | # 'type': 'string'}}, |
139 | 140 | class MyCustomField(Integer): |
140 | 141 | # ... |
141 | 142 | |
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) | |
143 | 144 | class MyCustomFieldThatsKindaLikeAnInteger(Integer): |
144 | 145 | # ... |
145 | 146 | """ |
172 | 173 | :param dict parameter: parameter fields. May contain a marshmallow |
173 | 174 | Schema class or instance. |
174 | 175 | """ |
175 | # In OpenAPIv3, this only works when using the complex form using "content" | |
176 | 176 | self.resolver.resolve_schema(parameter) |
177 | 177 | return parameter |
178 | 178 | |
186 | 186 | self.resolver.resolve_response(response) |
187 | 187 | return response |
188 | 188 | |
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 | ||
189 | 199 | 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) | |
202 | 201 | |
203 | 202 | def warn_if_schema_already_in_spec(self, schema_key): |
204 | 203 | """Method to warn the user if the schema has already been added to the |
19 | 19 | return schema() |
20 | 20 | if isinstance(schema, marshmallow.Schema): |
21 | 21 | 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)() | |
29 | 23 | |
30 | 24 | |
31 | 25 | def resolve_schema_cls(schema): |
38 | 32 | return schema |
39 | 33 | if isinstance(schema, marshmallow.Schema): |
40 | 34 | 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) | |
48 | 36 | |
49 | 37 | |
50 | 38 | def get_fields(schema, *, exclude_dump_only=False): |
54 | 42 | :param bool exclude_dump_only: whether to filter fields in Meta.dump_only |
55 | 43 | :rtype: dict, of field name field object pairs |
56 | 44 | """ |
57 | if hasattr(schema, "fields"): | |
45 | if isinstance(schema, marshmallow.Schema): | |
58 | 46 | fields = schema.fields |
59 | elif hasattr(schema, "_declared_fields"): | |
47 | elif isinstance(schema, type) and issubclass(schema, marshmallow.Schema): | |
60 | 48 | fields = copy.deepcopy(schema._declared_fields) |
61 | 49 | 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.") | |
65 | 51 | Meta = getattr(schema, "Meta", None) |
66 | 52 | warn_if_fields_defined_in_meta(fields, Meta) |
67 | 53 | return filter_excluded_fields(fields, Meta, exclude_dump_only=exclude_dump_only) |
90 | 76 | |
91 | 77 | :param dict fields: A dictionary of fields name field object pairs |
92 | 78 | :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 | |
94 | 80 | """ |
95 | 81 | exclude = list(getattr(Meta, "exclude", [])) |
96 | 82 | if exclude_dump_only: |
97 | 83 | exclude.extend(getattr(Meta, "dump_only", [])) |
98 | 84 | |
99 | 85 | 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) | |
101 | 89 | ) |
102 | 90 | |
103 | 91 | return filtered_fields |
132 | 120 | :param int counter: the counter of the number of recursions |
133 | 121 | :return: the unique name |
134 | 122 | """ |
135 | if name not in components._schemas: | |
123 | if name not in components.schemas: | |
136 | 124 | return name |
137 | 125 | if not counter: # first time through recursion |
138 | 126 | warnings.warn( |
16 | 16 | |
17 | 17 | RegexType = type(re.compile("")) |
18 | 18 | |
19 | MARSHMALLOW_VERSION_INFO = tuple( | |
20 | [int(part) for part in marshmallow.__version__.split(".") if part.isdigit()] | |
21 | ) | |
22 | ||
23 | ||
24 | 19 | # marshmallow field => (JSON Schema type, format) |
25 | 20 | DEFAULT_FIELD_MAPPING = { |
26 | marshmallow.fields.Integer: ("integer", "int32"), | |
21 | marshmallow.fields.Integer: ("integer", None), | |
27 | 22 | marshmallow.fields.Number: ("number", None), |
28 | marshmallow.fields.Float: ("number", "float"), | |
23 | marshmallow.fields.Float: ("number", None), | |
29 | 24 | marshmallow.fields.Decimal: ("number", None), |
30 | 25 | marshmallow.fields.String: ("string", None), |
31 | 26 | marshmallow.fields.Boolean: ("boolean", None), |
33 | 28 | marshmallow.fields.DateTime: ("string", "date-time"), |
34 | 29 | marshmallow.fields.Date: ("string", "date"), |
35 | 30 | marshmallow.fields.Time: ("string", None), |
31 | marshmallow.fields.TimeDelta: ("integer", None), | |
36 | 32 | marshmallow.fields.Email: ("string", "email"), |
37 | 33 | marshmallow.fields.URL: ("string", "url"), |
38 | 34 | marshmallow.fields.Dict: ("object", None), |
68 | 64 | "type", |
69 | 65 | "items", |
70 | 66 | "allOf", |
67 | "oneOf", | |
68 | "anyOf", | |
69 | "not", | |
71 | 70 | "properties", |
72 | 71 | "additionalProperties", |
73 | 72 | "readOnly", |
73 | "writeOnly", | |
74 | 74 | "xml", |
75 | 75 | "externalDocs", |
76 | 76 | "example", |
77 | "nullable", | |
78 | "deprecated", | |
77 | 79 | } |
78 | 80 | |
79 | 81 | |
87 | 89 | |
88 | 90 | def init_attribute_functions(self): |
89 | 91 | self.attribute_functions = [ |
92 | # self.field2type_and_format should run first | |
93 | # as other functions may rely on its output | |
90 | 94 | self.field2type_and_format, |
91 | 95 | self.field2default, |
92 | 96 | self.field2choices, |
98 | 102 | self.field2pattern, |
99 | 103 | self.metadata2properties, |
100 | 104 | self.nested2properties, |
105 | self.pluck2properties, | |
101 | 106 | self.list2properties, |
102 | 107 | self.dict2properties, |
108 | self.timedelta2properties, | |
103 | 109 | ] |
104 | 110 | |
105 | 111 | def map_to_openapi_type(self, *args): |
198 | 204 | def field2default(self, field, **kwargs): |
199 | 205 | """Return the dictionary containing the field's default value. |
200 | 206 | |
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 | |
202 | 208 | fall back on the field's `missing` parameter. A callable passed to the |
203 | 209 | field's missing parameter will be ignored. |
204 | 210 | |
206 | 212 | :rtype: dict |
207 | 213 | """ |
208 | 214 | 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"] | |
211 | 217 | else: |
212 | default = field.missing | |
218 | default = field.load_default | |
213 | 219 | 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) | |
216 | 221 | ret["default"] = default |
217 | 222 | return ret |
218 | 223 | |
264 | 269 | attributes["writeOnly"] = True |
265 | 270 | return attributes |
266 | 271 | |
267 | def field2nullable(self, field, **kwargs): | |
272 | def field2nullable(self, field, ret): | |
268 | 273 | """Return the dictionary of OpenAPI field attributes for a nullable field. |
269 | 274 | |
270 | 275 | :param Field field: A marshmallow field. |
272 | 277 | """ |
273 | 278 | attributes = {} |
274 | 279 | 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"] | |
278 | 286 | return attributes |
279 | 287 | |
280 | def field2range(self, field, **kwargs): | |
288 | def field2range(self, field, ret): | |
281 | 289 | """Return the dictionary of OpenAPI field attributes for a set of |
282 | 290 | :class:`Range <marshmallow.validators.Range>` validators. |
283 | 291 | |
294 | 302 | ) |
295 | 303 | ] |
296 | 304 | |
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) | |
310 | 311 | |
311 | 312 | def field2length(self, field, **kwargs): |
312 | 313 | """Return the dictionary of OpenAPI field attributes for a set of |
315 | 316 | :param Field field: A marshmallow field. |
316 | 317 | :rtype: dict |
317 | 318 | """ |
318 | attributes = {} | |
319 | ||
320 | 319 | validators = [ |
321 | 320 | validator |
322 | 321 | for validator in field.validators |
333 | 332 | min_attr = "minItems" if is_array else "minLength" |
334 | 333 | max_attr = "maxItems" if is_array else "maxLength" |
335 | 334 | |
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) | |
353 | 342 | |
354 | 343 | 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. | |
357 | 349 | |
358 | 350 | :param Field field: A marshmallow field. |
359 | 351 | :rtype: dict |
396 | 388 | metadata = { |
397 | 389 | key.replace("_", "-") if key.startswith("x_") else key: value |
398 | 390 | for key, value in field.metadata.items() |
391 | if isinstance(key, str) | |
399 | 392 | } |
400 | 393 | |
401 | 394 | # Avoid validation error with "Additional properties not allowed" |
417 | 410 | :param Field field: A marshmallow field. |
418 | 411 | :rtype: dict |
419 | 412 | """ |
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 | ): | |
421 | 418 | schema_dict = self.resolve_nested_schema(field.schema) |
422 | 419 | if ret and "$ref" in schema_dict: |
423 | 420 | ret.update({"allOf": [schema_dict]}) |
425 | 422 | ret.update(schema_dict) |
426 | 423 | return ret |
427 | 424 | |
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 | ||
428 | 440 | def list2properties(self, field, **kwargs): |
429 | 441 | """Return a dictionary of properties from :class:`List <marshmallow.fields.List>` fields. |
430 | 442 | |
435 | 447 | """ |
436 | 448 | ret = {} |
437 | 449 | 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) | |
442 | 451 | return ret |
443 | 452 | |
444 | 453 | def dict2properties(self, field, **kwargs): |
452 | 461 | """ |
453 | 462 | ret = {} |
454 | 463 | 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 |
10 | 10 | import marshmallow |
11 | 11 | from marshmallow.utils import is_collection |
12 | 12 | |
13 | from apispec.utils import OpenAPIVersion, build_reference | |
13 | from apispec.utils import OpenAPIVersion | |
14 | 14 | from apispec.exceptions import APISpecError |
15 | 15 | from .field_converter import FieldConverterMixin |
16 | 16 | from .common import ( |
18 | 18 | make_schema_key, |
19 | 19 | resolve_schema_instance, |
20 | 20 | get_unique_schema_name, |
21 | ) | |
22 | ||
23 | ||
24 | MARSHMALLOW_VERSION_INFO = tuple( | |
25 | [int(part) for part in marshmallow.__version__.split(".") if part.isdigit()] | |
26 | 21 | ) |
27 | 22 | |
28 | 23 | |
59 | 54 | # Schema references |
60 | 55 | self.refs = {} |
61 | 56 | |
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 | ||
77 | 57 | def resolve_nested_schema(self, schema): |
78 | 58 | """Return the OpenAPI representation of a marshmallow Schema. |
79 | 59 | |
86 | 66 | |
87 | 67 | :param schema: schema to add to the spec |
88 | 68 | """ |
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 | |
90 | 75 | schema_key = make_schema_key(schema_instance) |
91 | 76 | if schema_key not in self.refs: |
92 | 77 | name = self.schema_name_resolver(schema) |
109 | 94 | return self.get_ref_dict(schema_instance) |
110 | 95 | |
111 | 96 | 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 | |
119 | 98 | ): |
120 | 99 | """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 | |
122 | 101 | of a single parameter; else return an array of a parameter for each included field in |
123 | 102 | the :class:`Schema <marshmallow.Schema>`. |
124 | 103 | |
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 | ||
125 | 107 | https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#parameterObject |
126 | 108 | """ |
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": | |
131 | 112 | param = { |
132 | "in": openapi_default_in, | |
113 | "in": location, | |
133 | 114 | "required": required, |
134 | 115 | "name": name, |
135 | "schema": prop, | |
116 | "schema": self.resolve_nested_schema(schema), | |
136 | 117 | } |
137 | ||
138 | 118 | if description: |
139 | 119 | param["description"] = description |
140 | ||
141 | 120 | return [param] |
142 | 121 | |
143 | 122 | assert not getattr( |
146 | 125 | |
147 | 126 | fields = get_fields(schema, exclude_dump_only=True) |
148 | 127 | |
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( | |
171 | 130 | 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, | |
174 | 133 | ) |
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): | |
193 | 138 | """Return an OpenAPI parameter as a `dict`, given a marshmallow |
194 | 139 | :class:`Field <marshmallow.Field>`. |
195 | 140 | |
196 | 141 | https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#parameterObject |
197 | 142 | """ |
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 | ||
199 | 150 | 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) | |
235 | 157 | 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 | |
248 | 164 | return ret |
249 | 165 | |
250 | 166 | def schema2jsonschema(self, schema): |
260 | 176 | fields = get_fields(schema) |
261 | 177 | Meta = getattr(schema, "Meta", None) |
262 | 178 | partial = getattr(schema, "partial", None) |
263 | ordered = getattr(schema, "ordered", False) | |
179 | ordered = getattr(Meta, "ordered", False) | |
264 | 180 | |
265 | 181 | jsonschema = self.fields2jsonschema(fields, partial=partial, ordered=ordered) |
266 | 182 | |
268 | 184 | jsonschema["title"] = Meta.title |
269 | 185 | if hasattr(Meta, "description"): |
270 | 186 | jsonschema["description"] = Meta.description |
187 | if hasattr(Meta, "unknown") and Meta.unknown != marshmallow.EXCLUDE: | |
188 | jsonschema["additionalProperties"] = Meta.unknown == marshmallow.INCLUDE | |
271 | 189 | |
272 | 190 | return jsonschema |
273 | 191 | |
285 | 203 | jsonschema = {"type": "object", "properties": OrderedDict() if ordered else {}} |
286 | 204 | |
287 | 205 | 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 | |
291 | 209 | |
292 | 210 | if field_obj.required: |
293 | 211 | if not partial or ( |
305 | 223 | schema in the spec |
306 | 224 | """ |
307 | 225 | 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]) | |
311 | 227 | if getattr(schema, "many", False): |
312 | 228 | return {"type": "array", "items": ref_schema} |
313 | 229 | return ref_schema |
14 | 14 | self.openapi_version = openapi_version |
15 | 15 | self.converter = converter |
16 | 16 | |
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 | ||
17 | 86 | def resolve_parameters(self, parameters): |
18 | 87 | """Resolve marshmallow Schemas in a list of OpenAPI `Parameter Objects |
19 | 88 | <https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#parameter-object>`_. |
37 | 106 | |
38 | 107 | #Output |
39 | 108 | [ |
40 | {"in": "query", "name": "id", "required": False, "schema": {"type": "integer", "format": "int32"}}, | |
109 | {"in": "query", "name": "id", "required": False, "schema": {"type": "integer"}}, | |
41 | 110 | {"in": "query", "name": "name", "required": False, "schema": {"type": "string"}} |
42 | 111 | ] |
43 | 112 | |
75 | 144 | ): |
76 | 145 | schema_instance = resolve_schema_instance(parameter.pop("schema")) |
77 | 146 | resolved += self.converter.schema2parameters( |
78 | schema_instance, default_in=parameter.pop("in"), **parameter | |
147 | schema_instance, location=parameter.pop("in"), **parameter | |
79 | 148 | ) |
80 | 149 | else: |
81 | 150 | self.resolve_schema(parameter) |
149 | 218 | if not isinstance(data, dict): |
150 | 219 | return |
151 | 220 | |
152 | # OAS 2 component or OAS 3 header | |
221 | # OAS 2 component or OAS 3 parameter or header | |
153 | 222 | if "schema" in data: |
154 | 223 | data["schema"] = self.resolve_schema_dict(data["schema"]) |
155 | 224 | # OAS 3 component except header |
161 | 230 | |
162 | 231 | def resolve_schema_dict(self, schema): |
163 | 232 | """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. | |
166 | 235 | |
167 | 236 | If the input is a marshmallow Schema class, object or a string that resolves |
168 | 237 | to a Schema class the Schema will be translated to an OpenAPI Schema Object |
212 | 281 | k: self.resolve_schema_dict(v) |
213 | 282 | for k, v in schema["properties"].items() |
214 | 283 | } |
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"]) | |
215 | 291 | return schema |
216 | 292 | |
217 | 293 | return self.converter.resolve_nested_schema(schema) |
17 | 17 | |
18 | 18 | :param str name: Identifier by which schema may be referenced |
19 | 19 | :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()` | |
21 | 21 | """ |
22 | 22 | raise PluginMethodNotImplementedError |
23 | 23 | |
25 | 25 | """May return response component description as a dict. |
26 | 26 | |
27 | 27 | :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()` | |
29 | 29 | """ |
30 | 30 | raise PluginMethodNotImplementedError |
31 | 31 | |
33 | 33 | """May return parameter component description as a dict. |
34 | 34 | |
35 | 35 | :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()` | |
37 | 45 | """ |
38 | 46 | raise PluginMethodNotImplementedError |
39 | 47 | |
46 | 54 | :param list parameters: A `list` of parameters objects or references for the path. See |
47 | 55 | https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#parameterObject |
48 | 56 | 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()` | |
50 | 58 | |
51 | 59 | Return value should be a string or None. If a string is returned, it |
52 | 60 | is set as the path. |
63 | 71 | :param str path: Path to the resource |
64 | 72 | :param dict operations: A `dict` mapping HTTP methods to operation object. |
65 | 73 | 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()` | |
67 | 75 | """ |
68 | 76 | raise PluginMethodNotImplementedError |
19 | 19 | "schema": "schemas", |
20 | 20 | "response": "responses", |
21 | 21 | "parameter": "parameters", |
22 | "header": "headers", | |
22 | 23 | "example": "examples", |
23 | 24 | "security_scheme": "securitySchemes", |
24 | 25 | }, |
102 | 103 | < self.MAX_EXCLUSIVE_VERSION |
103 | 104 | ): |
104 | 105 | raise exceptions.APISpecError( |
105 | "Not a valid OpenAPI version number: {}".format(openapi_version) | |
106 | f"Not a valid OpenAPI version number: {openapi_version}" | |
106 | 107 | ) |
107 | 108 | super().__init__(openapi_version) |
108 | 109 |
14 | 14 | yaml.add_representer(OrderedDict, YAMLDumper._represent_dict, Dumper=YAMLDumper) |
15 | 15 | |
16 | 16 | |
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) | |
19 | 21 | |
20 | 22 | |
21 | 23 | def load_yaml_from_docstring(docstring): |
0 | 0 | from marshmallow import Schema, fields |
1 | ||
2 | from apispec.ext.marshmallow.openapi import MARSHMALLOW_VERSION_INFO | |
3 | 1 | |
4 | 2 | |
5 | 3 | class PetSchema(Schema): |
6 | 4 | 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"]}) | |
8 | 6 | name = fields.Str( |
9 | 7 | 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 | }, | |
13 | 13 | ) |
14 | password = fields.Str(load_only=True, description=description["password"]) | |
14 | password = fields.Str( | |
15 | load_only=True, metadata={"description": description["password"]} | |
16 | ) | |
15 | 17 | |
16 | 18 | |
17 | 19 | class SampleSchema(Schema): |
33 | 35 | |
34 | 36 | |
35 | 37 | 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}) | |
38 | 40 | |
39 | 41 | |
40 | 42 | class SelfReferencingSchema(Schema): |
41 | 43 | 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)) | |
48 | 46 | |
49 | 47 | |
50 | 48 | class OrderedSchema(Schema): |
59 | 57 | |
60 | 58 | |
61 | 59 | 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) | |
67 | 67 | |
68 | 68 | |
69 | 69 | class CategorySchema(Schema): |
0 | import copy | |
0 | 1 | from collections import OrderedDict |
1 | 2 | from http import HTTPStatus |
2 | 3 | |
16 | 17 | get_examples, |
17 | 18 | get_paths, |
18 | 19 | get_parameters, |
20 | get_headers, | |
19 | 21 | get_responses, |
20 | 22 | get_security_schemes, |
21 | 23 | build_ref, |
26 | 28 | 'about Swagger at <a href="http://swagger.wordnik.com">http://swagger.wordnik.com</a> ' |
27 | 29 | "or on irc.freenode.net, #swagger. For this sample, you can use the api " |
28 | 30 | '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 | ) | |
29 | 94 | |
30 | 95 | |
31 | 96 | @pytest.fixture(params=("2.0", "3.0.0")) |
59 | 124 | version="1.0.0", |
60 | 125 | openapi_version=openapi_version, |
61 | 126 | info={"description": description}, |
62 | **security_kwargs | |
127 | **security_kwargs, | |
63 | 128 | ) |
64 | 129 | |
65 | 130 | |
130 | 195 | assert spec.to_dict()["tags"] == [{"name": "tag1"}, {"name": "tag2"}] |
131 | 196 | |
132 | 197 | |
133 | class TestComponents: | |
198 | class TestComponents(RefsSchemaTestMixin): | |
134 | 199 | |
135 | 200 | properties = { |
136 | 201 | "id": {"type": "integer", "format": "int64"}, |
201 | 266 | spec.components.response("test_response") |
202 | 267 | |
203 | 268 | 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 | |
204 | 271 | parameter = {"format": "int64", "type": "integer"} |
205 | 272 | spec.components.parameter("PetId", "path", parameter) |
206 | 273 | params = get_parameters(spec) |
226 | 293 | ): |
227 | 294 | spec.components.parameter("test_parameter", "path") |
228 | 295 | |
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 | ||
229 | 323 | # Referenced examples are only supported in OAS 3.x |
230 | 324 | @pytest.mark.parametrize("spec", ("3.0.0",), indirect=True) |
231 | 325 | def test_example(self, spec): |
290 | 384 | "TestSchema": {"properties": {"key": {"type": "string"}}, "type": "object"} |
291 | 385 | } |
292 | 386 | |
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): | |
295 | 595 | paths = { |
296 | 596 | "/pet/{petId}": { |
297 | 597 | "get": { |
306 | 606 | } |
307 | 607 | ], |
308 | 608 | "responses": { |
309 | "200": {"schema": "Pet", "description": "successful operation"}, | |
609 | "200": {"description": "successful operation"}, | |
310 | 610 | "400": {"description": "Invalid ID supplied"}, |
311 | 611 | "404": {"description": "Pet not found"}, |
312 | 612 | }, |
359 | 659 | "/path4", |
360 | 660 | ] |
361 | 661 | |
362 | def test_paths_is_chainable(self, spec): | |
662 | def test_path_is_chainable(self, spec): | |
363 | 663 | spec.path(path="/path1").path("/path2") |
364 | 664 | assert list(spec.to_dict()["paths"].keys()) == ["/path1", "/path2"] |
365 | 665 | |
366 | def test_methods_maintain_order(self, spec): | |
666 | def test_path_methods_maintain_order(self, spec): | |
367 | 667 | methods = ["get", "post", "put", "patch", "delete", "head", "options"] |
368 | 668 | for method in methods: |
369 | 669 | spec.path(path="/path", operations=OrderedDict({method: {}})) |
461 | 761 | assert p["summary"] == summary |
462 | 762 | assert p["description"] == description |
463 | 763 | |
464 | def test_parameter(self, spec): | |
764 | def test_path_resolves_parameter(self, spec): | |
465 | 765 | route_spec = self.paths["/pet/{petId}"]["get"] |
466 | ||
467 | 766 | spec.components.parameter("test_parameter", "path", route_spec["parameters"][0]) |
468 | ||
469 | 767 | spec.path( |
470 | 768 | path="/pet/{petId}", operations={"get": {"parameters": ["test_parameter"]}} |
471 | 769 | ) |
472 | ||
473 | metadata = spec.to_dict() | |
474 | 770 | p = get_paths(spec)["/pet/{petId}"]["get"] |
475 | ||
476 | 771 | 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 | ) | |
486 | 772 | |
487 | 773 | @pytest.mark.parametrize( |
488 | 774 | "parameters", |
489 | 775 | ([{"name": "petId"}], [{"in": "path"}]), # missing "in" # missing "name" |
490 | 776 | ) |
491 | def test_invalid_parameter(self, spec, parameters): | |
777 | def test_path_invalid_parameter(self, spec, parameters): | |
492 | 778 | path = "/pet/{petId}" |
493 | 779 | |
494 | 780 | with pytest.raises(InvalidParameterError): |
563 | 849 | ], |
564 | 850 | ) |
565 | 851 | |
566 | def test_response(self, spec): | |
852 | def test_path_resolves_response(self, spec): | |
567 | 853 | route_spec = self.paths["/pet/{petId}"]["get"] |
568 | ||
569 | 854 | spec.components.response("test_response", route_spec["responses"]["200"]) |
570 | ||
571 | 855 | spec.path( |
572 | 856 | path="/pet/{petId}", |
573 | 857 | operations={"get": {"responses": {"200": "test_response"}}}, |
574 | 858 | ) |
575 | ||
576 | metadata = spec.to_dict() | |
577 | 859 | p = get_paths(spec)["/pet/{petId}"]["get"] |
578 | ||
579 | 860 | 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): | |
591 | 863 | code = HTTPStatus(200) |
592 | 864 | spec.path( |
593 | 865 | path="/pet/{petId}", |
596 | 868 | |
597 | 869 | assert "200" in get_paths(spec)["/pet/{petId}"]["get"]["responses"] |
598 | 870 | |
599 | def test_response_with_status_code_range(self, spec, recwarn): | |
871 | def test_path_response_with_status_code_range(self, spec, recwarn): | |
600 | 872 | status_code = "2XX" |
601 | 873 | |
602 | 874 | spec.path( |
617 | 889 | with pytest.raises(APISpecError, match=message): |
618 | 890 | spec.path("/pet/{petId}", operations={"dummy": {}}) |
619 | 891 | |
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 | ||
620 | 1025 | |
621 | 1026 | class TestPlugins: |
622 | 1027 | @staticmethod |
623 | 1028 | def test_plugin_factory(return_none=False): |
624 | 1029 | 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 | ||
625 | 1036 | def schema_helper(self, name, definition, **kwargs): |
1037 | definition.pop("dummy", None) | |
626 | 1038 | if not return_none: |
627 | 1039 | return {"properties": {"name": {"type": "string"}}} |
628 | 1040 | |
629 | 1041 | def parameter_helper(self, parameter, **kwargs): |
1042 | parameter.pop("dummy", None) | |
630 | 1043 | if not return_none: |
631 | 1044 | return {"description": "some parameter"} |
632 | 1045 | |
633 | 1046 | def response_helper(self, response, **kwargs): |
1047 | response.pop("dummy", None) | |
634 | 1048 | if not return_none: |
635 | 1049 | 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"} | |
636 | 1055 | |
637 | 1056 | def path_helper(self, path, operations, parameters, **kwargs): |
638 | 1057 | if not return_none: |
656 | 1075 | openapi_version=openapi_version, |
657 | 1076 | plugins=(self.test_plugin_factory(return_none),), |
658 | 1077 | ) |
659 | spec.components.schema("Pet") | |
1078 | schema = {"dummy": "dummy"} | |
1079 | spec.components.schema("Pet", schema) | |
660 | 1080 | definitions = get_schemas(spec) |
661 | 1081 | if return_none: |
662 | 1082 | assert definitions["Pet"] == {} |
663 | 1083 | else: |
664 | 1084 | assert definitions["Pet"] == {"properties": {"name": {"type": "string"}}} |
1085 | # Check original schema is not modified | |
1086 | assert schema == {"dummy": "dummy"} | |
665 | 1087 | |
666 | 1088 | @pytest.mark.parametrize("openapi_version", ("2.0", "3.0.0")) |
667 | 1089 | @pytest.mark.parametrize("return_none", (True, False)) |
672 | 1094 | openapi_version=openapi_version, |
673 | 1095 | plugins=(self.test_plugin_factory(return_none),), |
674 | 1096 | ) |
675 | spec.components.parameter("Pet", "body", {}) | |
1097 | parameter = {"dummy": "dummy"} | |
1098 | spec.components.parameter("Pet", "body", parameter) | |
676 | 1099 | parameters = get_parameters(spec) |
677 | 1100 | if return_none: |
678 | 1101 | assert parameters["Pet"] == {"in": "body", "name": "Pet"} |
682 | 1105 | "name": "Pet", |
683 | 1106 | "description": "some parameter", |
684 | 1107 | } |
1108 | # Check original parameter is not modified | |
1109 | assert parameter == {"dummy": "dummy"} | |
685 | 1110 | |
686 | 1111 | @pytest.mark.parametrize("openapi_version", ("2.0", "3.0.0")) |
687 | 1112 | @pytest.mark.parametrize("return_none", (True, False)) |
692 | 1117 | openapi_version=openapi_version, |
693 | 1118 | plugins=(self.test_plugin_factory(return_none),), |
694 | 1119 | ) |
695 | spec.components.response("Pet", {}) | |
1120 | response = {"dummy": "dummy"} | |
1121 | spec.components.response("Pet", response) | |
696 | 1122 | responses = get_responses(spec) |
697 | 1123 | if return_none: |
698 | 1124 | assert responses["Pet"] == {} |
699 | 1125 | else: |
700 | 1126 | 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"} | |
701 | 1150 | |
702 | 1151 | @pytest.mark.parametrize("openapi_version", ("2.0", "3.0.0")) |
703 | 1152 | @pytest.mark.parametrize("return_none", (True, False)) |
741 | 1190 | self.output = output |
742 | 1191 | |
743 | 1192 | 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") | |
745 | 1194 | |
746 | 1195 | 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") | |
748 | 1197 | |
749 | 1198 | def test_plugins_order(self): |
750 | 1199 | """Test plugins execution order in APISpec.path |
1 | 1 | |
2 | 2 | import pytest |
3 | 3 | |
4 | from marshmallow.fields import Field, DateTime, Dict, String, Nested, List | |
4 | from marshmallow.fields import Field, DateTime, Dict, String, Nested, List, TimeDelta | |
5 | 5 | from marshmallow import Schema |
6 | 6 | |
7 | 7 | from apispec import APISpec |
8 | 8 | from apispec.ext.marshmallow import MarshmallowPlugin |
9 | from apispec.ext.marshmallow.openapi import MARSHMALLOW_VERSION_INFO | |
10 | 9 | from apispec.ext.marshmallow import common |
11 | 10 | from apispec.exceptions import APISpecError |
12 | 11 | from .schemas import ( |
13 | 12 | PetSchema, |
13 | SampleSchema, | |
14 | 14 | AnalysisSchema, |
15 | 15 | RunSchema, |
16 | 16 | SelfReferencingSchema, |
20 | 20 | AnalysisWithListSchema, |
21 | 21 | ) |
22 | 22 | |
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 | ) | |
24 | 31 | |
25 | 32 | |
26 | 33 | class TestDefinitionHelper: |
219 | 226 | class TestComponentParameterHelper: |
220 | 227 | @pytest.mark.parametrize("schema", [PetSchema, PetSchema()]) |
221 | 228 | 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} | |
226 | 230 | spec.components.parameter("Pet", "body", param) |
227 | 231 | parameter = get_parameters(spec)["Pet"] |
228 | 232 | 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"] | |
233 | 234 | assert reference == build_ref(spec, "schema", "Pet") |
234 | 235 | |
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"] | |
236 | 252 | assert resolved_schema["properties"]["name"]["type"] == "string" |
237 | 253 | assert resolved_schema["properties"]["password"]["type"] == "string" |
238 | 254 | assert resolved_schema["properties"]["id"]["type"] == "integer" |
253 | 269 | reference = response["content"]["application/json"]["schema"] |
254 | 270 | assert reference == build_ref(spec, "schema", "Pet") |
255 | 271 | |
256 | resolved_schema = spec.components._schemas["Pet"] | |
272 | resolved_schema = spec.components.schemas["Pet"] | |
257 | 273 | assert resolved_schema["properties"]["id"]["type"] == "integer" |
258 | 274 | assert resolved_schema["properties"]["name"]["type"] == "string" |
259 | 275 | assert resolved_schema["properties"]["password"]["type"] == "string" |
266 | 282 | reference = response["headers"]["PetHeader"]["schema"] |
267 | 283 | assert reference == build_ref(spec, "schema", "Pet") |
268 | 284 | |
269 | resolved_schema = spec.components._schemas["Pet"] | |
285 | resolved_schema = spec.components.schemas["Pet"] | |
270 | 286 | assert resolved_schema["properties"]["id"]["type"] == "integer" |
271 | 287 | assert resolved_schema["properties"]["name"]["type"] == "string" |
272 | 288 | assert resolved_schema["properties"]["password"]["type"] == "string" |
279 | 295 | assert response == resp |
280 | 296 | |
281 | 297 | |
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 | ||
282 | 314 | class TestCustomField: |
283 | 315 | def test_can_use_custom_field_decorator(self, spec_fixture): |
284 | 316 | @spec_fixture.marshmallow_plugin.map_to_openapi_type(DateTime) |
327 | 359 | |
328 | 360 | |
329 | 361 | 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 | ||
330 | 375 | @pytest.mark.parametrize( |
331 | 376 | "pet_schema", |
332 | 377 | (PetSchema, PetSchema(), PetSchema(many=True), "tests.schemas.PetSchema"), |
363 | 408 | header_reference = get["responses"]["200"]["headers"]["PetHeader"]["schema"] |
364 | 409 | assert schema_reference == build_ref(spec_fixture.spec, "schema", "Pet") |
365 | 410 | 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"] | |
368 | 413 | assert resolved_schema == spec_fixture.openapi.schema2jsonschema(PetSchema) |
369 | 414 | assert get["responses"]["200"]["description"] == "successful operation" |
370 | 415 | |
412 | 457 | |
413 | 458 | assert schema_reference == build_ref(spec_fixture.spec, "schema", "Pet") |
414 | 459 | 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"] | |
417 | 512 | assert resolved_schema == spec_fixture.openapi.schema2jsonschema(PetSchema) |
418 | 513 | assert get["responses"]["200"]["description"] == "successful operation" |
419 | 514 | |
439 | 534 | p = get_paths(spec_fixture.spec)["/pet"] |
440 | 535 | get = p["get"] |
441 | 536 | assert get["parameters"] == spec_fixture.openapi.schema2parameters( |
442 | PetSchema(), default_in="query" | |
537 | PetSchema(), location="query" | |
443 | 538 | ) |
444 | 539 | post = p["post"] |
445 | 540 | assert post["parameters"] == spec_fixture.openapi.schema2parameters( |
446 | 541 | PetSchema, |
447 | default_in="body", | |
542 | location="body", | |
448 | 543 | required=True, |
449 | 544 | name="pet", |
450 | 545 | description="a pet schema", |
468 | 563 | p = get_paths(spec_fixture.spec)["/pet"] |
469 | 564 | get = p["get"] |
470 | 565 | assert get["parameters"] == spec_fixture.openapi.schema2parameters( |
471 | PetSchema(), default_in="query" | |
566 | PetSchema(), location="query" | |
472 | 567 | ) |
473 | 568 | for parameter in get["parameters"]: |
474 | 569 | description = parameter.get("description", False) |
485 | 580 | assert post["requestBody"]["description"] == "a pet schema" |
486 | 581 | assert post["requestBody"]["required"] |
487 | 582 | |
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 | ||
488 | 618 | @pytest.mark.parametrize("spec_fixture", ("2.0",), indirect=True) |
489 | 619 | def test_schema_uses_ref_if_available_v2(self, spec_fixture): |
490 | 620 | spec_fixture.spec.components.schema("Pet", schema=PetSchema) |
510 | 640 | }, |
511 | 641 | ) |
512 | 642 | 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"] | |
513 | 661 | assert get["responses"]["200"]["content"]["application/json"][ |
514 | 662 | "schema" |
515 | 663 | ] == build_ref(spec_fixture.spec, "schema", "Pet") |
557 | 705 | "schema" |
558 | 706 | ] == build_ref(spec, "schema", "Pet") |
559 | 707 | |
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 | ||
560 | 790 | @pytest.mark.parametrize( |
561 | "pet_schema", (PetSchema, PetSchema(), "tests.schemas.PetSchema"), | |
791 | "pet_schema", | |
792 | (PetSchema, PetSchema(), "tests.schemas.PetSchema"), | |
562 | 793 | ) |
563 | 794 | def test_schema_name_resolver_returns_none_v2(self, pet_schema): |
564 | 795 | def resolver(schema): |
578 | 809 | assert "properties" in get["responses"]["200"]["schema"] |
579 | 810 | |
580 | 811 | @pytest.mark.parametrize( |
581 | "pet_schema", (PetSchema, PetSchema(), "tests.schemas.PetSchema"), | |
812 | "pet_schema", | |
813 | (PetSchema, PetSchema(), "tests.schemas.PetSchema"), | |
582 | 814 | ) |
583 | 815 | def test_schema_name_resolver_returns_none_v3(self, pet_schema): |
584 | 816 | def resolver(schema): |
605 | 837 | "properties" |
606 | 838 | in get["responses"]["200"]["content"]["application/json"]["schema"] |
607 | 839 | ) |
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") | |
608 | 882 | |
609 | 883 | @pytest.mark.parametrize("spec_fixture", ("2.0",), indirect=True) |
610 | 884 | def test_schema_uses_ref_in_parameters_and_request_body_if_available_v2( |
645 | 919 | p = get_paths(spec_fixture.spec)["/pet"] |
646 | 920 | assert "schema" in p["get"]["parameters"][0] |
647 | 921 | 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"] | |
648 | 943 | schema_ref = post["requestBody"]["content"]["application/json"]["schema"] |
649 | 944 | assert schema_ref == build_ref(spec_fixture.spec, "schema", "Pet") |
650 | 945 | |
720 | 1015 | ] |
721 | 1016 | assert response_schema == resolved_schema |
722 | 1017 | |
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 | ||
723 | 1063 | @pytest.mark.parametrize("spec_fixture", ("2.0",), indirect=True) |
724 | 1064 | def test_schema_partially_v2(self, spec_fixture): |
725 | 1065 | spec_fixture.spec.components.schema("Pet", schema=PetSchema) |
784 | 1124 | }, |
785 | 1125 | } |
786 | 1126 | |
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 | ||
787 | 1161 | def test_parameter_reference(self, spec_fixture): |
788 | 1162 | if spec_fixture.spec.openapi_version.major < 3: |
789 | 1163 | param = {"schema": PetSchema} |
818 | 1192 | |
819 | 1193 | def test_schema_global_state_untouched_2parameters(self, spec_fixture): |
820 | 1194 | assert get_nested_schema(RunSchema, "sample") is None |
821 | data = spec_fixture.openapi.schema2parameters(RunSchema) | |
1195 | data = spec_fixture.openapi.schema2parameters(RunSchema, location="json") | |
822 | 1196 | json.dumps(data) |
823 | 1197 | 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") | |
824 | 1218 | |
825 | 1219 | |
826 | 1220 | class TestCircularReference: |
882 | 1276 | assert "default" not in props["numbers"] |
883 | 1277 | |
884 | 1278 | |
885 | @pytest.mark.skipif( | |
886 | MARSHMALLOW_VERSION_INFO[0] < 3, reason="Values ignored in marshmallow 2" | |
887 | ) | |
888 | 1279 | class TestDictValues: |
889 | 1280 | def test_dict_values_resolve_to_additional_properties(self, spec): |
890 | 1281 | class SchemaWithDict(Schema): |
928 | 1319 | |
929 | 1320 | result = get_schemas(spec)["SchemaWithList"]["properties"]["list_field"] |
930 | 1321 | 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 | ) |
78 | 78 | assert list(get_fields(ExcludeSchema, exclude_dump_only=True).keys()) == [ |
79 | 79 | "field5" |
80 | 80 | ] |
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) |
3 | 3 | import pytest |
4 | 4 | from marshmallow import fields, validate |
5 | 5 | |
6 | from apispec.ext.marshmallow.openapi import MARSHMALLOW_VERSION_INFO | |
7 | ||
8 | 6 | from .schemas import CategorySchema, CustomList, CustomStringField, CustomIntegerField |
9 | from .utils import build_ref | |
7 | from .utils import build_ref, get_schemas | |
10 | 8 | |
11 | 9 | |
12 | 10 | def test_field2choices_preserving_order(openapi): |
29 | 27 | (fields.DateTime, "string"), |
30 | 28 | (fields.Date, "string"), |
31 | 29 | (fields.Time, "string"), |
30 | (fields.TimeDelta, "integer"), | |
32 | 31 | (fields.Email, "string"), |
33 | 32 | (fields.URL, "string"), |
34 | 33 | # Custom fields inherit types from their parents |
60 | 59 | @pytest.mark.parametrize( |
61 | 60 | ("FieldClass", "expected_format"), |
62 | 61 | [ |
63 | (fields.Integer, "int32"), | |
64 | (fields.Float, "float"), | |
65 | 62 | (fields.UUID, "uuid"), |
66 | 63 | (fields.DateTime, "date-time"), |
67 | 64 | (fields.Date, "date"), |
76 | 73 | |
77 | 74 | |
78 | 75 | def test_field_with_description(spec_fixture): |
79 | field = fields.Str(description="a username") | |
76 | field = fields.Str(metadata={"description": "a username"}) | |
80 | 77 | res = spec_fixture.openapi.field2property(field) |
81 | 78 | assert res["description"] == "a username" |
82 | 79 | |
83 | 80 | |
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") | |
86 | 83 | res = spec_fixture.openapi.field2property(field) |
87 | 84 | assert res["default"] == "bar" |
88 | 85 | |
89 | 86 | |
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) | |
92 | 89 | res = spec_fixture.openapi.field2property(field) |
93 | 90 | assert res["default"] is False |
94 | 91 | |
95 | 92 | |
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)) | |
101 | 95 | res = spec_fixture.openapi.field2property(field) |
102 | 96 | assert res["default"] == dt.date(2014, 7, 18).isoformat() |
103 | 97 | |
104 | 98 | |
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") | |
107 | 101 | res = spec_fixture.openapi.field2property(field) |
108 | 102 | assert "default" not in res |
109 | 103 | |
110 | 104 | |
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"}) | |
113 | 107 | res = spec_fixture.openapi.field2property(field) |
114 | 108 | assert res["default"] == "Manual default" |
115 | 109 | |
116 | 110 | |
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}) | |
119 | 113 | res = spec_fixture.openapi.field2property(field) |
120 | 114 | assert res["default"] == 42 |
121 | 115 | |
134 | 128 | |
135 | 129 | def test_only_allows_valid_properties_in_metadata(spec_fixture): |
136 | 130 | 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 | |
145 | 141 | assert "description" in res |
146 | 142 | assert "enum" in res |
147 | 143 | assert "allOf" in res |
160 | 156 | |
161 | 157 | |
162 | 158 | 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}) | |
164 | 160 | res = spec_fixture.openapi.field2property(field) |
165 | 161 | assert res["maxLength"] == 100 |
166 | 162 | assert res["minLength"] == 6 |
167 | 163 | |
168 | 164 | |
165 | @pytest.mark.parametrize("spec_fixture", ("2.0", "3.0.0", "3.1.0"), indirect=True) | |
169 | 166 | def test_field_with_allow_none(spec_fixture): |
170 | 167 | field = fields.Str(allow_none=True) |
171 | 168 | res = spec_fixture.openapi.field2property(field) |
172 | 169 | if spec_fixture.openapi.openapi_version.major < 3: |
173 | 170 | assert res["x-nullable"] is True |
171 | elif spec_fixture.openapi.openapi_version.minor < 1: | |
172 | assert res["nullable"] is True | |
174 | 173 | 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"] | |
176 | 235 | |
177 | 236 | |
178 | 237 | def test_field_with_str_regex(spec_fixture): |
207 | 266 | spec_fixture.spec.components.schema("Category", schema=CategorySchema) |
208 | 267 | category = fields.Nested( |
209 | 268 | 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 | }, | |
213 | 274 | ) |
214 | 275 | result = spec_fixture.openapi.field2property(category) |
215 | 276 | assert result == { |
273 | 334 | } |
274 | 335 | |
275 | 336 | |
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 | ||
276 | 383 | def test_custom_properties_for_custom_fields(spec_fixture): |
277 | 384 | def custom_string2properties(self, field, **kwargs): |
278 | 385 | ret = {} |
292 | 399 | assert properties["x-customString"] == ( |
293 | 400 | spec_fixture.openapi.openapi_version == "2.0" |
294 | 401 | ) |
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"} |
0 | 0 | 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 | |
3 | 5 | |
4 | 6 | from apispec.ext.marshmallow import MarshmallowPlugin |
5 | from apispec.ext.marshmallow.openapi import MARSHMALLOW_VERSION_INFO | |
6 | 7 | from apispec import exceptions, utils, APISpec |
7 | 8 | |
8 | 9 | from .schemas import CustomList, CustomStringField |
10 | 11 | |
11 | 12 | |
12 | 13 | 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") | |
16 | 19 | if openapi.openapi_version.major < 3: |
17 | 20 | assert res[0]["default"] == "bar" |
18 | 21 | else: |
19 | 22 | 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" | |
63 | 23 | |
64 | 24 | # json/body is invalid for OpenAPI 3 |
65 | 25 | @pytest.mark.parametrize("openapi", ("2.0",), indirect=True) |
68 | 28 | id = fields.Int() |
69 | 29 | |
70 | 30 | schema = ExampleSchema(many=True) |
71 | res = openapi.schema2parameters(schema=schema, default_in="json") | |
31 | res = openapi.schema2parameters(schema=schema, location="json") | |
72 | 32 | assert res[0]["in"] == "body" |
73 | 33 | |
74 | 34 | def test_fields_with_dump_only(self, openapi): |
75 | 35 | class UserSchema(Schema): |
76 | 36 | name = fields.Str(dump_only=True) |
77 | 37 | |
78 | res = openapi.fields2parameters(UserSchema._declared_fields, default_in="query") | |
38 | res = openapi.schema2parameters(schema=UserSchema(), location="query") | |
79 | 39 | 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") | |
81 | 48 | assert len(res) == 0 |
82 | 49 | |
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 | ||
92 | 50 | |
93 | 51 | class TestMarshmallowSchemaToModelDefinition: |
94 | def test_invalid_schema(self, openapi): | |
95 | with pytest.raises(ValueError): | |
96 | openapi.schema2jsonschema(None) | |
97 | ||
98 | 52 | def test_schema2jsonschema_with_explicit_fields(self, openapi): |
99 | 53 | class UserSchema(Schema): |
100 | 54 | _id = fields.Int() |
101 | email = fields.Email(description="email address of the user") | |
55 | email = fields.Email(metadata={"description": "email address of the user"}) | |
102 | 56 | name = fields.Str() |
103 | 57 | |
104 | 58 | class Meta: |
113 | 67 | assert props["email"]["format"] == "email" |
114 | 68 | assert props["email"]["description"] == "email address of the user" |
115 | 69 | |
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): | |
146 | 71 | class ExampleSchema(Schema): |
147 | 72 | _id = fields.Int(data_key="id") |
148 | 73 | _global = fields.Int(data_key="global") |
177 | 102 | res = openapi.schema2jsonschema(BandSchema(partial=("drummer",))) |
178 | 103 | assert res["required"] == ["bassist"] |
179 | 104 | |
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 | ||
180 | 120 | def test_no_required_fields(self, openapi): |
181 | 121 | class BandSchema(Schema): |
182 | 122 | drummer = fields.Str() |
206 | 146 | |
207 | 147 | res = openapi.schema2jsonschema(WhiteStripesSchema) |
208 | 148 | 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 | |
209 | 179 | |
210 | 180 | def test_only_explicitly_declared_fields_are_translated(self, openapi): |
211 | 181 | class UserSchema(Schema): |
226 | 196 | assert "email" not in props |
227 | 197 | |
228 | 198 | 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)} | |
236 | 200 | res = openapi.fields2jsonschema(fields_dict) |
237 | 201 | assert res["required"] == ["id"] |
238 | 202 | |
250 | 214 | class NotASchema: |
251 | 215 | pass |
252 | 216 | |
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." | |
255 | 219 | ) |
256 | 220 | with pytest.raises(ValueError, match=expected_error): |
257 | 221 | openapi.schema2jsonschema(NotASchema) |
260 | 224 | class TestMarshmallowSchemaToParameters: |
261 | 225 | @pytest.mark.parametrize("ListClass", [fields.List, CustomList]) |
262 | 226 | 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") | |
265 | 229 | assert res["in"] == "query" |
266 | 230 | if openapi.openapi_version.major < 3: |
267 | 231 | assert res["type"] == "array" |
274 | 238 | assert res["explode"] is True |
275 | 239 | |
276 | 240 | 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") | |
279 | 243 | assert res["required"] is True |
280 | 244 | |
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 | |
284 | 269 | |
285 | 270 | # json/body is invalid for OpenAPI 3 |
286 | 271 | @pytest.mark.parametrize("openapi", ("2.0",), indirect=True) |
289 | 274 | name = fields.Str() |
290 | 275 | email = fields.Email() |
291 | 276 | |
292 | res = openapi.schema2parameters(UserSchema, default_in="body") | |
277 | res = openapi.schema2parameters(UserSchema, location="body") | |
293 | 278 | assert len(res) == 1 |
294 | 279 | param = res[0] |
295 | 280 | assert param["in"] == "body" |
302 | 287 | name = fields.Str() |
303 | 288 | email = fields.Email(dump_only=True) |
304 | 289 | |
305 | res_nodump = openapi.schema2parameters(UserSchema, default_in="body") | |
290 | res_nodump = openapi.schema2parameters(UserSchema, location="body") | |
306 | 291 | assert len(res_nodump) == 1 |
307 | 292 | param = res_nodump[0] |
308 | 293 | assert param["in"] == "body" |
315 | 300 | name = fields.Str() |
316 | 301 | email = fields.Email() |
317 | 302 | |
318 | res = openapi.schema2parameters(UserSchema(many=True), default_in="body") | |
303 | res = openapi.schema2parameters(UserSchema(many=True), location="body") | |
319 | 304 | assert len(res) == 1 |
320 | 305 | param = res[0] |
321 | 306 | assert param["in"] == "body" |
327 | 312 | name = fields.Str() |
328 | 313 | email = fields.Email() |
329 | 314 | |
330 | res = openapi.schema2parameters(UserSchema, default_in="query") | |
315 | res = openapi.schema2parameters(UserSchema, location="query") | |
331 | 316 | assert len(res) == 2 |
332 | 317 | res.sort(key=lambda param: param["name"]) |
333 | 318 | assert res[0]["name"] == "email" |
340 | 325 | name = fields.Str() |
341 | 326 | email = fields.Email() |
342 | 327 | |
343 | res = openapi.schema2parameters(UserSchema(), default_in="query") | |
328 | res = openapi.schema2parameters(UserSchema(), location="query") | |
344 | 329 | assert len(res) == 2 |
345 | 330 | res.sort(key=lambda param: param["name"]) |
346 | 331 | assert res[0]["name"] == "email" |
354 | 339 | email = fields.Email() |
355 | 340 | |
356 | 341 | with pytest.raises(AssertionError): |
357 | openapi.schema2parameters(UserSchema(many=True), default_in="query") | |
342 | openapi.schema2parameters(UserSchema(many=True), location="query") | |
358 | 343 | |
359 | 344 | 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") | |
362 | 350 | assert len(res) == 2 |
363 | 351 | res.sort(key=lambda param: param["name"]) |
364 | 352 | assert res[0]["name"] == "email" |
370 | 358 | class NotASchema: |
371 | 359 | pass |
372 | 360 | |
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." | |
375 | 363 | ) |
376 | 364 | with pytest.raises(ValueError, match=expected_error): |
377 | 365 | openapi.schema2jsonschema(NotASchema) |
418 | 406 | assert ("i" in props) == (modifier == "only") |
419 | 407 | assert ("j" not in props) == (modifier == "only") |
420 | 408 | |
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 | ||
421 | 419 | def test_schema2jsonschema_with_nested_fields_with_adhoc_changes( |
422 | 420 | self, spec_fixture |
423 | 421 | ): |
440 | 438 | assert props["Category"] == spec_fixture.openapi.schema2jsonschema( |
441 | 439 | CategorySchema |
442 | 440 | ) |
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 | |
443 | 455 | |
444 | 456 | def test_schema2jsonschema_with_nested_excluded_fields(self, spec): |
445 | 457 | category_schema = CategorySchema(exclude=("breed",)) |
476 | 488 | "required": True, |
477 | 489 | "type": "string", |
478 | 490 | }, |
479 | openapi.field2parameter( | |
491 | openapi._field2parameter( | |
480 | 492 | field=fields.List( |
481 | 493 | fields.Str(), |
482 | 494 | validate=validate.OneOf(["freddie", "roger"]), |
483 | location="querystring", | |
484 | 495 | ), |
485 | default_in=None, | |
496 | location="query", | |
486 | 497 | name="body", |
487 | 498 | ), |
488 | 499 | ] |
489 | + openapi.schema2parameters(PageSchema, default_in="query"), | |
500 | + openapi.schema2parameters(PageSchema, location="query"), | |
490 | 501 | "responses": {200: {"schema": PetSchema, "description": "A pet"}}, |
491 | 502 | }, |
492 | 503 | "post": { |
499 | 510 | "type": "string", |
500 | 511 | } |
501 | 512 | ] |
502 | + openapi.schema2parameters(CategorySchema, default_in="body") | |
513 | + openapi.schema2parameters(CategorySchema, location="body") | |
503 | 514 | ), |
504 | 515 | "responses": {201: {"schema": PetSchema, "description": "A pet"}}, |
505 | 516 | }, |
534 | 545 | "required": True, |
535 | 546 | "schema": {"type": "string"}, |
536 | 547 | }, |
537 | openapi.field2parameter( | |
548 | openapi._field2parameter( | |
538 | 549 | field=fields.List( |
539 | 550 | fields.Str(), |
540 | 551 | validate=validate.OneOf(["freddie", "roger"]), |
541 | location="querystring", | |
542 | 552 | ), |
543 | default_in=None, | |
553 | location="query", | |
544 | 554 | name="body", |
545 | 555 | ), |
546 | 556 | ] |
547 | + openapi.schema2parameters(PageSchema, default_in="query"), | |
557 | + openapi.schema2parameters(PageSchema, location="query"), | |
548 | 558 | "responses": { |
549 | 559 | 200: { |
550 | 560 | "description": "success", |
585 | 595 | class ValidationSchema(Schema): |
586 | 596 | id = fields.Int(dump_only=True) |
587 | 597 | range = fields.Int(validate=validate.Range(min=1, max=10)) |
598 | range_no_upper = fields.Float(validate=validate.Range(min=1)) | |
588 | 599 | multiple_ranges = fields.Int( |
589 | 600 | validate=[ |
590 | 601 | validate.Range(min=1), |
610 | 621 | equal_length = fields.Str( |
611 | 622 | validate=[validate.Length(equal=5), validate.Length(min=1, max=10)] |
612 | 623 | ) |
624 | date_range = fields.DateTime( | |
625 | validate=validate.Range( | |
626 | min=datetime(1900, 1, 1), | |
627 | ) | |
628 | ) | |
613 | 629 | |
614 | 630 | @pytest.mark.parametrize( |
615 | 631 | ("field", "properties"), |
616 | 632 | [ |
617 | 633 | ("range", {"minimum": 1, "maximum": 10}), |
634 | ("range_no_upper", {"minimum": 1}), | |
618 | 635 | ("multiple_ranges", {"minimum": 3, "maximum": 7}), |
619 | 636 | ("list_length", {"minItems": 1, "maxItems": 10}), |
620 | 637 | ("custom_list_length", {"minItems": 1, "maxItems": 10}), |
622 | 639 | ("custom_field_length", {"minLength": 1, "maxLength": 10}), |
623 | 640 | ("multiple_lengths", {"minLength": 3, "maxLength": 7}), |
624 | 641 | ("equal_length", {"minLength": 5, "maxLength": 5}), |
642 | ("date_range", {"x-minimum": datetime(1900, 1, 1)}), | |
625 | 643 | ], |
626 | 644 | ) |
627 | 645 | def test_properties(self, field, properties, spec): |
25 | 25 | @pytest.mark.parametrize("docstring", (None, "", "---")) |
26 | 26 | def test_load_operations_from_docstring_empty_docstring(docstring): |
27 | 27 | 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" |
20 | 20 | return spec.to_dict()["components"]["parameters"] |
21 | 21 | |
22 | 22 | |
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 | ||
23 | 29 | def get_examples(spec): |
24 | 30 | return spec.to_dict()["components"]["examples"] |
25 | 31 |
0 | 0 | [tox] |
1 | 1 | envlist= |
2 | 2 | lint |
3 | py{35,36,37,38}-marshmallow2 | |
4 | py{35,36,37,38}-marshmallow3 | |
3 | py{36,37,38,39}-marshmallow3 | |
5 | 4 | py38-marshmallowdev |
6 | 5 | docs |
7 | 6 | |
8 | 7 | [testenv] |
9 | 8 | extras = tests |
10 | 9 | 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 | |
13 | 11 | marshmallowdev: https://github.com/marshmallow-code/marshmallow/archive/dev.tar.gz |
14 | 12 | commands = pytest {posargs} |
15 | 13 | |
27 | 25 | [testenv:watch-docs] |
28 | 26 | deps = sphinx-autobuild |
29 | 27 | 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 | |
31 | 29 | |
32 | 30 | [testenv:watch-readme] |
33 | 31 | deps = restview |