Codebase list apispec / 409d0d9
New upstream version 3.3.1 Sophie Brun 3 years ago
57 changed file(s) with 8475 addition(s) and 0 deletion(s). Raw diff Collapse all Expand all
0 open_collective: "marshmallow"
1 tidelift: "pypi/apispec"
0 # Byte-compiled / optimized / DLL files
1 __pycache__/
2 *.py[cod]
3 *$py.class
4
5 # C extensions
6 *.so
7
8 # Distribution / packaging
9 .Python
10 build/
11 develop-eggs/
12 dist/
13 downloads/
14 eggs/
15 .eggs/
16 lib/
17 lib64/
18 parts/
19 sdist/
20 var/
21 wheels/
22 *.egg-info/
23 .installed.cfg
24 *.egg
25 MANIFEST
26
27 # PyInstaller
28 # Usually these files are written by a python script from a template
29 # before PyInstaller builds the exe, so as to inject date/other infos into it.
30 *.manifest
31 *.spec
32
33 # Installer logs
34 pip-log.txt
35 pip-delete-this-directory.txt
36
37 # Unit test / coverage reports
38 htmlcov/
39 .tox/
40 .coverage
41 .coverage.*
42 .cache
43 nosetests.xml
44 coverage.xml
45 *.cover
46 .hypothesis/
47 .pytest_cache/
48
49 # Translations
50 *.mo
51 *.pot
52
53 # Django stuff:
54 *.log
55 local_settings.py
56 db.sqlite3
57
58 # Flask stuff:
59 instance/
60 .webassets-cache
61
62 # Scrapy stuff:
63 .scrapy
64
65 # Sphinx documentation
66 docs/_build/
67 README.html
68
69 # PyBuilder
70 target/
71
72 # Jupyter Notebook
73 .ipynb_checkpoints
74
75 # pyenv
76 .python-version
77
78 # celery beat schedule file
79 celerybeat-schedule
80
81 # SageMath parsed files
82 *.sage.py
83
84 # Environments
85 .env
86 .venv
87 env/
88 venv/
89 ENV/
90 env.bak/
91 venv.bak/
92
93 # Spyder project settings
94 .spyderproject
95 .spyproject
96
97 # Rope project settings
98 .ropeproject
99
100 # mkdocs documentation
101 /site
102
103 # mypy
104 .mypy_cache/
0 repos:
1 - repo: https://github.com/asottile/pyupgrade
2 rev: v2.4.1
3 hooks:
4 - id: pyupgrade
5 args: [--py3-plus]
6 - repo: https://github.com/python/black
7 rev: 19.10b0
8 hooks:
9 - id: black
10 language_version: python3
11 - repo: https://gitlab.com/pycqa/flake8
12 rev: 3.8.1
13 hooks:
14 - id: flake8
15 additional_dependencies: [flake8-bugbear==20.1.4]
16 - repo: https://github.com/asottile/blacken-docs
17 rev: v1.7.0
18 hooks:
19 - id: blacken-docs
20 additional_dependencies: [black==19.10b0]
0 *******
1 Authors
2 *******
3
4 Leads
5 =====
6
7 - Steven Loria `@sloria <https://github.com/sloria>`_
8 - Jérôme Lafréchoux `@lafrech <https://github.com/lafrech>`_
9
10 Contributors (chronological)
11 ============================
12
13 - Josh Johnston `@Trii <https://github.com/Trii>`_
14 - Vlad Frolov `@frol <https://github.com/frol>`_
15 - Josh Carp `@jmcarp <https://github.com/jmcarp>`_
16 - Andrew Pashkin `@AndrewPashkin <https://github.com/AndrewPashkin>`_
17 - João Taveira Araújo `@jta <https://github.com/jta>`_
18 - Giacomo Tagliabue `@itajaja <https://github.com/itajaja>`_
19 - Ben Beadle `@benbeadle <https://github.com/benbeadle>`_
20 - Martin Latrille `@martinlatrille <https://github.com/martinlatrille>`_
21 - Lucas Costa `@lucascosta <https://github.com/lucascosta>`_
22 - Jared Deckard `@deckar01 <https://github.com/deckar01>`_
23 - Eric Bobbitt `@ericb <https://github.com/ericb>`_
24 - Nick Phillips `@incognick <https://github.com/incognick>`_
25 - Ashish Ranjan `@ranjanashish <https://github.com/ranjanashish>`_
26 - Jérôme Lafréchoux `@lafrech <https://github.com/lafrech>`_
27 - Anders Steinlein `@asteinlein <https://github.com/asteinlein>`_
28 - Yuri Heupa `@YuriHeupa <https://github.com/YuriHeupa>`_
29 - Matija Besednik `@matijabesednik <https://github.com/matijabesednik>`_
30 - Boris Serebrov `@serebrov <https://github.com/serebrov>`_
31 - Daniel Radetsky `@dradetsky <https://github.com/dradetsky>`_
32 - Lucas Coutinho `@lucasrc <https://github.com/lucasrc>`_
33 - `@lamiskin <https://github.com/lamiskin>`_
34 - Florian Scheffler `@nebularazer <https://github.com/nebularazer>`_
35 - Yoichi NAKAYAMA `@yoichi <https://github.com/yoichi>`_
36 - Vadim Radovel `@NightBlues <https://github.com/NightBlues>`_
37 - Douglas Anderson `@djanderson <https://github.com/djanderson>`_
38 - Marat Sharafutdinov `@decaz <https://github.com/decaz>`_
39 - Daniel Radetsky `@dradetsky <https://github.com/dradetsky>`_
40 - Evgeny Seliverstov `@theirix <https://github.com/theirix>`_
41 - Michael Bangert `@Bangertm <https://github.com/Bangertm>`_
42 - Bastien Sevajol `@buxx <https://github.com/buxx>`_
43 - Durmus Karatay `@ukaratay <https://github.com/ukaratay>`_
44 - Julien Danjou `@jd <https://github.com/jd>`_
45 - Daisuke Taniwaki `@dtaniwaki <https://github.com/dtaniwaki>`_
46 - `@mathewmarcus <https://github.com/mathewmarcus>`_
47 - Louis-Philippe Huberdeau `@lphuberdeau <https://github.com/lphuberdeau>`_
48 - Urban `@UrKr <https://github.com/UrKr>`_
49 - Christina Long `@cvlong <https://github.com/cvlong>`_
50 - Felix Yan `@felixonmars <https://github.com/felixonmars>`_
51 - Guoli Lyu `@Guoli-Lyu <https://github.com/Guoli-Lyu>`_
52 - Laura Beaufort `@lbeaufort <https://github.com/lbeaufort>`_
53 - Marcin Lulek `@ergo <https://github.com/ergo>`_
54 - Jonathan Beezley `@jbeezley <https://github.com/jbeezley>`_
55 - David Stapleton `@dstape <https://github.com/DStape>`_
56 - Szabolcs Blága `@blagasz <https://github.com/blagasz>`_
57 - Andrew Johnson `@andrjohn <https://github.com/andrjohn>`_
58 - Dave `@zedrdave <https://github.com/zedrdave>`_
59 - Emmanuel Valette `@karec <https://github.com/karec/>`_
60 - Hugo van Kemenade `@hugovk <https://github.com/hugovk>`_
61 - Bastien Gerard `@bagerard <https://github.com/bagerard>`_
62 - Ashutosh Chaudhary `@codeasashu <https://github.com/codeasashu>`_
63 - Fedor Fominykh `@fedorfo <https://github.com/fedorfo>`_
64 - Colin Bounouar `@Colin-b <https://github.com/Colin-b>`_
0 Changelog
1 ---------
2
3 3.3.1 (2020-06-06)
4 ******************
5
6 Bug fixes:
7
8 - Fix ``MarshmallowPlugin`` crash when ``resolve_schema_dict`` is passed a
9 schema as string and ``schema_name_resolver`` returns ``None``
10 (:issue:`566`). Thanks :user:`black3r` for reporting and thanks
11 :user:`Bangterm` for the PR.
12
13 3.3.0 (2020-02-14)
14 ******************
15
16 Features:
17
18 - Instantiate ``Components`` before calling plugins' ``init_spec`` (:pr:`539`).
19 Thanks :user:`Colin-b` for the PR.
20
21 3.2.0 (2019-12-22)
22 ******************
23
24 Features:
25
26 - Add ``match_info`` to ``__location_map__`` (:pr:`517`).
27 Thanks :user:`fedorfo` for the PR.
28
29 3.1.1 (2019-12-17)
30 ******************
31
32 Bug fixes:
33
34 - Don't emit a warning when passing "default" as response status code in OASv2
35 (:pr:`521`).
36
37 3.1.0 (2019-11-04)
38 ******************
39
40 Features:
41
42 - Add `apispec.core.Components.example` for adding Example Objects
43 (:pr:`515`). Thanks :user:`codeasashu` for the PR.
44
45 Support:
46
47 - Test against Python 3.8 (:pr:`510`).
48
49 3.0.0 (2019-09-17)
50 ++++++++++++++++++
51
52 Features:
53
54 - Add support for generating user-defined OpenAPI properties for custom field
55 classes via an ``add_attribute_function`` method (:pr:`478` and :pr:`498`).
56 - [apispec.ext.marshmallow]: *Backwards-incompatible* ``fields.Raw`` and
57 ``fields.Field`` are now represented by OpenAPI
58 `Any Type <https://swagger.io/docs/specification/data-models/data-types/#any>`_
59 (:pr:`495`).
60 - [apispec.ext.marshmallow]: *Backwards-incompatible*: The
61 ``schema_name_resolver`` function now receives a ``Schema`` class, a
62 ``Schema`` instance or a string that resolves to a ``Schema`` class. This
63 allows a custom resolver to generate different names depending on schema
64 modifiers used in a ``Schema`` instance (:pr:`476`).
65
66 Bug fixes:
67
68 - [apispec.ext.marshmallow]: With marshmallow 3, the default value of a field
69 in the documentation is the serialized value of the ``missing`` attribute,
70 not ``missing`` itself (:pr:`490`).
71
72 Refactoring:
73
74 - ``clean_parameters`` and ``clean_operations`` are now ``APISpec`` methods
75 (:pr:`489`).
76 - [apispec.ext.marshmallow]: ``Schema`` resolver methods are extracted from
77 ``MarshmallowPlugin`` into a ``SchemaResolver`` class member (:pr:`496`).
78 - [apispec.ext.marshmallow]: ``OpenAPIConverter`` is now a class member of
79 ``MarshmallowPlugin`` (:pr:`493`).
80 - [apispec.ext.marshmallow]: ``Field`` to properties conversion logic is
81 extracted from ``OpenAPIConverter`` into ``FieldConverterMixin`` (:pr:`478`).
82
83 Other changes:
84
85 - Drop support for Python 2 (:issue:`491`). Thanks :user:`hugovk` for the PR.
86 - Drop support for marshmallow pre-releases. Only stable 2.x and 3.x versions
87 are supported (:issue:`485`).
88
89 2.0.2 (2019-07-04)
90 ++++++++++++++++++
91
92 Bug fixes:
93
94 - Fix compatibility with marshmallow 3.0.0rc8 (:pr:`469`).
95
96 Other changes:
97
98 - Switch to Azure Pipelines (:pr:`468`).
99
100 2.0.1 (2019-06-26)
101 ++++++++++++++++++
102
103 Bug fixes:
104
105 - Don't mutate ``operations`` and ``parameters`` in ``APISpec.path`` to avoid
106 issues when calling it twice with the same ``operations`` or ``parameters``
107 (:pr:`464`).
108
109 2.0.0 (2019-06-18)
110 ++++++++++++++++++
111
112 Features:
113
114 - Add support for path level parameters (:issue:`453`).
115 Thanks :user:`karec` for the PR.
116 - *Backwards-incompatible*: A `apispec.exceptions.DuplicateParameterError` is
117 raised when two parameters with same name and location are passed to a path
118 or an operation (:pr:`455`).
119 - *Backwards-incompatible*: A `apispec.exceptions.InvalidParameterError` is
120 raised when a parameter is missing required ``name`` and ``in`` attributes
121 after helpers have been executed (:pr:`455`).
122
123 Other changes:
124
125 - *Backwards-incompatible*: All plugin helpers must accept extra `**kwargs`
126 (:issue:`453`).
127 - *Backwards-incompatible*: Components must be referenced by ID, not full path
128 (:issue:`463`).
129
130 1.3.3 (2019-05-05)
131 ++++++++++++++++++
132
133 Bug fixes:
134
135 - marshmallow 3.0.0rc6 compatibility (:pr:`445`).
136
137 1.3.2 (2019-05-02)
138 ++++++++++++++++++
139
140 Bug fixes:
141
142 - Fix handling of OpenAPI v3 components content without schema in
143 ``MarshmallowPlugin`` (:pr:`443`).
144
145 1.3.1 (2019-04-29)
146 ++++++++++++++++++
147
148 Bug fixes:
149
150 - Fix handling of `http.HTTPStatus` objects (:issue:`426`). Thanks
151 :user:`DStape`.
152 - [apispec.ext.marshmallow]: Ensure make_schema_key returns a unique key on
153 unhashable iterables (:pr:`416`, :pr:`439`). Thanks :user:`zedrdave`.
154
155 1.3.0 (2019-04-24)
156 ++++++++++++++++++
157
158 Features:
159
160 - [apispec.ext.marshmallow]: Use class hierarchy to infer
161 ``type`` and ``format`` properties (:issue:`433`, :issue:`250`).
162 Thanks :user:`andrjohn` for the PR.
163
164 1.2.1 (2019-04-18)
165 ++++++++++++++++++
166
167 Bug fixes:
168
169 - Fix error in ``MarshmallowPlugin`` when passing ``exclude`` and ``dump_only``
170 as ``class Meta`` attributes mixing ``list`` and ``tuple`` (:pr:`431`).
171 Thanks :user:`blagasz` for the PR.
172
173 1.2.0 (2019-04-08)
174 ++++++++++++++++++
175
176 Features:
177
178 - Strip empty sections (components, tags) from generated documentation
179 (:pr:`421` and :pr:`425`).
180
181 1.1.2 (2019-04-07)
182 ++++++++++++++++++
183
184 Bug fixes:
185
186 - Fix behavior when using "2xx", 3xx", etc. for response keys (:issue:`422`).
187 Thanks :user:`zachmullen` for reporting.
188
189 1.1.1 (2019-04-02)
190 ++++++++++++++++++
191
192 Bug fixes:
193
194 - Fix passing references for parameters/responses when using
195 ``MarshmallowPlugin`` (:pr:`414`).
196
197 1.1.0 (2019-03-17)
198 ++++++++++++++++++
199
200 Features:
201
202 - Resolve ``Schema`` classes in response headers (:pr:`409`).
203
204 1.0.0 (2019-02-08)
205 ++++++++++++++++++
206
207 Features:
208
209 - Expanded support for OpenAPI Specification version 3 (:issue:`165`).
210 - Add ``summary`` and ``description`` parameters to ``APISpec.path``
211 (:issue:`227`). Thanks :user:`timakro` for the suggestion.
212 - Add `apispec.core.Components.security_scheme` for adding Security
213 Scheme Objects (:issue:`245`).
214 - [apispec.ext.marshmallow]: Add support for outputting field patterns
215 from ``Regexp`` validators (:pr:`364`).
216 Thanks :user:`DStape` for the PR.
217
218 Bug fixes:
219
220 - [apispec.ext.marshmallow]: Fix automatic documentation of schemas when
221 using ``Nested(MySchema, many==True)`` (:issue:`383`). Thanks
222 :user:`whoiswes` for reporting.
223
224 Other changes:
225
226 - *Backwards-incompatible*: Components properties are now passed as dictionaries rather than keyword arguments (:pr:`381`).
227
228 .. code-block:: python
229
230 # <1.0.0
231 spec.components.schema("Pet", properties={"name": {"type": "string"}})
232 spec.components.parameter("PetId", "path", format="int64", type="integer")
233 spec.components.response("NotFound", description="Pet not found")
234
235 # >=1.0.0
236 spec.components.schema("Pet", {"properties": {"name": {"type": "string"}}})
237 spec.components.parameter("PetId", "path", {"format": "int64", "type": "integer"})
238 spec.components.response("NotFound", {"description": "Pet not found"})
239
240 Deprecations/Removals:
241
242 - *Backwards-incompatible*: The ``ref`` argument passed to fields is no
243 longer used (:issue:`354`). References for nested ``Schema`` are
244 stored automatically.
245 - *Backwards-incompatible*: The ``extra_fields`` argument of
246 `apispec.core.Components.schema` is removed. All properties may be
247 passed in the ``component`` argument.
248
249 .. code-block:: python
250
251 # <1.0.0
252 spec.definition("Pet", schema=PetSchema, extra_fields={"discriminator": "name"})
253
254 # >=1.0.0
255 spec.components.schema("Pet", schema=PetSchema, component={"discriminator": "name"})
256
257 1.0.0rc1 (2018-01-29)
258 +++++++++++++++++++++
259
260 Features:
261
262 - Automatically generate references to nested schemas with a computed name, e.g.
263 ``fields.Nested(PetSchema())`` -> ``#components/schemas/Pet``.
264 - Automatically generate references for ``requestBody`` using the above mechanism.
265 - Ability to opt out of the above behavior by passing a ``schema_name_resolver``
266 function that returns ``None`` to ``api.ext.MarshmallowPlugin``.
267 - References now respect Schema modifiers, including ``exclude`` and ``partial``.
268 - *Backwards-incompatible*: A `apispec.exceptions.DuplicateComponentNameError` is raised
269 when registering two components with the same name (:issue:`340`).
270
271 1.0.0b6 (2018-12-16)
272 ++++++++++++++++++++
273
274 Features:
275
276 - *Backwards-incompatible*: `basePath` is not removed from paths anymore.
277 Paths passed to ``APISpec.path`` should not contain the application base path
278 (:pr:`345`).
279 - Add ``apispec.ext.marshmallow.openapi.OpenAPIConverter.resolve_schema_class`` (:pr:`346`).
280 Thanks :user:`buxx`.
281
282 1.0.0b5 (2018-11-06)
283 ++++++++++++++++++++
284
285 Features:
286
287 - ``apispec.core.Components`` is added. Each ``APISpec`` instance has a
288 ``Components`` object used to define components such as schemas, parameters
289 or reponses. "Components" is the OpenAPI v3 terminology for those reusable
290 top-level objects.
291 - ``apispec.core.Components.parameter`` and ``apispec.core.Components.response``
292 are added.
293 - *Backwards-incompatible*: ``apispec.APISpec.add_path`` and
294 ``apispec.APISpec.add_tag`` are renamed to ``apispec.APISpec.path`` and
295 ``apispec.APISpec.tag``.
296 - *Backwards-incompatible*: ``apispec.APISpec.definition`` is moved to the
297 ``Components`` class and renamed to ``apispec.core.Components.schema``.
298
299 ::
300
301 # apispec<1.0.0b5
302 spec.add_tag({'name': 'Pet', 'description': 'Operations on pets'})
303 spec.add_path('/pets/', operations=...)
304 spec.definition('Pet', properties=...)
305
306 # apispec>=1.0.0b5
307 spec.tag({'name': 'Pet', 'description': 'Operations on pets'})
308 spec.path('/pets/', operations=...)
309 spec.components.schema('Pet', properties=...)
310
311 - Plugins can define ``parameter_helper`` and ``response_helper`` to modify
312 parameter and response components definitions.
313 - ``MarshmallowPlugin`` resolves schemas in parameters and responses components.
314 - Components helpers may return ``None`` as a no-op rather than an empty `dict`
315 (:pr:`336`).
316
317 Bug fixes:
318
319 - ``MarshmallowPlugin.schema_helper`` does not crash when no schema is passed
320 (:pr:`336`).
321
322 Deprecations/Removals:
323
324 - The legacy ``response_helper`` feature is removed. The same can be achieved
325 from ``operation_helper``.
326
327 1.0.0b4 (2018-10-28)
328 ++++++++++++++++++++
329
330 - *Backwards-incompatible*: ``apispec.ext.flask``,
331 ``apispec.ext.bottle``, and ``apispec.ext.tornado`` are moved to
332 a separate package, `apispec-webframeworks <https://github.com/marshmallow-code/apispec-webframeworks>`_.
333 (:issue:`302`).
334
335 If you use these plugins, install ``apispec-webframeworks`` and
336 update your imports like so: ::
337
338 # apispec<1.0.0b4
339 from apispec.ext.flask import FlaskPlugin
340
341 # apispec>=1.0.0b4
342 from apispec_webframeworks.flask import FlaskPlugin
343
344 Thanks :user:`ergo` for the suggestion and the PR.
345
346 1.0.0b3 (2018-10-08)
347 ++++++++++++++++++++
348
349 Features:
350
351 - [apispec.core]: *Backwards-incompatible*: ``openapi_version`` parameter of
352 ``APISpec`` class does not default to `'2.0'` anymore and ``info`` parameter
353 is merged with ``**options`` kwargs.
354
355 Bug fixes:
356
357 - [apispec.ext.marshmallow]: Exclude ``load_only`` fields when documenting
358 responses (:issue:`119`). Thanks :user:`luisincrespo` for reporting.
359 - [apispec.ext.marshmallow]: Exclude ``dump_only`` fields when documenting
360 request body parameter schema.
361
362 1.0.0b2 (2018-09-09)
363 ++++++++++++++++++++
364
365 - Drop deprecated plugin interface. Only plugin classes are now supported. This
366 includes the removal of ``APISpec``'s ``register_*_helper`` methods, as well
367 as its ``schema_name_resolver`` parameter. Also drop deprecated
368 ``apispec.utils.validate_swagger``. (:pr:`259`)
369 - Use ``yaml.safe_load`` instead of ``yaml.load`` when reading
370 docstrings (:issue:`278`). Thanks :user:`lbeaufort` for the suggestion
371 and the PR.
372
373 1.0.0b1 (2018-07-29)
374 ++++++++++++++++++++
375
376 Features:
377
378 - [apispec.core]: *Backwards-incompatible*: Remove `Path` class.
379 Plugins' `path_helper` methods should now return a path as a string
380 and optionally mutate the `operations` dictionary (:pr:`238`).
381 - [apispec.core]: *Backwards-incompatible*: YAML support is optional. To
382 install with YAML support, use ``pip install 'apispec[yaml]'``. You
383 will need to do this if you use ``FlaskPlugin``,
384 ``BottlePlugin``, or ``TornadoPlugin`` (:pr:`251`).
385 - [apispec.ext.marshmallow]: Allow overriding the documentation for
386 a field's default. This is especially useful for documenting
387 callable defaults (:issue:`196`).
388
389 0.39.0 (2018-06-28)
390 +++++++++++++++++++
391
392 Features:
393
394 - [apispec.core]: *Backwards-incompatible*: Change plugin interface. Plugins are
395 now child classes of ``apispec.BasePlugin``. Built-in plugins are still usable
396 with the deprecated legacy interface. However, the new class interface is
397 mandatory to pass parameters to plugins or to access specific methods that used to be
398 accessed as module level functions (typically in ``apispec.ext.marshmallow.swagger``).
399 Also, ``schema_name_resolver`` is now a parameter of
400 ``apispec.ext.marshmallow.MarshmallowPlugin``. It can still be passed to ``APISpec``
401 while using the legacy interface. (:issue:`207`)
402 - [apispec.core]: *Backwards-incompatible*: ``APISpec.openapi_version`` is now an
403 ``apispec.utils.OpenAPIVersion`` instance.
404
405 0.38.0 (2018-06-10)
406 +++++++++++++++++++
407
408 Features:
409
410 - [apispec.core]: *Backwards-incompatible*: Rename ``apispec.utils.validate_swagger``
411 to ``apispec.utils.validate_spec`` and
412 ``apispec.exceptions.SwaggerError`` to ``apispec.exceptions.OpenAPIError``.
413 Using ``validate_swagger`` will raise a ``DeprecationWarning`` (:pr:`224`).
414 - [apispec.core]: ``apispec.utils.validate_spec`` no longer relies on
415 the ``check_api`` NPM module. ``prance`` and
416 ``openapi-spec-validator`` are required for validation, and can be
417 installed using ``pip install 'apispec[validation]'`` (:pr:`224`).
418 - [apispec.core]: Deep update components instead of overwriting components
419 for OpenAPI 3 (:pr:`222`). Thanks :user:`Guoli-Lyu`.
420
421 Bug fixes:
422
423 - [apispec.ext.marshmallow]: Fix description for parameters in OpenAPI 3
424 (:pr:`223`). Thanks again :user:`Guoli-Lyu`.
425
426 Other changes:
427
428 - Drop official support for Python 3.4. Only Python 2.7 and >=3.5 are
429 supported.
430
431
432 0.37.1 (2018-05-28)
433 +++++++++++++++++++
434
435 Features:
436
437 - [apispec.ext.marshmallow]: Fix OpenAPI 3 conversion of schemas in
438 parameters (:issue:`217`). Thanks :user:`Guoli-Lyu` for the PR.
439
440 0.37.0 (2018-05-14)
441 +++++++++++++++++++
442
443 Features:
444
445 - [apispec.ext.marshmallow]: Resolve an array of schema objects in
446 parameters (:issue:`209`). Thanks :user:`cvlong` for reporting and
447 implementing this.
448
449 0.36.0 (2018-05-07)
450 +++++++++++++++++++
451
452 Features:
453
454 - [apispec.ext.marshmallow]: Document ``values`` parameter of ``Dict`` field
455 as ``additionalProperties`` (:issue:`201`). Thanks :user:`UrKr`.
456
457 0.35.0 (2018-04-10)
458 +++++++++++++++++++
459
460 Features:
461
462 - [apispec.ext.marshmallow]: Recurse over properties when resolving
463 schemas (:issue:`186`). Thanks :user:`lphuberdeau`.
464 - [apispec.ext.marshmallow]: Support ``writeOnly`` and ``nullable`` in
465 OpenAPI 3 (fall back to ``x-nullable`` for OpenAPI 2) (:issue:`165`).
466 Thanks :user:`lafrech`.
467
468 Bug fixes:
469
470 - [apispec.ext.marshmallow]: Always use `field.missing` instead of
471 `field.default` when introspecting fields (:issue:`32`). Thanks
472 :user:`lafrech`.
473
474 Other changes:
475
476 - [apispec.ext.marshmallow]: Refactor some of the internal functions in
477 `apispec.ext.marshmallow.swagger` for consistent API (:issue:`199`).
478 Thanks :user:`lafrech`.
479
480 0.34.0 (2018-04-04)
481 +++++++++++++++++++
482
483 Features:
484
485 - [apispec.core]: Maintain order in which methods are added to an
486 endpoint (:issue:`189`). Thanks :user:`lafrech`.
487
488 Other changes:
489
490 - [apispec.core]: `Path` no longer inherits from `dict` (:issue:`190`).
491 Thanks :user:`lafrech`.
492
493 0.33.0 (2018-04-01)
494 +++++++++++++++++++
495
496 Features:
497
498 - [apispec.ext.marshmallow]: Respect ``data_key`` argument on fields
499 (in marshmallow 3). Thanks :user:`lafrech`.
500
501 0.32.0 (2018-03-24)
502 +++++++++++++++++++
503
504 Features:
505
506 - [apispec.ext.bottle]: Allow `app` to be passed to `spec.add_path`
507 (:issue:`188`). Thanks :user:`dtaniwaki` for the PR.
508
509 Bug fixes:
510
511 - [apispec.ext.marshmallow]: Fix issue where "body" and "required" were
512 getting overwritten when passing a ``Schema`` to a parameter
513 (:issue:`168`, :issue:`184`).
514 Thanks :user:`dlopuch` and :user:`mathewmarcus` for reporting and
515 thanks :user:`mathewmarcus` for the PR.
516
517 0.31.0 (2018-01-30)
518 +++++++++++++++++++
519
520 - [apispec.ext.marshmallow]: Use ``dump_to`` for name even if
521 ``load_from`` does not match it (:issue:`178`). Thanks :user:`LeonAgmonNacht`
522 for reporting and thanks :user:`lafrech` for the fix.
523
524 0.30.0 (2018-01-12)
525 +++++++++++++++++++
526
527 Features:
528
529 - [apispec.core]: Add ``Spec.to_yaml`` method for serializing to YAML
530 (:issue:`161`). Thanks :user:`jd`.
531
532 0.29.0 (2018-01-04)
533 +++++++++++++++++++
534
535 Features:
536
537 - [apispec.core and apispec.ext.marshmallow]: Add limited support for
538 OpenAPI v3. Pass `openapi_version='3.0.0'` to `Spec` to use it
539 (:issue:`165`). Thanks :user:`Bangertm`.
540
541 0.28.0 (2017-12-09)
542 +++++++++++++++++++
543
544 Features:
545
546 - [apispec.core and apispec.ext.marshmallow]: Add `schema_name_resolver`
547 param to `APISpec` for resolving ref names for marshmallow Schemas.
548 This is useful when a self-referencing schema is nested within another
549 schema (:issue:`167`). Thanks :user:`buxx` for the PR.
550
551 0.27.1 (2017-12-06)
552 +++++++++++++++++++
553
554 Bug fixes:
555
556 * [apispec.ext.flask]: Don't document view methods that aren't included
557 in ``app.add_url_rule(..., methods=[...]))`` (:issue:`173`). Thanks :user:`ukaratay`.
558
559 0.27.0 (2017-10-30)
560 +++++++++++++++++++
561
562 Features:
563
564 * [apispec.core]: Add ``register_operation_helper``.
565
566 Bug fixes:
567
568 * Order of plugins does not matter (:issue:`136`).
569
570 Thanks :user:`yoichi` for these changes.
571
572 0.26.0 (2017-10-23)
573 +++++++++++++++++++
574
575 Features:
576
577 * [apispec.ext.marshmallow]: Generate "enum" property with single entry
578 when the ``validate.Equal`` validator is used (:issue:`155`). Thanks
579 :user:`Bangertm` for the suggestion and PR.
580
581 Bug fixes:
582
583 * Allow OPTIONS to be documented (:issue:`162`). Thanks :user:`buxx` for
584 the PR.
585 * Fix regression from 0.25.3 that caused a ``KeyError`` (:issue:`163`). Thanks
586 :user:`yoichi`.
587
588 0.25.4 (2017-10-09)
589 +++++++++++++++++++
590
591 Bug fixes:
592
593 * [apispec.ext.marshmallow]: Fix swagger location mapping for ``default_in``
594 param in fields2parameters (:issue:`156`). Thanks :user:`decaz`.
595
596 0.25.3 (2017-09-27)
597 +++++++++++++++++++
598
599 Bug fixes:
600
601 * [apispec.ext.marshmallow]: Correctly handle multiple fields with
602 ``location=json`` (:issue:`75`). Thanks :user:`shaicantor` for
603 reporting and thanks :user:`yoichi` for the patch.
604
605
606 0.25.2 (2017-09-05)
607 +++++++++++++++++++
608
609 Bug fixes:
610
611 * [apispec.ext.marshmallow]: Avoid AttributeError when passing non-dict
612 items to path objects (:issue:`151`). Thanks :user:`yoichi`.
613
614 0.25.1 (2017-08-23)
615 +++++++++++++++++++
616
617 Bug fixes:
618
619 * [apispec.ext.marshmallow]: Fix ``use_instances`` when ``many=True`` is
620 set (:issue:`148`). Thanks :user:`theirix`.
621
622 0.25.0 (2017-08-15)
623 +++++++++++++++++++
624
625 Features:
626
627 * [apispec.ext.marshmallow]: Add ``use_instances`` parameter to
628 ``fields2paramters`` (:issue:`144`). Thanks :user:`theirix`.
629
630 Other changes:
631
632 * Don't swallow ``YAMLError`` when YAML parsing fails
633 (:issue:`135`). Thanks :user:`djanderson` for the suggestion
634 and the PR.
635
636 0.24.0 (2017-08-15)
637 +++++++++++++++++++
638
639 Features:
640
641 * [apispec.ext.marshmallow]: Add ``swagger.map_to_swagger_field``
642 decorator to support custom field classes (:issue:`120`). Thanks
643 :user:`frol` for the suggestion and thanks :user:`dradetsky` for the
644 PR.
645
646 0.23.1 (2017-08-08)
647 +++++++++++++++++++
648
649 Bug fixes:
650
651 * [apispec.ext.marshmallow]: Fix swagger location mapping for
652 ``default_in`` param in `property2parameter` (:issue:`142`). Thanks
653 :user:`decaz`.
654
655 0.23.0 (2017-08-03)
656 +++++++++++++++++++
657
658 * Pass `operations` constructed by plugins to downstream marshmallow
659 plugin (:issue:`138`). Thanks :user:`yoichi`.
660 * [apispec.ext.marshmallow] Generate parameter specification from marshmallow Schemas (:issue:`127`).
661 Thanks :user:`ewalker11` for the suggestion thanks :user:`yoichi` for the PR.
662 * [apispec.ext.flask] Add support for Flask MethodViews (:issue:`85`,
663 :issue:`125`). Thanks :user:`lafrech` and :user:`boosh` for the
664 suggestion. Thanks :user:`djanderson` and :user:`yoichi` for the PRs.
665
666 0.22.3 (2017-07-16)
667 +++++++++++++++++++
668
669 * Release wheel distribution.
670
671 0.22.2 (2017-07-12)
672 +++++++++++++++++++
673
674 Bug fixes:
675
676 * [apispec.ext.marshmallow]: Properly handle callable ``default`` values
677 in output spec (:issue:`131`). Thanks :user:`NightBlues`.
678
679 0.22.1 (2017-06-25)
680 +++++++++++++++++++
681
682 Bug fixes:
683
684 * [apispec.ext.marshmallow]: Include ``default`` in output spec when
685 ``False`` is the default for a ``Boolean`` field (:issue:`130`).
686 Thanks :user:`nebularazer`.
687
688 0.22.0 (2017-05-30)
689 +++++++++++++++++++
690
691 Features:
692
693 * [apispec.ext.bottle] Added bottle plugin (:issue:`128`). Thanks :user:`lucasrc`.
694
695 0.21.0 (2017-04-21)
696 +++++++++++++++++++
697
698 Features:
699
700 * [apispec.ext.marshmallow] Sort list of required field names in generated spec (:issue:`124`). Thanks :user:`dradetsky`.
701
702 0.20.1 (2017-04-18)
703 +++++++++++++++++++
704
705 Bug fixes:
706
707 * [apispec.ext.tornado]: Fix compatibility with Tornado>=4.5.
708 * [apispec.ext.tornado]: Fix adding paths for handlers with coroutine methods in Python 2 (:issue:`99`).
709
710 0.20.0 (2017-03-19)
711 +++++++++++++++++++
712
713 Features:
714
715 * [apispec.core]: Definition helper functions receive the ``definition`` keyword argument, which is the current state of the definition (:issue:`122`). Thanks :user:`martinlatrille` for the PR.
716
717 Other changes:
718
719 * [apispec.ext.marshmallow] *Backwards-incompatible*: Remove ``dump`` parameter from ``schema2parameters``, ``fields2parameters``, and ``field2parameter`` (:issue:`114`). Thanks :user:`lafrech` and :user:`frol` for the feedback and :user:`lafrech` for the PR.
720
721 0.19.0 (2017-03-05)
722 +++++++++++++++++++
723
724 Features:
725
726 * [apispec.core]: Add ``extra_fields`` parameter to `APISpec.definition` (:issue:`110`). Thanks :user:`lafrech` for the PR.
727 * [apispec.ext.marshmallow]: Preserve the order of ``choices`` (:issue:`113`). Thanks :user:`frol` for the PR.
728
729 Bug fixes:
730
731 * [apispec.ext.marshmallow]: 'discriminator' is no longer valid as field metadata. It should be defined by passing ``extra_fields={'discriminator': '...'}`` to `APISpec.definition`. Thanks for reporting, :user:`lafrech`.
732 * [apispec.ext.marshmallow]: Allow additional properties when translating ``Nested`` fields using ``allOf`` (:issue:`108`). Thanks :user:`lafrech` for the suggestion and the PR.
733 * [apispec.ext.marshmallow]: Respect ``dump_only`` and ``load_only`` specified in ``class Meta`` (:issue:`84`). Thanks :user:`lafrech` for the fix.
734
735 Other changes:
736
737 * Drop support for Python 3.3.
738
739
740 0.18.0 (2017-02-19)
741 +++++++++++++++++++
742
743 Features:
744
745 * [apispec.ext.marshmallow]: Translate ``allow_none`` on ``Fields`` to ``x-nullable`` (:issue:`66`). Thanks :user:`lafrech`.
746
747 0.17.4 (2017-02-16)
748 +++++++++++++++++++
749
750 Bug fixes:
751
752 * [apispec.ext.marshmallow]: Fix corruption of ``Schema._declared_fields`` when serializing an APISpec (:issue:`107`). Thanks :user:`serebrov` for the catch and patch.
753
754 0.17.3 (2017-01-21)
755 +++++++++++++++++++
756
757 Bug fixes:
758
759 * [apispec.ext.marshmallow]: Fix behavior when passing `Schema` instances to `APISpec.definition`. The `Schema's` class will correctly be registered as a an available `ref` (:issue:`84`). Thanks :user:`lafrech` for reporting and for the PR.
760
761 0.17.2 (2017-01-03)
762 +++++++++++++++++++
763
764 Bug fixes:
765
766 * [apispec.ext.tornado]: Remove usage of ``inspect.getargspec`` for Python >= 3.3 (:issue:`102`). Thanks :user:`matijabesednik`.
767
768 0.17.1 (2016-11-19)
769 +++++++++++++++++++
770
771 Bug fixes:
772
773 * [apispec.ext.marshmallow]: Prevent unnecessary warning when generating specs for marshmallow Schema's with autogenerated fields (:issue:`95`). Thanks :user:`khorolets` reporting and for the PR.
774 * [apispec.ext.marshmallow]: Correctly translate ``Length`` validator to `minItems` and `maxItems` for array-type fields (``Nested`` and ``List``) (:issue:`97`). Thanks :user:`YuriHeupa` for reporting and for the PR.
775
776 0.17.0 (2016-10-30)
777 +++++++++++++++++++
778
779 Features:
780
781 * [apispec.ext.marshmallow]: Add support for properties that start with `x-`. Thanks :user:`martinlatrille` for the PR.
782
783 0.16.0 (2016-10-12)
784 +++++++++++++++++++
785
786 Features:
787
788 * [apispec.core]: Allow ``description`` to be passed to ``APISpec.definition`` (:issue:`93`). Thanks :user:`martinlatrille`.
789
790 0.15.0 (2016-10-02)
791 +++++++++++++++++++
792
793 Features:
794
795 * [apispec.ext.marshmallow]: Allow ``'query'`` to be passed as a field location (:issue:`89`). Thanks :user:`lafrech`.
796
797 Bug fixes:
798
799 * [apispec.ext.flask]: Properly strip off ``basePath`` when ``APPLICATION_ROOT`` is set on a Flask app's config (:issue:`78`). Thanks :user:`deckar01` for reporting and :user:`asteinlein` for the PR.
800
801 0.14.0 (2016-08-14)
802 +++++++++++++++++++
803
804 Features:
805
806 * [apispec.core]: Maintain order in which paths are added to a spec (:issue:`87`). Thanks :user:`ranjanashish` for the PR.
807 * [apispec.ext.marshmallow]: Maintain order of fields when ``ordered=True`` on Schema. Thanks again :user:`ranjanashish`.
808
809 0.13.0 (2016-07-03)
810 +++++++++++++++++++
811
812 Features:
813
814 * [apispec.ext.marshmallow]: Add support for ``Dict`` field (:issue:`80`). Thanks :user:`ericb` for the PR.
815 * [apispec.ext.marshmallow]: ``dump_only`` fields add ``readOnly`` flag in OpenAPI spec (:issue:`79`). Thanks :user:`itajaja` for the suggestion and PR.
816
817 Bug fixes:
818
819 * [apispec.ext.marshmallow]: Properly exclude nested dump-only fields from parameters (:issue:`82`). Thanks :user:`incognick` for the catch and patch.
820
821 Support:
822
823 * Update tasks.py for compatibility with invoke>=0.13.0.
824
825 0.12.0 (2016-05-22)
826 +++++++++++++++++++
827
828 Features:
829
830 * [apispec.ext.marshmallow]: Inspect validators to set additional attributes (:issue:`66`). Thanks :user:`deckar01` for the PR.
831
832 Bug fixes:
833
834 * [apispec.ext.marshmallow]: Respect ``partial`` parameters on ``Schemas`` (:issue:`74`). Thanks :user:`incognick` for reporting.
835
836 0.11.1 (2016-05-02)
837 +++++++++++++++++++
838
839 Bug fixes:
840
841 * [apispec.ext.flask]: Flask plugin respects ``APPLICATION_ROOT`` from app's config (:issue:`69`). Thanks :user:`deckar01` for the catch and patch.
842 * [apispec.ext.marshmallow]: Fix support for plural schema instances (:issue:`71`). Thanks again :user:`deckar01`.
843
844 0.11.0 (2016-04-12)
845 +++++++++++++++++++
846
847 Features:
848
849 * Support vendor extensions on paths (:issue:`65`). Thanks :user:`lucascosta` for the PR.
850 * *Backwards-incompatible*: Remove support for old versions (<=0.15.0) of webargs.
851
852 Bug fixes:
853
854 * Fix error message when plugin does not have a ``setup()`` function.
855 * [apispec.ext.marshmallow] Fix bug in introspecting self-referencing marshmallow fields, i.e. ``fields.Nested('self')`` (:issue:`55`). Thanks :user:`whoiswes` for reporting.
856 * [apispec.ext.marshmallow] ``field2property`` no longer pops off ``location`` from a field's metadata (:issue:`67`).
857
858 Support:
859
860 * Lots of new docs, including a User Guide and improved extension docs.
861
862 0.10.1 (2016-04-09)
863 +++++++++++++++++++
864
865 Note: This version is a re-upload of 0.10.0. There is no 0.10.0 release on PyPI.
866
867 Features:
868
869 * Add Tornado extension (:issue:`62`).
870
871 Bug fixes:
872
873 * Compatibility fix with marshmallow>=2.7.0 (:issue:`64`).
874 * Fix bug that raised error for Swagger parameters that didn't include the ``in`` key (:issue:`63`).
875
876 Big thanks :user:`lucascosta` for all these changes.
877
878 0.9.1 (2016-03-17)
879 ++++++++++++++++++
880
881 Bug fixes:
882
883 * Fix generation of metadata for ``Nested`` fields (:issue:`61`). Thanks :user:`martinlatrille`.
884
885 0.9.0 (2016-03-13)
886 ++++++++++++++++++
887
888 Features:
889
890 * Add ``APISpec.add_tags`` method for adding Swagger tags. Thanks :user:`martinlatrille`.
891
892 Bug fixes:
893
894 * Fix bug in marshmallow extension where metadata was being lost when converting marshmallow ``Schemas`` when ``many=False``. Thanks again :user:`martinlatrille`.
895
896 Other changes:
897
898 * Remove duplicate ``SWAGGER_VERSION`` from ``api.ext.marshmallow.swagger``.
899
900 Support:
901
902 * Update docs to reflect rename of Swagger to OpenAPI.
903
904
905 0.8.0 (2016-03-06)
906 ++++++++++++++++++
907
908 Features:
909
910 * ``apispec.ext.marshmallow.swagger.schema2jsonschema`` properly introspects ``Schema`` instances when ``many=True`` (:issue:`53`). Thanks :user:`frol` for the PR.
911
912 Bug fixes:
913
914 * Fix error reporting when an invalid object is passed to ``schema2jsonschema`` or ``schema2parameters`` (:issue:`52`). Thanks again :user:`frol`.
915
916 0.7.0 (2016-02-11)
917 ++++++++++++++++++
918
919 Features:
920
921 * ``APISpec.add_path`` accepts ``Path`` objects (:issue:`49`). Thanks :user:`Trii` for the suggestion and the implementation.
922
923 Bug fixes:
924
925 * Use correct field name in "required" array when ``load_from`` and ``dump_to`` are used (:issue:`48`). Thanks :user:`benbeadle` for the catch and patch.
926
927 0.6.0 (2016-01-04)
928 ++++++++++++++++++
929
930 Features:
931
932 * Add ``APISpec#add_parameter`` for adding common Swagger parameter objects. Thanks :user:`jta`.
933 * The field name in a spec will be adjusted if a ``Field's`` ``load_from`` and ``dump_to`` attributes are the same. :issue:`43`. Thanks again :user:`jta`.
934
935 Bug fixes:
936
937 * Fix bug that caused a stack overflow when adding nested Schemas to an ``APISpec`` (:issue:`31`, :issue:`41`). Thanks :user:`alapshin` and :user:`itajaja` for reporting. Thanks :user:`itajaja` for the patch.
938
939 0.5.0 (2015-12-13)
940 ++++++++++++++++++
941
942 * ``schema2jsonschema`` and ``schema2parameters`` can introspect a marshmallow ``Schema`` instance as well as a ``Schema`` class (:issue:`37`). Thanks :user:`frol`.
943 * *Backwards-incompatible*: The first argument to ``schema2jsonschema`` and ``schema2parameters`` was changed from ``schema_cls`` to ``schema``.
944
945 Bug fixes:
946
947 * Handle conflicting signatures for plugin helpers. Thanks :user:`AndrewPashkin` for the catch and patch.
948
949 0.4.2 (2015-11-23)
950 ++++++++++++++++++
951
952 * Skip dump-only fields when ``dump=False`` is passed to ``schema2parameters`` and ``fields2parameters``. Thanks :user:`frol`.
953
954 Bug fixes:
955
956 * Raise ``SwaggerError`` when ``validate_swagger`` fails. Thanks :user:`frol`.
957
958 0.4.1 (2015-10-19)
959 ++++++++++++++++++
960
961 * Correctly pass ``dump`` parameter to ``field2parameters``.
962
963 0.4.0 (2015-10-18)
964 ++++++++++++++++++
965
966 * Add ``dump`` parameter to ``field2property`` (:issue:`32`).
967
968 0.3.0 (2015-10-02)
969 ++++++++++++++++++
970
971 * Rename and repackage as "apispec".
972 * Support ``enum`` field of JSON Schema based on ``OneOf`` and ``ContainsOnly`` validators.
973
974 0.2.0 (2015-09-27)
975 ++++++++++++++++++
976
977 * Add ``schema2parameters``, ``fields2parameters``, and ``field2parameters``.
978 * Removed ``Fixed`` from ``swagger.FIELD_MAPPING`` for compatibility with marshmallow>=2.0.0.
979
980 0.1.0 (2015-09-13)
981 ++++++++++++++++++
982
983 * First release.
0 Contributing Guidelines
1 =======================
2
3 Security Contact Information
4 ----------------------------
5
6 To report a security vulnerability, please use the
7 `Tidelift security contact <https://tidelift.com/security>`_.
8 Tidelift will coordinate the fix and disclosure.
9
10 Questions, Feature Requests, Bug Reports, and Feedback. . .
11 -----------------------------------------------------------
12
13 . . .should all be reported on the `GitHub Issue Tracker`_ .
14
15 .. _`GitHub Issue Tracker`: https://github.com/marshmallow-code/apispec/issues?state=open
16
17 Contributing Code
18 -----------------
19
20 Setting Up for Local Development
21 ++++++++++++++++++++++++++++++++
22
23 1. Fork apispec_ on GitHub.
24
25 ::
26
27 $ git clone https://github.com/marshmallow-code/apispec.git
28 $ cd apispec
29
30 2. Install development requirements. **It is highly recommended that you use a virtualenv.**
31 Use the following command to install an editable version of
32 apispec along with its development requirements.
33
34 ::
35
36 # After activating your virtualenv
37 $ pip install -e '.[dev]'
38
39 3. Install the pre-commit hooks, which will format and lint your git staged files.
40
41 ::
42
43 # The pre-commit CLI was installed above
44 $ pre-commit install
45
46
47 Git Branch Structure
48 ++++++++++++++++++++
49
50 apispec abides by the following branching model:
51
52
53 ``dev``
54 Current development branch. **New features should branch off here**.
55
56 ``X.Y-line``
57 Maintenance branch for release ``X.Y``. **Bug fixes should be sent to the most recent release branch.** The maintainer will forward-port the fix to ``dev``. Note: exceptions may be made for bug fixes that introduce large code changes.
58
59 **Always make a new branch for your work**, no matter how small. Also, **do not put unrelated changes in the same branch or pull request**. This makes it more difficult to merge your changes.
60
61 Pull Requests
62 ++++++++++++++
63
64 1. Create a new local branch.
65
66 ::
67
68 # For a new feature
69 $ git checkout -b name-of-feature dev
70
71 # For a bugfix
72 $ git checkout -b fix-something 1.2-line
73
74 2. Commit your changes. Write `good commit messages <http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html>`_.
75
76 ::
77
78 $ git commit -m "Detailed commit message"
79 $ git push origin name-of-feature
80
81 3. Before submitting a pull request, check the following:
82
83 - If the pull request adds functionality, it is tested and the docs are updated.
84 - You've added yourself to ``AUTHORS.rst``.
85
86 4. Submit a pull request to ``marshmallow-code:dev`` or the appropriate maintenance branch.
87 The `CI <https://dev.azure.com/sloria/sloria/_build/latest?definitionId=8&branchName=dev>`_
88 build must be passing before your pull request is merged.
89
90 Running Tests
91 +++++++++++++
92
93 To run all tests: ::
94
95 $ pytest
96
97 To run syntax checks: ::
98
99 $ tox -e lint
100
101 (Optional) To run tests in all supported Python versions in their own virtual environments (must have each interpreter installed): ::
102
103 $ tox
104
105 Documentation
106 +++++++++++++
107
108 Contributions to the documentation are welcome. Documentation is written in `reStructuredText`_ (rST). A quick rST reference can be found `here <https://docutils.sourceforge.io/docs/user/rst/quickref.html>`_. Builds are powered by Sphinx_.
109
110 To build the docs in "watch" mode: ::
111
112 $ tox -e watch-docs
113
114 Changes in the `docs/` directory will automatically trigger a rebuild.
115
116 .. _Sphinx: http://sphinx.pocoo.org/
117 .. _`reStructuredText`: https://docutils.sourceforge.io/rst.html
118 .. _`apispec`: https://github.com/marshmallow-code/apispec
0 Copyright 2015-2020 Steven Loria, Jérôme Lafréchoux, and contributors
1
2 Permission is hereby granted, free of charge, to any person obtaining a copy
3 of this software and associated documentation files (the "Software"), to deal
4 in the Software without restriction, including without limitation the rights
5 to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
6 copies of the Software, and to permit persons to whom the Software is
7 furnished to do so, subject to the following conditions:
8
9 The above copyright notice and this permission notice shall be included in
10 all copies or substantial portions of the Software.
11
12 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
13 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
14 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
15 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
16 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
17 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
18 THE SOFTWARE.
0 include LICENSE
1 include *.rst
2 recursive-include tests *
3 recursive-include docs *
4 recursive-exclude docs *.pyc
5 recursive-exclude docs *.pyo
6 recursive-exclude tests *.pyc
7 recursive-exclude tests *.pyo
8 prune docs/_build
0 *******
1 apispec
2 *******
3
4 .. image:: https://badgen.net/pypi/v/apispec
5 :target: https://pypi.org/project/apispec/
6 :alt: PyPI version
7
8 .. image:: https://dev.azure.com/sloria/sloria/_apis/build/status/marshmallow-code.apispec?branchName=dev
9 :target: https://dev.azure.com/sloria/sloria/_build/latest?definitionId=8&branchName=dev
10 :alt: Build status
11
12 .. image:: https://readthedocs.org/projects/apispec/badge/
13 :target: https://apispec.readthedocs.io/
14 :alt: Documentation
15
16 .. image:: https://badgen.net/badge/marshmallow/2,3?list=1
17 :target: https://marshmallow.readthedocs.io/en/latest/upgrading.html
18 :alt: marshmallow 2/3 compatible
19
20 .. image:: https://badgen.net/badge/OAS/2,3?list=1&color=cyan
21 :target: https://github.com/OAI/OpenAPI-Specification
22 :alt: OpenAPI Specification 2/3 compatible
23
24 .. image:: https://badgen.net/badge/code%20style/black/000
25 :target: https://github.com/ambv/black
26 :alt: code style: black
27
28 A pluggable API specification generator. Currently supports the `OpenAPI Specification <https://github.com/OAI/OpenAPI-Specification>`_ (f.k.a. the Swagger specification).
29
30 Features
31 ========
32
33 - Supports the OpenAPI Specification (versions 2 and 3)
34 - Framework-agnostic
35 - Built-in support for `marshmallow <https://marshmallow.readthedocs.io/>`_
36 - Utilities for parsing docstrings
37
38 Example Application
39 ===================
40
41 .. code-block:: python
42
43 from apispec import APISpec
44 from apispec.ext.marshmallow import MarshmallowPlugin
45 from apispec_webframeworks.flask import FlaskPlugin
46 from flask import Flask
47 from marshmallow import Schema, fields
48
49
50 # Create an APISpec
51 spec = APISpec(
52 title="Swagger Petstore",
53 version="1.0.0",
54 openapi_version="3.0.2",
55 plugins=[FlaskPlugin(), MarshmallowPlugin()],
56 )
57
58 # Optional marshmallow support
59 class CategorySchema(Schema):
60 id = fields.Int()
61 name = fields.Str(required=True)
62
63
64 class PetSchema(Schema):
65 category = fields.List(fields.Nested(CategorySchema))
66 name = fields.Str()
67
68
69 # Optional Flask support
70 app = Flask(__name__)
71
72
73 @app.route("/random")
74 def random_pet():
75 """A cute furry animal endpoint.
76 ---
77 get:
78 description: Get a random pet
79 responses:
80 200:
81 content:
82 application/json:
83 schema: PetSchema
84 """
85 pet = get_random_pet()
86 return PetSchema().dump(pet)
87
88
89 # Register the path and the entities within it
90 with app.test_request_context():
91 spec.path(view=random_pet)
92
93
94 Generated OpenAPI Spec
95 ----------------------
96
97 .. code-block:: python
98
99 import json
100
101 print(json.dumps(spec.to_dict(), indent=2))
102 # {
103 # "paths": {
104 # "/random": {
105 # "get": {
106 # "description": "Get a random pet",
107 # "responses": {
108 # "200": {
109 # "content": {
110 # "application/json": {
111 # "schema": {
112 # "$ref": "#/components/schemas/Pet"
113 # }
114 # }
115 # }
116 # }
117 # }
118 # }
119 # }
120 # },
121 # "tags": [],
122 # "info": {
123 # "title": "Swagger Petstore",
124 # "version": "1.0.0"
125 # },
126 # "openapi": "3.0.2",
127 # "components": {
128 # "parameters": {},
129 # "responses": {},
130 # "schemas": {
131 # "Category": {
132 # "type": "object",
133 # "properties": {
134 # "name": {
135 # "type": "string"
136 # },
137 # "id": {
138 # "type": "integer",
139 # "format": "int32"
140 # }
141 # },
142 # "required": [
143 # "name"
144 # ]
145 # },
146 # "Pet": {
147 # "type": "object",
148 # "properties": {
149 # "name": {
150 # "type": "string"
151 # },
152 # "category": {
153 # "type": "array",
154 # "items": {
155 # "$ref": "#/components/schemas/Category"
156 # }
157 # }
158 # }
159 # }
160 # }
161 # }
162 # }
163
164 print(spec.to_yaml())
165 # components:
166 # parameters: {}
167 # responses: {}
168 # schemas:
169 # Category:
170 # properties:
171 # id: {format: int32, type: integer}
172 # name: {type: string}
173 # required: [name]
174 # type: object
175 # Pet:
176 # properties:
177 # category:
178 # items: {$ref: '#/components/schemas/Category'}
179 # type: array
180 # name: {type: string}
181 # type: object
182 # info: {title: Swagger Petstore, version: 1.0.0}
183 # openapi: 3.0.2
184 # paths:
185 # /random:
186 # get:
187 # description: Get a random pet
188 # responses:
189 # 200:
190 # content:
191 # application/json:
192 # schema: {$ref: '#/components/schemas/Pet'}
193 # tags: []
194
195
196 Documentation
197 =============
198
199 Documentation is available at https://apispec.readthedocs.io/ .
200
201 Ecosystem
202 =========
203
204 A list of apispec-related libraries can be found at the GitHub wiki here:
205
206 https://github.com/marshmallow-code/apispec/wiki/Ecosystem
207
208 Support apispec
209 ===============
210
211 apispec is maintained by a group of
212 `volunteers <https://apispec.readthedocs.io/en/latest/authors.html>`_.
213 If you'd like to support the future of the project, please consider
214 contributing to our Open Collective:
215
216 .. image:: https://opencollective.com/marshmallow/donate/button.png
217 :target: https://opencollective.com/marshmallow
218 :width: 200
219 :alt: Donate to our collective
220
221 Professional Support
222 ====================
223
224 Professionally-supported apispec is available through the
225 `Tidelift Subscription <https://tidelift.com/subscription/pkg/pypi-apispec?utm_source=pypi-apispec&utm_medium=referral&utm_campaign=readme>`_.
226
227 Tidelift gives software development teams a single source for purchasing and maintaining their software,
228 with professional-grade assurances from the experts who know it best,
229 while seamlessly integrating with existing tools. [`Get professional support`_]
230
231 .. _`Get professional support`: https://tidelift.com/subscription/pkg/pypi-apispec?utm_source=pypi-apispec&utm_medium=referral&utm_campaign=readme
232
233 .. image:: https://user-images.githubusercontent.com/2379650/45126032-50b69880-b13f-11e8-9c2c-abd16c433495.png
234 :target: https://tidelift.com/subscription/pkg/pypi-apispec?utm_source=pypi-apispec&utm_medium=referral&utm_campaign=readme
235 :alt: Get supported apispec with Tidelift
236
237 Security Contact Information
238 ============================
239
240 To report a security vulnerability, please use the
241 `Tidelift security contact <https://tidelift.com/security>`_.
242 Tidelift will coordinate the fix and disclosure.
243
244 Project Links
245 =============
246
247 - Docs: https://apispec.readthedocs.io/
248 - Changelog: https://apispec.readthedocs.io/en/latest/changelog.html
249 - Contributing Guidelines: https://apispec.readthedocs.io/en/latest/contributing.html
250 - PyPI: https://pypi.python.org/pypi/apispec
251 - Issues: https://github.com/marshmallow-code/apispec/issues
252
253
254 License
255 =======
256
257 MIT licensed. See the bundled `LICENSE <https://github.com/marshmallow-code/apispec/blob/dev/LICENSE>`_ file for more details.
0 trigger:
1 branches:
2 include: [dev, test-me-*]
3 tags:
4 include: ['*']
5
6 # Run builds nightly to catch incompatibilities with new marshmallow releases
7 schedules:
8 - cron: "0 0 * * *"
9 displayName: Daily midnight build
10 branches:
11 include:
12 - dev
13 always: "true"
14
15 resources:
16 repositories:
17 - repository: sloria
18 type: github
19 endpoint: github
20 name: sloria/azure-pipeline-templates
21 ref: refs/heads/sloria
22
23 jobs:
24 - template: job--python-tox.yml@sloria
25 parameters:
26 toxenvs:
27 - lint
28
29 - py35-marshmallow2
30 - py35-marshmallow3
31
32 - py36-marshmallow3
33
34 - py37-marshmallow3
35
36 - py38-marshmallow2
37 - py38-marshmallow3
38
39 - py38-marshmallowdev
40
41 - docs
42 os: linux
43 - template: job--pypi-release.yml@sloria
44 parameters:
45 python: "3.8"
46 distributions: "sdist bdist_wheel"
47 dependsOn:
48 - tox_linux
0 # Makefile for Sphinx documentation
1 #
2
3 # You can set these variables from the command line.
4 SPHINXOPTS =
5 SPHINXBUILD = sphinx-build
6 PAPER =
7 BUILDDIR = _build
8
9 # User-friendly check for sphinx-build
10 ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1)
11 $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/)
12 endif
13
14 # Internal variables.
15 PAPEROPT_a4 = -D latex_paper_size=a4
16 PAPEROPT_letter = -D latex_paper_size=letter
17 ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
18 # the i18n builder cannot share the environment and doctrees with the others
19 I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
20
21 .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext
22
23 help:
24 @echo "Please use \`make <target>' where <target> is one of"
25 @echo " html to make standalone HTML files"
26 @echo " dirhtml to make HTML files named index.html in directories"
27 @echo " singlehtml to make a single large HTML file"
28 @echo " pickle to make pickle files"
29 @echo " json to make JSON files"
30 @echo " htmlhelp to make HTML files and a HTML help project"
31 @echo " qthelp to make HTML files and a qthelp project"
32 @echo " devhelp to make HTML files and a Devhelp project"
33 @echo " epub to make an epub"
34 @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
35 @echo " latexpdf to make LaTeX files and run them through pdflatex"
36 @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx"
37 @echo " text to make text files"
38 @echo " man to make manual pages"
39 @echo " texinfo to make Texinfo files"
40 @echo " info to make Texinfo files and run them through makeinfo"
41 @echo " gettext to make PO message catalogs"
42 @echo " changes to make an overview of all changed/added/deprecated items"
43 @echo " xml to make Docutils-native XML files"
44 @echo " pseudoxml to make pseudoxml-XML files for display purposes"
45 @echo " linkcheck to check all external links for integrity"
46 @echo " doctest to run all doctests embedded in the documentation (if enabled)"
47
48 clean:
49 rm -rf $(BUILDDIR)/*
50
51 html:
52 $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
53 @echo
54 @echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
55
56 dirhtml:
57 $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
58 @echo
59 @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
60
61 singlehtml:
62 $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
63 @echo
64 @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
65
66 pickle:
67 $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
68 @echo
69 @echo "Build finished; now you can process the pickle files."
70
71 json:
72 $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
73 @echo
74 @echo "Build finished; now you can process the JSON files."
75
76 htmlhelp:
77 $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
78 @echo
79 @echo "Build finished; now you can run HTML Help Workshop with the" \
80 ".hhp project file in $(BUILDDIR)/htmlhelp."
81
82 qthelp:
83 $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
84 @echo
85 @echo "Build finished; now you can run "qcollectiongenerator" with the" \
86 ".qhcp project file in $(BUILDDIR)/qthelp, like this:"
87 @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/complexity.qhcp"
88 @echo "To view the help file:"
89 @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/complexity.qhc"
90
91 devhelp:
92 $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
93 @echo
94 @echo "Build finished."
95 @echo "To view the help file:"
96 @echo "# mkdir -p $$HOME/.local/share/devhelp/complexity"
97 @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/complexity"
98 @echo "# devhelp"
99
100 epub:
101 $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
102 @echo
103 @echo "Build finished. The epub file is in $(BUILDDIR)/epub."
104
105 latex:
106 $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
107 @echo
108 @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
109 @echo "Run \`make' in that directory to run these through (pdf)latex" \
110 "(use \`make latexpdf' here to do that automatically)."
111
112 latexpdf:
113 $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
114 @echo "Running LaTeX files through pdflatex..."
115 $(MAKE) -C $(BUILDDIR)/latex all-pdf
116 @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
117
118 latexpdfja:
119 $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
120 @echo "Running LaTeX files through platex and dvipdfmx..."
121 $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja
122 @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
123
124 text:
125 $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
126 @echo
127 @echo "Build finished. The text files are in $(BUILDDIR)/text."
128
129 man:
130 $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
131 @echo
132 @echo "Build finished. The manual pages are in $(BUILDDIR)/man."
133
134 texinfo:
135 $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
136 @echo
137 @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
138 @echo "Run \`make' in that directory to run these through makeinfo" \
139 "(use \`make info' here to do that automatically)."
140
141 info:
142 $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
143 @echo "Running Texinfo files through makeinfo..."
144 make -C $(BUILDDIR)/texinfo info
145 @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
146
147 gettext:
148 $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
149 @echo
150 @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
151
152 changes:
153 $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
154 @echo
155 @echo "The overview file is in $(BUILDDIR)/changes."
156
157 linkcheck:
158 $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
159 @echo
160 @echo "Link check complete; look for any errors in the above output " \
161 "or in $(BUILDDIR)/linkcheck/output.txt."
162
163 doctest:
164 $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
165 @echo "Testing of doctests in the sources finished, look at the " \
166 "results in $(BUILDDIR)/doctest/output.txt."
167
168 xml:
169 $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml
170 @echo
171 @echo "Build finished. The XML files are in $(BUILDDIR)/xml."
172
173 pseudoxml:
174 $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml
175 @echo
176 @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml."
0 Core API
1 ========
2
3 apispec
4 -------
5
6 .. automodule:: apispec
7 :members:
8
9
10 apispec.core
11 ------------
12
13 .. automodule:: apispec.core
14 :members: Components
15
16 apispec.exceptions
17 ------------------
18
19 .. automodule:: apispec.exceptions
20 :members:
21
22 apispec.utils
23 -------------
24
25 .. automodule:: apispec.utils
26 :members:
0 Built-in Plugins
1 ================
2
3 apispec.ext.marshmallow
4 -----------------------
5
6 .. automodule:: apispec.ext.marshmallow
7 :members:
8
9 apispec.ext.marshmallow.schema_resolver
10 +++++++++++++++++++++++++++++++++++++++
11
12 .. automodule:: apispec.ext.marshmallow.schema_resolver
13 :members:
14
15 apispec.ext.marshmallow.openapi
16 +++++++++++++++++++++++++++++++
17
18 .. automodule:: apispec.ext.marshmallow.openapi
19 :members:
20
21 apispec.ext.marshmallow.field_converter
22 +++++++++++++++++++++++++++++++++++++++
23
24 .. automodule:: apispec.ext.marshmallow.field_converter
25 :members:
26
27 apispec.ext.marshmallow.common
28 ++++++++++++++++++++++++++++++
29 .. automodule:: apispec.ext.marshmallow.common
30 :members:
0 .. include:: ../AUTHORS.rst
0 .. seealso::
1 Need help upgrading to a newer version? Check out the :doc:`upgrading guide <upgrading>`.
2
3 .. include:: ../CHANGELOG.rst
0 import datetime as dt
1 import os
2 import sys
3
4 sys.path.insert(0, os.path.abspath(os.path.join("..", "src")))
5 import apispec # noqa: E402
6
7 extensions = [
8 "sphinx.ext.autodoc",
9 "sphinx.ext.intersphinx",
10 "sphinx.ext.viewcode",
11 "sphinx.ext.todo",
12 "sphinx_issues",
13 ]
14
15 primary_domain = "py"
16 default_role = "py:obj"
17
18 intersphinx_mapping = {
19 "python": ("http://python.readthedocs.io/en/latest/", None),
20 "marshmallow": ("http://marshmallow.readthedocs.io/en/latest/", None),
21 }
22
23 issues_github_path = "marshmallow-code/apispec"
24
25 source_suffix = ".rst"
26 master_doc = "index"
27 project = "apispec"
28 copyright = "Steven Loria {:%Y}".format(dt.datetime.utcnow())
29
30 version = release = apispec.__version__
31
32 exclude_patterns = ["_build"]
33
34 # THEME
35
36 # on_rtd is whether we are on readthedocs.org
37 on_rtd = os.environ.get("READTHEDOCS", None) == "True"
38
39 if not on_rtd: # only import and set the theme if we're building docs locally
40 import sphinx_rtd_theme
41
42 html_theme = "sphinx_rtd_theme"
43 html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]
0 .. include:: ../CONTRIBUTING.rst
0 Ecosystem
1 =========
2
3 A list of apispec-related projects can be found at the GitHub wiki here:
4
5 https://github.com/marshmallow-code/apispec/wiki/Ecosystem
0 *******
1 apispec
2 *******
3
4 Release v\ |version| (:doc:`Changelog <changelog>`)
5
6 A pluggable API specification generator. Currently supports the `OpenAPI Specification <https://github.com/OAI/OpenAPI-Specification>`_ (f.k.a. the Swagger specification).
7
8 Features
9 ========
10
11 - Supports the OpenAPI Specification (versions 2 and 3)
12 - Framework-agnostic
13 - Built-in support for `marshmallow <https://marshmallow.readthedocs.io/>`_
14 - Utilities for parsing docstrings
15
16 Example Application
17 ===================
18
19 .. code-block:: python
20
21 import uuid
22
23 from apispec import APISpec
24 from apispec.ext.marshmallow import MarshmallowPlugin
25 from apispec_webframeworks.flask import FlaskPlugin
26 from flask import Flask
27 from marshmallow import Schema, fields
28
29
30 # Create an APISpec
31 spec = APISpec(
32 title="Swagger Petstore",
33 version="1.0.0",
34 openapi_version="3.0.2",
35 plugins=[FlaskPlugin(), MarshmallowPlugin()],
36 )
37
38 # Optional marshmallow support
39 class CategorySchema(Schema):
40 id = fields.Int()
41 name = fields.Str(required=True)
42
43
44 class PetSchema(Schema):
45 categories = fields.List(fields.Nested(CategorySchema))
46 name = fields.Str()
47
48
49 # Optional Flask support
50 app = Flask(__name__)
51
52
53 @app.route("/random")
54 def random_pet():
55 """A cute furry animal endpoint.
56 ---
57 get:
58 description: Get a random pet
59 responses:
60 200:
61 description: Return a pet
62 content:
63 application/json:
64 schema: PetSchema
65 """
66 # Hardcoded example data
67 pet_data = {
68 "name": "sample_pet_" + str(uuid.uuid1()),
69 "categories": [{"id": 1, "name": "sample_category"}],
70 }
71 return PetSchema().dump(pet_data)
72
73
74 # Register the path and the entities within it
75 with app.test_request_context():
76 spec.path(view=random_pet)
77
78
79 Generated OpenAPI Spec
80 ----------------------
81
82 .. code-block:: python
83
84 import json
85
86 print(json.dumps(spec.to_dict(), indent=2))
87 # {
88 # "info": {
89 # "title": "Swagger Petstore",
90 # "version": "1.0.0"
91 # },
92 # "openapi": "3.0.2",
93 # "components": {
94 # "schemas": {
95 # "Category": {
96 # "type": "object",
97 # "properties": {
98 # "id": {
99 # "type": "integer",
100 # "format": "int32"
101 # },
102 # "name": {
103 # "type": "string"
104 # }
105 # },
106 # "required": [
107 # "name"
108 # ]
109 # },
110 # "Pet": {
111 # "type": "object",
112 # "properties": {
113 # "categories": {
114 # "type": "array",
115 # "items": {
116 # "$ref": "#/components/schemas/Category"
117 # }
118 # },
119 # "name": {
120 # "type": "string"
121 # }
122 # }
123 # }
124 # }
125 # },
126 # "paths": {
127 # "/random": {
128 # "get": {
129 # "description": "Get a random pet",
130 # "responses": {
131 # "200": {
132 # "description": "Return a pet",
133 # "content": {
134 # "application/json": {
135 # "schema": {
136 # "$ref": "#/components/schemas/Pet"
137 # }
138 # }
139 # }
140 # }
141 # }
142 # }
143 # }
144 # },
145 # }
146
147 print(spec.to_yaml())
148 # info:
149 # title: Swagger Petstore
150 # version: 1.0.0
151 # openapi: 3.0.2
152 # components:
153 # schemas:
154 # Category:
155 # properties:
156 # id:
157 # format: int32
158 # type: integer
159 # name:
160 # type: string
161 # required:
162 # - name
163 # type: object
164 # Pet:
165 # properties:
166 # categories:
167 # items:
168 # $ref: '#/components/schemas/Category'
169 # type: array
170 # name:
171 # type: string
172 # type: object
173 # paths:
174 # /random:
175 # get:
176 # description: Get a random pet
177 # responses:
178 # '200':
179 # content:
180 # application/json:
181 # schema:
182 # $ref: '#/components/schemas/Pet'
183 # description: Return a pet
184
185 User Guide
186 ==========
187
188 .. toctree::
189 :maxdepth: 2
190
191 install
192 quickstart
193 using_plugins
194 writing_plugins
195 special_topics
196
197 API Reference
198 =============
199
200 .. toctree::
201 :maxdepth: 2
202
203 api_core
204 api_ext
205
206 Project Links
207 =============
208
209 - `apispec @ GitHub <https://github.com/marshmallow-code/apispec>`_
210 - `Issue Tracker <https://github.com/marshmallow-code/apispec/issues>`_
211
212 Project Info
213 ============
214
215 .. toctree::
216 :maxdepth: 1
217
218 changelog
219 upgrading
220 ecosystem
221 authors
222 contributing
223 license
0 Install
1 =======
2
3 **apispec** requires Python >= 3.5.
4
5 From the PyPI
6 -------------
7
8 To install the latest version from the PyPI:
9
10 ::
11
12 pip install -U apispec
13
14
15 To install with validation support:
16
17
18 ::
19
20 pip install -U 'apispec[validation]'
21
22 To install with YAML support:
23
24 ::
25
26 pip install -U 'apispec[yaml]'
27
28
29 Get the Bleeding Edge Version
30 -----------------------------
31
32 To install the latest development version:
33
34 ::
35
36 pip install -U git+https://github.com/marshmallow-code/apispec@dev
0 *******
1 License
2 *******
3
4 .. literalinclude:: ../LICENSE
0 @ECHO OFF
1
2 REM Command file for Sphinx documentation
3
4 if "%SPHINXBUILD%" == "" (
5 set SPHINXBUILD=sphinx-build
6 )
7 set BUILDDIR=_build
8 set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% .
9 set I18NSPHINXOPTS=%SPHINXOPTS% .
10 if NOT "%PAPER%" == "" (
11 set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS%
12 set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS%
13 )
14
15 if "%1" == "" goto help
16
17 if "%1" == "help" (
18 :help
19 echo.Please use `make ^<target^>` where ^<target^> is one of
20 echo. html to make standalone HTML files
21 echo. dirhtml to make HTML files named index.html in directories
22 echo. singlehtml to make a single large HTML file
23 echo. pickle to make pickle files
24 echo. json to make JSON files
25 echo. htmlhelp to make HTML files and a HTML help project
26 echo. qthelp to make HTML files and a qthelp project
27 echo. devhelp to make HTML files and a Devhelp project
28 echo. epub to make an epub
29 echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter
30 echo. text to make text files
31 echo. man to make manual pages
32 echo. texinfo to make Texinfo files
33 echo. gettext to make PO message catalogs
34 echo. changes to make an overview over all changed/added/deprecated items
35 echo. xml to make Docutils-native XML files
36 echo. pseudoxml to make pseudoxml-XML files for display purposes
37 echo. linkcheck to check all external links for integrity
38 echo. doctest to run all doctests embedded in the documentation if enabled
39 goto end
40 )
41
42 if "%1" == "clean" (
43 for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i
44 del /q /s %BUILDDIR%\*
45 goto end
46 )
47
48
49 %SPHINXBUILD% 2> nul
50 if errorlevel 9009 (
51 echo.
52 echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
53 echo.installed, then set the SPHINXBUILD environment variable to point
54 echo.to the full path of the 'sphinx-build' executable. Alternatively you
55 echo.may add the Sphinx directory to PATH.
56 echo.
57 echo.If you don't have Sphinx installed, grab it from
58 echo.http://sphinx-doc.org/
59 exit /b 1
60 )
61
62 if "%1" == "html" (
63 %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html
64 if errorlevel 1 exit /b 1
65 echo.
66 echo.Build finished. The HTML pages are in %BUILDDIR%/html.
67 goto end
68 )
69
70 if "%1" == "dirhtml" (
71 %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml
72 if errorlevel 1 exit /b 1
73 echo.
74 echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml.
75 goto end
76 )
77
78 if "%1" == "singlehtml" (
79 %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml
80 if errorlevel 1 exit /b 1
81 echo.
82 echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml.
83 goto end
84 )
85
86 if "%1" == "pickle" (
87 %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle
88 if errorlevel 1 exit /b 1
89 echo.
90 echo.Build finished; now you can process the pickle files.
91 goto end
92 )
93
94 if "%1" == "json" (
95 %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json
96 if errorlevel 1 exit /b 1
97 echo.
98 echo.Build finished; now you can process the JSON files.
99 goto end
100 )
101
102 if "%1" == "htmlhelp" (
103 %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp
104 if errorlevel 1 exit /b 1
105 echo.
106 echo.Build finished; now you can run HTML Help Workshop with the ^
107 .hhp project file in %BUILDDIR%/htmlhelp.
108 goto end
109 )
110
111 if "%1" == "qthelp" (
112 %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp
113 if errorlevel 1 exit /b 1
114 echo.
115 echo.Build finished; now you can run "qcollectiongenerator" with the ^
116 .qhcp project file in %BUILDDIR%/qthelp, like this:
117 echo.^> qcollectiongenerator %BUILDDIR%\qthelp\complexity.qhcp
118 echo.To view the help file:
119 echo.^> assistant -collectionFile %BUILDDIR%\qthelp\complexity.ghc
120 goto end
121 )
122
123 if "%1" == "devhelp" (
124 %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp
125 if errorlevel 1 exit /b 1
126 echo.
127 echo.Build finished.
128 goto end
129 )
130
131 if "%1" == "epub" (
132 %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub
133 if errorlevel 1 exit /b 1
134 echo.
135 echo.Build finished. The epub file is in %BUILDDIR%/epub.
136 goto end
137 )
138
139 if "%1" == "latex" (
140 %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
141 if errorlevel 1 exit /b 1
142 echo.
143 echo.Build finished; the LaTeX files are in %BUILDDIR%/latex.
144 goto end
145 )
146
147 if "%1" == "latexpdf" (
148 %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
149 cd %BUILDDIR%/latex
150 make all-pdf
151 cd %BUILDDIR%/..
152 echo.
153 echo.Build finished; the PDF files are in %BUILDDIR%/latex.
154 goto end
155 )
156
157 if "%1" == "latexpdfja" (
158 %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
159 cd %BUILDDIR%/latex
160 make all-pdf-ja
161 cd %BUILDDIR%/..
162 echo.
163 echo.Build finished; the PDF files are in %BUILDDIR%/latex.
164 goto end
165 )
166
167 if "%1" == "text" (
168 %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text
169 if errorlevel 1 exit /b 1
170 echo.
171 echo.Build finished. The text files are in %BUILDDIR%/text.
172 goto end
173 )
174
175 if "%1" == "man" (
176 %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man
177 if errorlevel 1 exit /b 1
178 echo.
179 echo.Build finished. The manual pages are in %BUILDDIR%/man.
180 goto end
181 )
182
183 if "%1" == "texinfo" (
184 %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo
185 if errorlevel 1 exit /b 1
186 echo.
187 echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo.
188 goto end
189 )
190
191 if "%1" == "gettext" (
192 %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale
193 if errorlevel 1 exit /b 1
194 echo.
195 echo.Build finished. The message catalogs are in %BUILDDIR%/locale.
196 goto end
197 )
198
199 if "%1" == "changes" (
200 %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes
201 if errorlevel 1 exit /b 1
202 echo.
203 echo.The overview file is in %BUILDDIR%/changes.
204 goto end
205 )
206
207 if "%1" == "linkcheck" (
208 %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck
209 if errorlevel 1 exit /b 1
210 echo.
211 echo.Link check complete; look for any errors in the above output ^
212 or in %BUILDDIR%/linkcheck/output.txt.
213 goto end
214 )
215
216 if "%1" == "doctest" (
217 %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest
218 if errorlevel 1 exit /b 1
219 echo.
220 echo.Testing of doctests in the sources finished, look at the ^
221 results in %BUILDDIR%/doctest/output.txt.
222 goto end
223 )
224
225 if "%1" == "xml" (
226 %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml
227 if errorlevel 1 exit /b 1
228 echo.
229 echo.Build finished. The XML files are in %BUILDDIR%/xml.
230 goto end
231 )
232
233 if "%1" == "pseudoxml" (
234 %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml
235 if errorlevel 1 exit /b 1
236 echo.
237 echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml.
238 goto end
239 )
240
241 :end
0 Quickstart
1 ==========
2
3 Basic Usage
4 -----------
5
6 First, create an `APISpec <apispec.APISpec>` object, passing basic information about your API.
7
8 .. code-block:: python
9
10 from apispec import APISpec
11
12 spec = APISpec(
13 title="Gisty",
14 version="1.0.0",
15 openapi_version="3.0.2",
16 info=dict(description="A minimal gist API"),
17 )
18
19 Add schemas to your spec using `spec.components.schema <apispec.core.Components.schema>`.
20
21 .. code-block:: python
22
23 spec.components.schema(
24 "Gist",
25 {
26 "properties": {
27 "id": {"type": "integer", "format": "int64"},
28 "name": {"type": "string"},
29 }
30 },
31 )
32
33
34 Add paths to your spec using `path <apispec.APISpec.path>`.
35
36 .. code-block:: python
37
38
39 spec.path(
40 path="/gist/{gist_id}",
41 operations=dict(
42 get=dict(
43 responses={"200": {"content": {"application/json": {"schema": "Gist"}}}}
44 )
45 ),
46 )
47
48
49 The API is chainable, allowing you to combine multiple method calls in
50 one statement:
51
52 .. code-block:: python
53
54 spec.path(...).path(...).tag(...)
55
56 spec.components.schema(...).parameter(...)
57
58 To output your OpenAPI spec, invoke the `to_dict <apispec.APISpec.to_dict>` method.
59
60 .. code-block:: python
61
62 from pprint import pprint
63
64 pprint(spec.to_dict())
65 # {'components': {'parameters': {},
66 # 'responses': {},
67 # 'schemas': {'Gist': {'properties': {'id': {'format': 'int64',
68 # 'type': 'integer'},
69 # 'name': {'type': 'string'}}}}},
70 # 'info': {'description': 'A minimal gist API',
71 # 'title': 'Gisty',
72 # 'version': '1.0.0'},
73 # 'openapi': '3.0.2',
74 # 'paths': OrderedDict([('/gist/{gist_id}',
75 # {'get': {'responses': {'200': {'content': {'application/json': {'schema': {'$ref': '#/definitions/Gist'}}}}}}})]),
76 # 'tags': []}
77
78 Use `to_yaml <apispec.APISpec.to_yaml>` to export your spec to YAML.
79
80 .. code-block:: python
81
82 print(spec.to_yaml())
83 # components:
84 # parameters: {}
85 # responses: {}
86 # schemas:
87 # Gist:
88 # properties:
89 # id: {format: int64, type: integer}
90 # name: {type: string}
91 # info: {description: A minimal gist API, title: Gisty, version: 1.0.0}
92 # openapi: 3.0.2
93 # paths:
94 # /gist/{gist_id}:
95 # get:
96 # responses:
97 # '200':
98 # content:
99 # application/json:
100 # schema: {$ref: '#/definitions/Gist'}
101 # tags: []
102
103 .. seealso::
104 For a full reference of the `APISpec <apispec.APISpec>` class, see the :doc:`Core API Reference <api_core>`.
105
106
107 Next Steps
108 ----------
109
110 We've learned how to programmatically construct an OpenAPI spec, but defining our entities was verbose.
111
112 In the next section, we'll learn how to let plugins do the dirty work: :doc:`Using Plugins <using_plugins>`.
0 Special Topics
1 ==============
2
3 Solutions to specific problems are documented here.
4
5
6 Adding Additional Fields To Schema Objects
7 ------------------------------------------
8
9 To add additional fields (e.g. ``"discriminator"``) to Schema objects generated from `spec.components.schema <apispec.core.Components.schema>` , pass them
10 to the ``component`` parameter. If your'e using ``MarshmallowPlugin``, the ``component`` properties will get merged with the autogenerated properties.
11
12 .. code-block:: python
13
14 properties = {
15 "id": {"type": "integer", "format": "int64"},
16 "name": {"type": "string", "example": "doggie"},
17 }
18
19 spec.components.schema("Pet", component={"discriminator": "petType"}, schema=PetSchema)
20
21
22 .. note::
23 Be careful about the input that you pass to ``component``. ``apispec`` will not guarantee that the passed fields are valid against the OpenAPI spec.
24
25 Rendering to YAML or JSON
26 -------------------------
27
28 YAML
29 ++++
30
31 .. code-block:: python
32
33 spec.to_yaml()
34
35
36 .. note::
37 `to_yaml <apispec.APISpec.to_yaml>` requires `PyYAML` to be installed. You can install
38 apispec with YAML support using: ::
39
40 pip install 'apispec[yaml]'
41
42
43 JSON
44 ++++
45
46 .. code-block:: python
47
48 import json
49
50 json.dumps(spec.to_dict())
51
52 Documenting Top-level Components
53 --------------------------------
54
55 The ``APISpec`` object contains helpers to add top-level components.
56
57 To add a schema (f.k.a. "definition" in OAS v2), use
58 `spec.components.schema <apispec.core.Components.schema>`.
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>`.
63
64 To add other top-level objects, pass them to the ``APISpec`` as keyword arguments.
65
66 Here is an example that includes a `Server Object <https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#serverObject>`_.
67
68 .. code-block:: python
69
70 import yaml
71 from apispec import APISpec
72 from apispec.ext.marshmallow import MarshmallowPlugin
73 from apispec.utils import validate_spec
74
75 OPENAPI_SPEC = """
76 openapi: 3.0.2
77 info:
78 description: Server API document
79 title: Server API
80 version: 1.0.0
81 servers:
82 - url: http://localhost:{port}/
83 description: The development API server
84 variables:
85 port:
86 enum:
87 - '3000'
88 - '8888'
89 default: '3000'
90 """
91
92 settings = yaml.safe_load(OPENAPI_SPEC)
93 # retrieve title, version, and openapi version
94 title = settings["info"].pop("title")
95 spec_version = settings["info"].pop("version")
96 openapi_version = settings.pop("openapi")
97
98 spec = APISpec(
99 title=title,
100 version=spec_version,
101 openapi_version=openapi_version,
102 plugins=(MarshmallowPlugin(),),
103 **settings
104 )
105
106 validate_spec(spec)
107
108
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 Documenting Security Schemes
115 ----------------------------
116
117 Use `spec.components.security_scheme <apispec.core.Components.security_scheme>`
118 to document `Security Scheme Objects <https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#securitySchemeObject>`_.
119
120 .. code-block:: python
121
122 from pprint import pprint
123 from apispec import APISpec
124
125 spec = APISpec(title="Swagger Petstore", version="1.0.0", openapi_version="3.0.2")
126
127 api_key_scheme = {"type": "apiKey", "in": "header", "name": "X-API-Key"}
128 jwt_scheme = {"type": "http", "scheme": "bearer", "bearerFormat": "JWT"}
129
130 spec.components.security_scheme("api_key", api_key_scheme)
131 spec.components.security_scheme("jwt", jwt_scheme)
132
133 pprint(spec.to_dict()["components"]["securitySchemes"], indent=2)
134 # { 'api_key': {'in': 'header', 'name': 'X-API-Key', 'type': 'apiKey'},
135 # 'jwt': {'bearerFormat': 'JWT', 'scheme': 'bearer', 'type': 'http'}}
0 Upgrading to Newer Releases
1 ===========================
2
3 This section documents migration paths to new releases.
4
5 Upgrading to 2.0.0
6 ------------------
7
8 plugin helpers must accept extra `**kwargs`
9 *******************************************
10
11 Since custom plugins helpers may define extra kwargs and those kwargs are passed
12 to all plugin helpers by :meth:`APISpec.path <APISpec.path>`, all plugins should
13 accept unknown kwargs.
14
15 The example plugin below defines an additional `func` argument and accepts extra
16 `**kwargs`.
17
18 .. code-block:: python
19 :emphasize-lines: 2
20
21 class MyPlugin(BasePlugin):
22 def path_helper(self, path, func, **kwargs):
23 """Path helper that parses docstrings for operations. Adds a
24 ``func`` parameter to `apispec.APISpec.path`.
25 """
26 operations = load_operations_from_docstring(func.__doc__)
27 return Path(path=path, operations=operations)
28
29 Components must be referenced by ID, not full path
30 **************************************************
31
32 While apispec 1.x would let the user reference components by path or ID,
33 apispec 2.x only accepts references by ID.
34
35 .. code-block:: python
36
37 # apispec<2.0.0
38 spec.path(
39 path="/gist/{gist_id}",
40 operations=dict(
41 get=dict(
42 responses={
43 "200": {
44 "content": {
45 "application/json": {"schema": {"$ref": "#/definitions/Gist"}}
46 }
47 }
48 }
49 )
50 ),
51 )
52
53 # apispec>=2.0.0
54 spec.path(
55 path="/gist/{gist_id}",
56 operations=dict(
57 get=dict(
58 responses={"200": {"content": {"application/json": {"schema": "Gist"}}}}
59 )
60 ),
61 )
62
63 References by ID are accepted by both apispec 1.x ad 2.x and are a better
64 choice because they delegate the creation of the full component path to apispec.
65 This allows more flexibility as apispec creates the component path according to
66 the OpenAPI version.
67
68 Upgrading to 1.0.0
69 ------------------
70
71 ``openapi_version`` Is Required
72 *******************************
73
74 ``openapi_version`` no longer defaults to ``"2.0"``. It is now a
75 required argument.
76
77 .. code-block:: python
78 :emphasize-lines: 4
79
80 spec = APISpec(
81 title="Swagger Petstore",
82 version="1.0.0",
83 openapi_version="2.0", # or "3.0.2"
84 plugins=[MarshmallowPlugin()],
85 )
86
87 Web Framework Plugins Packaged Separately
88 *****************************************
89
90 ``apispec.ext.flask``, ``apispec.ext.bottle``, and
91 ``apispec.ext.tornado`` have been moved to a a separate package,
92 `apispec-webframeworks <https://github.com/marshmallow-code/apispec-webframeworks>`_.
93
94 If you use these plugins, install ``apispec-webframeworks`` with
95 ``pip``:
96
97 ::
98
99 $ pip install apispec-webframeworks
100
101 Then, update your imports:
102
103 .. code-block:: python
104
105 # apispec<1.0.0
106 from apispec.ext.flask import FlaskPlugin
107
108 # apispec>=1.0.0
109 from apispec_webframeworks.flask import FlaskPlugin
110
111
112 YAML Support Is Optional
113 ************************
114
115 YAML functionality is now optional. To install with YAML support:
116
117 ::
118
119 $ pip install 'apispec[yaml]'
120
121 You will need to do this if you use ``apispec-webframeworks`` or call
122 `APISpec.to_yaml <apispec.APISpec.to_yaml>` in your code.
123
124
125 Registering Entities
126 ********************
127
128 Methods for registering OAS entities are changed to the noun form
129 for internal consistency and for consistency with OAS v3 terminology.
130
131 .. code-block:: python
132
133 # apispec<1.0.0
134 spec.add_tag({"name": "Pet", "description": "Operations on pets"})
135 spec.add_path("/pets/", operations={...})
136 spec.definition("Pet", properties={...})
137 spec.add_parameter("PetID", "path", {...})
138
139 # apispec>=1.0.0
140 spec.tag({"name": "Pet", "description": "Operations on pets"})
141 spec.path("/pets/", operations={...})
142 spec.components.schema("Pet", {"properties": {...}})
143 spec.components.parameter("PetID", "path", {...})
144
145 Adding Additional Fields to Schemas
146 ***********************************
147
148 The ``extra_fields`` parameter to ``schema`` is removed. It is no longer
149 necessary. Pass all fields in to the component ``dict``.
150
151 .. code-block:: python
152
153 # <1.0.0
154 spec.definition("Pet", schema=PetSchema, extra_fields={"discriminator": "name"})
155
156 # >=1.0.0
157 spec.components.schema("Pet", schema=PetSchema, component={"discriminator": "name"})
158
159
160 Nested Schemas Are Referenced
161 *****************************
162
163 When using the `MarshmallowPlugin
164 <apispec.ext.marshmallow.MarshmallowPlugin>`, nested `Schema
165 <marshmallow.Schema>` classes are referenced (with ``"$ref"``) in the output spec.
166 By default, the name in the spec will be the class name with the "Schema" suffix
167 removed, e.g. ``fields.Nested(PetSchema())`` -> ``"#components/schemas/Pet"``.
168
169 The `ref` argument to `fields.Nested <marshmallow.fields.Nested>`_ is no
170 longer respected.
171
172
173 .. code-block:: python
174
175 # apispec<1.0.0
176 class PetSchema(Schema):
177 owner = fields.Nested(
178 HumanSchema,
179 # `ref` has no effect in 1.0.0. Remove.
180 ref="#components/schemas/Human",
181 )
182
183
184 # apispec>=1.0.0
185 class PetSchema(Schema):
186 owner = fields.Nested(HumanSchema)
187
188
189 .. seealso::
190
191 This behavior is customizable. See :ref:`marshmallow_nested_schemas`.
0 Using Plugins
1 =============
2
3 What is an apispec "plugin"?
4 ----------------------------
5
6 An apispec *plugin* is an object that provides helper methods for generating OpenAPI entities from objects in your application.
7
8 A plugin may modify the behavior of `APISpec <apispec.APISpec>` methods so that they can take your application's objects as input.
9
10 Enabling Plugins
11 ----------------
12
13 To enable a plugin, pass an instance to the constructor of `APISpec <apispec.APISpec>`.
14
15 .. code-block:: python
16 :emphasize-lines: 9
17
18 from apispec import APISpec
19 from apispec.ext.marshmallow import MarshmallowPlugin
20
21 spec = APISpec(
22 title="Gisty",
23 version="1.0.0",
24 openapi_version="3.0.2",
25 info=dict(description="A minimal gist API"),
26 plugins=[MarshmallowPlugin()],
27 )
28
29
30 Example: Flask and Marshmallow Plugins
31 --------------------------------------
32
33 The bundled marshmallow plugin (`apispec.ext.marshmallow.MarshmallowPlugin`)
34 provides helpers for generating OpenAPI schema and parameter objects from `marshmallow <https://marshmallow.readthedocs.io/en/latest/>`_ schemas and fields.
35
36 The `apispec-webframeworks <https://github.com/marshmallow-code/apispec-webframeworks>`_
37 package includes a Flask plugin with helpers for generating path objects from view functions.
38
39 Let's recreate the spec from the :doc:`Quickstart guide <quickstart>` using these two plugins.
40
41 First, ensure that ``apispec-webframeworks`` is installed: ::
42
43 $ pip install apispec-webframeworks
44
45 We can now use the marshmallow and Flask plugins.
46
47 .. code-block:: python
48
49 from apispec import APISpec
50 from apispec.ext.marshmallow import MarshmallowPlugin
51 from apispec_webframeworks.flask import FlaskPlugin
52
53 spec = APISpec(
54 title="Gisty",
55 version="1.0.0",
56 openapi_version="3.0.2",
57 info=dict(description="A minimal gist API"),
58 plugins=[FlaskPlugin(), MarshmallowPlugin()],
59 )
60
61
62 Our application will have a marshmallow `Schema <marshmallow.Schema>` for gists.
63
64 .. code-block:: python
65
66 from marshmallow import Schema, fields
67
68
69 class GistParameter(Schema):
70 gist_id = fields.Int()
71
72
73 class GistSchema(Schema):
74 id = fields.Int()
75 content = fields.Str()
76
77
78 The marshmallow plugin allows us to pass this `Schema` to
79 `spec.components.schema <apispec.core.Components.schema>`.
80
81
82 .. code-block:: python
83
84 spec.components.schema("Gist", schema=GistSchema)
85
86 The schema is now added to the spec.
87
88 .. code-block:: python
89
90 from pprint import pprint
91
92 pprint(spec.to_dict())
93 # {'components': {'parameters': {}, 'responses': {}, 'schemas': {}},
94 # 'info': {'description': 'A minimal gist API',
95 # 'title': 'Gisty',
96 # 'version': '1.0.0'},
97 # 'openapi': '3.0.2',
98 # 'paths': OrderedDict(),
99 # 'tags': []}
100
101 Our application will have a Flask route for the gist detail endpoint.
102
103 We'll add some YAML in the docstring to add response information.
104
105 .. code-block:: python
106
107 from flask import Flask
108
109 app = Flask(__name__)
110
111 # NOTE: Plugins may inspect docstrings to gather more information for the spec
112 @app.route("/gists/<gist_id>")
113 def gist_detail(gist_id):
114 """Gist detail view.
115 ---
116 get:
117 parameters:
118 - in: path
119 schema: GistParameter
120 responses:
121 200:
122 content:
123 application/json:
124 schema: GistSchema
125 """
126 return "details about gist {}".format(gist_id)
127
128 The Flask plugin allows us to pass this view to `spec.path <apispec.APISpec.path>`.
129
130
131 .. code-block:: python
132
133 # Since path inspects the view and its route,
134 # we need to be in a Flask request context
135 with app.test_request_context():
136 spec.path(view=gist_detail)
137
138
139 Our OpenAPI spec now looks like this:
140
141 .. code-block:: python
142
143 pprint(spec.to_dict())
144 # {'components': {'parameters': {},
145 # 'responses': {},
146 # 'schemas': {'Gist': {'properties': {'content': {'type': 'string'},
147 # 'id': {'format': 'int32',
148 # 'type': 'integer'}},
149 # 'type': 'object'}}},
150 # 'info': {'description': 'A minimal gist API',
151 # 'title': 'Gisty',
152 # 'version': '1.0.0'},
153 # 'openapi': '3.0.2',
154 # 'paths': OrderedDict([('/gists/{gist_id}',
155 # OrderedDict([('get',
156 # {'parameters': [{'in': 'path',
157 # 'name': 'gist_id',
158 # 'required': True,
159 # 'schema': {'format': 'int32',
160 # 'type': 'integer'}}],
161 # 'responses': {200: {'content': {'application/json': {'schema': {'$ref': '#/components/schemas/Gist'}}}}}})]))]),
162 # 'tags': []}
163
164 If your API uses `method-based dispatching <http://flask.pocoo.org/docs/0.12/views/#method-based-dispatching>`_, the process is similar. Note that the method no longer needs to be included in the docstring.
165
166 .. code-block:: python
167
168 from flask.views import MethodView
169
170
171 class GistApi(MethodView):
172 def get(self):
173 """Gist view
174 ---
175 description: Get a gist
176 responses:
177 200:
178 content:
179 application/json:
180 schema: GistSchema
181 """
182 pass
183
184 def post(self):
185 pass
186
187
188 method_view = GistApi.as_view("gist")
189 app.add_url_rule("/gist", view_func=method_view)
190 with app.test_request_context():
191 spec.path(view=method_view)
192 pprint(dict(spec.to_dict()["paths"]["/gist"]))
193 # {'get': {'description': 'get a gist',
194 # 'responses': {200: {'content': {'application/json': {'schema': {'$ref': '#/components/schemas/Gist'}}}}}},
195 # 'post': {}}
196
197
198 Marshmallow Plugin
199 ------------------
200
201 .. _marshmallow_nested_schemas:
202
203 Nested Schemas
204 **************
205
206 By default, Marshmallow `Nested` fields are represented by a `JSON Reference object
207 <https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#referenceObject>`_.
208 If the schema has been added to the spec via `spec.components.schema <apispec.core.Components.schema>`,
209 the user-supplied name will be used in the reference. Otherwise apispec will
210 add the nested schema to the spec using an automatically resolved name for the
211 nested schema. The default `resolver <apispec.ext.marshmallow.resolver>`
212 function will resolve a name based on the schema's class `__name__`, dropping a
213 trailing "Schema" so that `class PetSchema(Schema)` resolves to "Pet".
214
215 To change the behavior of the name resolution simply pass a
216 function accepting a `Schema` class, `Schema` instance or a string that resolves
217 to a `Schema` class and returning a string to the plugin's
218 constructor. To easily work with these argument types the marshmallow plugin provides
219 `resolve_schema_cls <apispec.ext.marshmallow.common.resolve_schema_cls>`
220 and `resolve_schema_instance <apispec.ext.marshmallow.common.resolve_schema_instance>`
221 functions. If the `schema_name_resolver` function returns a value that
222 evaluates to `False` in a boolean context the nested schema will not be added to
223 the spec and instead defined in-line.
224
225 .. note::
226 A `schema_name_resolver` function must return a string name when
227 working with circular-referencing schemas in order to avoid infinite
228 recursion.
229
230 Schema Modifiers
231 ****************
232
233 apispec will respect schema modifiers such as ``exclude`` and ``partial`` in the generated schema definition. If a schema is initialized with modifiers, apispec will treat each combination of modifiers as a unique schema definition.
234
235 Custom Fields
236 *************
237
238 apispec maps standard marshmallow fields to OpenAPI types and formats. If your
239 custom field subclasses a standard marshmallow `Field` class then it will
240 inherit the default mapping. If you want to override the OpenAPI type and format
241 for custom fields, use the
242 `map_to_openapi_type <apispec.ext.marshmallow.MarshmallowPlugin.map_to_openapi_type>`
243 decorator. It can be invoked with either a pair of strings providing the
244 OpenAPI type and format, or a marshmallow `Field` that has the desired target mapping.
245
246 .. code-block:: python
247
248 from apispec import APISpec
249 from apispec.ext.marshmallow import MarshmallowPlugin
250 from marshmallow.fields import Integer, Field
251
252 ma_plugin = MarshmallowPlugin()
253
254 spec = APISpec(
255 title="Demo", version="0.1", openapi_version="3.0.0", plugins=(ma_plugin,)
256 )
257
258 # Inherits Integer mapping of ('integer', 'int32')
259 class MyCustomInteger(Integer):
260 pass
261
262
263 # Override Integer mapping
264 @ma_plugin.map_to_openapi_type("string", "uuid")
265 class MyCustomField(Integer):
266 pass
267
268
269 @ma_plugin.map_to_openapi_type(Integer) # will map to ('integer', 'int32')
270 class MyCustomFieldThatsKindaLikeAnInteger(Field):
271 pass
272
273 In situations where greater control of the properties generated for a custom field
274 is desired, users may add custom logic to the conversion of fields to OpenAPI properties
275 through the use of the `add_attribute_function
276 <apispec.ext.marshmallow.field_converter.FieldConverterMixin.add_attribute_function>`
277 method. Continuing from the example above:
278
279 .. code-block:: python
280
281 def my_custom_field2properties(self, field, **kwargs):
282 """Add an OpenAPI extension flag to MyCustomField instances
283 """
284 ret = {}
285 if isinstance(field, MyCustomField):
286 if self.openapi_version.major > 2:
287 ret["x-customString"] = True
288 return ret
289
290
291 ma_plugin.converter.add_attribute_function(my_custom_field2properties)
292
293 The function passed to `add_attribute_function` will be bound to the converter.
294 It must accept the converter instance as first positional argument.
295
296 Next Steps
297 ----------
298
299 You now know how to use plugins. The next section will show you how to write plugins: :doc:`Writing Plugins <writing_plugins>`.
0 Writing Plugins
1 ===============
2
3 A plugins is a subclass of `apispec.plugin.BasePlugin`.
4
5
6 Helper Methods
7 --------------
8
9 Plugins provide "helper" methods that augment the behavior of `apispec.APISpec` methods.
10
11 There are five types of helper methods:
12
13 * Schema helpers
14 * Parameter helpers
15 * Response helpers
16 * Path helpers
17 * Operation helpers
18
19 Helper functions modify `apispec.APISpec` methods. For example, path helpers modify `apispec.APISpec.path`.
20
21
22 A plugin with a path helper function may look something like this:
23
24 .. code-block:: python
25
26 from apispec import Path, BasePlugin
27 from apispec.utils import load_operations_from_docstring
28
29
30 class MyPlugin(BasePlugin):
31 def path_helper(self, path, func, **kwargs):
32 """Path helper that parses docstrings for operations. Adds a
33 ``func`` parameter to `apispec.APISpec.path`.
34 """
35 operations = load_operations_from_docstring(func.__doc__)
36 return Path(path=path, operations=operations)
37
38
39 All plugin helpers must accept extra `**kwargs`, allowing custom plugins to define new arguments if required.
40
41 A plugin with an operation helper that adds `deprecated` flag may look like this
42
43 .. code-block:: python
44
45 # deprecated_plugin.py
46
47 from apispec import BasePlugin
48 from apispec.yaml_utils import load_operations_from_docstring
49
50
51 class DeprecatedPlugin(BasePlugin):
52 def operation_helper(self, path, operations, **kwargs):
53 """Operation helper that add `deprecated` flag if in `kwargs`
54 """
55 if kwargs.pop("deprecated", False) is True:
56 for key, value in operations.items():
57 value["deprecated"] = True
58
59
60 Using this plugin
61
62 .. code-block:: python
63
64 import json
65 from apispec import APISpec
66 from deprecated_plugin import DeprecatedPlugin
67
68 spec = APISpec(
69 title="Gisty",
70 version="1.0.0",
71 openapi_version="3.0.2",
72 plugins=[DeprecatedPlugin()],
73 )
74
75 # path will call operation_helper on operations
76 spec.path(
77 path="/gists/{gist_id}",
78 operations={"get": {"responses": {"200": {"description": "standard response"}}}},
79 deprecated=True,
80 )
81 print(json.dumps(spec.to_dict()["paths"]))
82 # {"/gists/{gist_id}": {"get": {"responses": {"200": {"description": "standard response"}}, "deprecated": true}}}
83
84
85
86 The ``init_spec`` Method
87 ------------------------
88
89 `BasePlugin` has an `init_spec` method that `APISpec` calls on each plugin at initialization with the spec object itself as parameter. It is no-op by default, but a plugin may override it to access and store useful information on the spec object.
90
91 A typical use case is conditional code depending on the OpenAPI version, which is stored as ``openapi_version`` on the `spec` object. See source code for `apispec.ext.marshmallow.MarshmallowPlugin </_modules/apispec/ext/marshmallow.html>`_ for an example.
92
93 Example: Docstring-parsing Plugin
94 ---------------------------------
95
96 Here's a plugin example involving conditional processing depending on the OpenAPI version:
97
98 .. code-block:: python
99
100 # docplugin.py
101
102 from apispec import BasePlugin
103 from apispec.yaml_utils import load_operations_from_docstring
104
105
106 class DocPlugin(BasePlugin):
107 def init_spec(self, spec):
108 super(DocPlugin, self).init_spec(spec)
109 self.openapi_major_version = spec.openapi_version.major
110
111 def operation_helper(self, operations, func, **kwargs):
112 """Operation helper that parses docstrings for operations. Adds a
113 ``func`` parameter to `apispec.APISpec.path`.
114 """
115 doc_operations = load_operations_from_docstring(func.__doc__)
116 # Apply conditional processing
117 if self.openapi_major_version < 3:
118 "...Mutating doc_operations for OpenAPI v2..."
119 else:
120 "...Mutating doc_operations for OpenAPI v3+..."
121 operations.update(doc_operations)
122
123
124 To use the plugin:
125
126 .. code-block:: python
127
128 from apispec import APISpec
129 from docplugin import DocPlugin
130
131 spec = APISpec(
132 title="Gisty", version="1.0.0", openapi_version="3.0.2", plugins=[DocPlugin()]
133 )
134
135
136 def gist_detail(gist_id):
137 """Gist detail view.
138 ---
139 get:
140 responses:
141 200:
142 content:
143 application/json:
144 schema: '#/definitions/Gist'
145 """
146 pass
147
148
149 spec.path(path="/gists/{gist_id}", func=gist_detail)
150 print(dict(spec.to_dict()["paths"]))
151 # {'/gists/{gist_id}': OrderedDict([('get', {'responses': {200: {'content': {'application/json': {'schema': '#/definitions/Gist'}}}}})])}
152
153
154 Next Steps
155 ----------
156
157 To learn more about how to write plugins:
158
159 * Consult the :doc:`Core API docs <api_core>` for `BasePlugin <apispec.BasePlugin>`
160 * View the source for an existing apispec plugin, e.g. `FlaskPlugin <https://github.com/marshmallow-code/apispec-webframeworks/blob/master/src/apispec_webframeworks/flask.py>`_.
161 * Check out some projects using apispec: https://github.com/marshmallow-code/apispec/wiki/Ecosystem
0 version: 2
1 sphinx:
2 configuration: docs/conf.py
3 formats: all
4 python:
5 version: 3.7
6 install:
7 - method: pip
8 path: .
9 extra_requirements:
10 - docs
0 [metadata]
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
9 [flake8]
10 ignore = E203, E266, E501, W503
11 max-line-length = 110
12 max-complexity = 18
13 select = B,C,E,F,W,T4,B9
0 import re
1 from setuptools import setup, find_packages
2
3 EXTRAS_REQUIRE = {
4 "yaml": ["PyYAML>=3.10"],
5 "validation": ["prance[osv]>=0.11"],
6 "lint": ["flake8==3.8.2", "flake8-bugbear==20.1.4", "pre-commit~=2.4"],
7 "docs": [
8 "marshmallow>=2.19.2",
9 "pyyaml==5.3.1",
10 "sphinx==3.0.4",
11 "sphinx-issues==1.2.0",
12 "sphinx-rtd-theme==0.4.3",
13 ],
14 }
15 EXTRAS_REQUIRE["tests"] = (
16 EXTRAS_REQUIRE["yaml"]
17 + EXTRAS_REQUIRE["validation"]
18 + ["marshmallow>=2.19.2", "pytest", "mock"]
19 )
20 EXTRAS_REQUIRE["dev"] = EXTRAS_REQUIRE["tests"] + EXTRAS_REQUIRE["lint"] + ["tox"]
21
22
23 def find_version(fname):
24 """Attempts to find the version number in the file names fname.
25 Raises RuntimeError if not found.
26 """
27 version = ""
28 with open(fname) as fp:
29 reg = re.compile(r'__version__ = [\'"]([^\'"]*)[\'"]')
30 for line in fp:
31 m = reg.match(line)
32 if m:
33 version = m.group(1)
34 break
35 if not version:
36 raise RuntimeError("Cannot find version information.")
37 return version
38
39
40 def read(fname):
41 with open(fname) as fp:
42 content = fp.read()
43 return content
44
45
46 setup(
47 name="apispec",
48 version=find_version("src/apispec/__init__.py"),
49 description="A pluggable API specification generator. Currently supports the "
50 "OpenAPI Specification (f.k.a. the Swagger specification).",
51 long_description=read("README.rst"),
52 author="Steven Loria",
53 author_email="[email protected]",
54 url="https://github.com/marshmallow-code/apispec",
55 packages=find_packages("src"),
56 package_dir={"": "src"},
57 include_package_data=True,
58 extras_require=EXTRAS_REQUIRE,
59 license="MIT",
60 zip_safe=False,
61 keywords="apispec swagger openapi specification oas documentation spec rest api",
62 python_requires=">=3.5",
63 classifiers=[
64 "License :: OSI Approved :: MIT License",
65 "Programming Language :: Python :: 3",
66 "Programming Language :: Python :: 3.5",
67 "Programming Language :: Python :: 3.6",
68 "Programming Language :: Python :: 3.7",
69 "Programming Language :: Python :: 3.8",
70 "Programming Language :: Python :: 3 :: Only",
71 ],
72 test_suite="tests",
73 project_urls={
74 "Funding": "https://opencollective.com/marshmallow",
75 "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 },
78 )
0 """Contains main apispec classes: `APISpec` and `BasePlugin`"""
1
2 from .core import APISpec
3 from .plugin import BasePlugin
4
5 __version__ = "3.3.1"
6 __all__ = ["APISpec", "BasePlugin"]
0 """Core apispec classes and functions."""
1 from collections import OrderedDict
2 from copy import deepcopy
3 import warnings
4
5 from .exceptions import (
6 APISpecError,
7 PluginMethodNotImplementedError,
8 DuplicateComponentNameError,
9 DuplicateParameterError,
10 InvalidParameterError,
11 )
12 from .utils import OpenAPIVersion, deepupdate, COMPONENT_SUBSECTIONS, build_reference
13
14 VALID_METHODS_OPENAPI_V2 = ["get", "post", "put", "patch", "delete", "head", "options"]
15
16 VALID_METHODS_OPENAPI_V3 = VALID_METHODS_OPENAPI_V2 + ["trace"]
17
18 VALID_METHODS = {2: VALID_METHODS_OPENAPI_V2, 3: VALID_METHODS_OPENAPI_V3}
19
20
21 class Components:
22 """Stores OpenAPI components
23
24 Components are top-level fields in OAS v2.
25 They became sub-fields of "components" top-level field in OAS v3.
26 """
27
28 def __init__(self, plugins, openapi_version):
29 self._plugins = plugins
30 self.openapi_version = openapi_version
31 self._schemas = {}
32 self._responses = {}
33 self._parameters = {}
34 self._examples = {}
35 self._security_schemes = {}
36
37 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 return {
46 COMPONENT_SUBSECTIONS[self.openapi_version.major][k]: v
47 for k, v in subsections.items()
48 if v != {}
49 }
50
51 def schema(self, name, component=None, **kwargs):
52 """Add a new schema to the spec.
53
54 :param str name: identifier by which schema may be referenced.
55 :param dict component: schema definition.
56
57 .. note::
58
59 If you are using `apispec.ext.marshmallow`, you can pass fields' metadata as
60 additional keyword arguments.
61
62 For example, to add ``enum`` and ``description`` to your field: ::
63
64 status = fields.String(
65 required=True,
66 enum=['open', 'closed'],
67 description='Status (open or closed)',
68 )
69
70 https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#schemaObject
71 """
72 if name in self._schemas:
73 raise DuplicateComponentNameError(
74 'Another schema with name "{}" is already registered.'.format(name)
75 )
76 component = component or {}
77 ret = component.copy()
78 # Execute all helpers from plugins
79 for plugin in self._plugins:
80 try:
81 ret.update(plugin.schema_helper(name, component, **kwargs) or {})
82 except PluginMethodNotImplementedError:
83 continue
84 self._schemas[name] = ret
85 return self
86
87 def response(self, component_id, component=None, **kwargs):
88 """Add a response which can be referenced.
89
90 :param str component_id: ref_id to use as reference
91 :param dict component: response fields
92 :param dict kwargs: plugin-specific arguments
93 """
94 if component_id in self._responses:
95 raise DuplicateComponentNameError(
96 'Another response with name "{}" is already registered.'.format(
97 component_id
98 )
99 )
100 component = component or {}
101 ret = component.copy()
102 # Execute all helpers from plugins
103 for plugin in self._plugins:
104 try:
105 ret.update(plugin.response_helper(component, **kwargs) or {})
106 except PluginMethodNotImplementedError:
107 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:
120 raise DuplicateComponentNameError(
121 'Another parameter with name "{}" is already registered.'.format(
122 component_id
123 )
124 )
125 component = component or {}
126 ret = component.copy()
127 ret.setdefault("name", component_id)
128 ret["in"] = location
129
130 # if "in" is set to "path", enforce required flag to True
131 if location == "path":
132 ret["required"] = True
133
134 # Execute all helpers from plugins
135 for plugin in self._plugins:
136 try:
137 ret.update(plugin.parameter_helper(component, **kwargs) or {})
138 except PluginMethodNotImplementedError:
139 continue
140 self._parameters[component_id] = ret
141 return self
142
143 def example(self, name, component, **kwargs):
144 """Add an example which can be referenced
145
146 :param str name: identifier by which example may be referenced.
147 :param dict component: example fields.
148
149 https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.1.md#exampleObject
150 """
151 if name in self._examples:
152 raise DuplicateComponentNameError(
153 'Another example with name "{}" is already registered.'.format(name)
154 )
155 self._examples[name] = component
156 return self
157
158 def security_scheme(self, component_id, component):
159 """Add a security scheme which can be referenced.
160
161 :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:
165 raise DuplicateComponentNameError(
166 'Another security scheme with name "{}" is already registered.'.format(
167 component_id
168 )
169 )
170 self._security_schemes[component_id] = component
171 return self
172
173
174 class APISpec:
175 """Stores metadata that describes a RESTful API using the OpenAPI specification.
176
177 :param str title: API title
178 :param str version: API version
179 :param list|tuple plugins: Plugin instances.
180 See https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#infoObject
181 :param str|OpenAPIVersion openapi_version: OpenAPI Specification version.
182 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
184 See https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#openapi-object
185 """
186
187 def __init__(self, title, version, openapi_version, plugins=(), **options):
188 self.title = title
189 self.version = version
190 self.openapi_version = OpenAPIVersion(openapi_version)
191 self.options = options
192 self.plugins = plugins
193
194 # Metadata
195 self._tags = []
196 self._paths = OrderedDict()
197
198 # Components
199 self.components = Components(self.plugins, self.openapi_version)
200
201 # Plugins
202 for plugin in self.plugins:
203 plugin.init_spec(self)
204
205 def to_dict(self):
206 ret = {
207 "paths": self._paths,
208 "info": {"title": self.title, "version": self.version},
209 }
210 if self._tags:
211 ret["tags"] = self._tags
212 if self.openapi_version.major < 3:
213 ret["swagger"] = self.openapi_version.vstring
214 ret.update(self.components.to_dict())
215 else:
216 ret["openapi"] = self.openapi_version.vstring
217 components_dict = self.components.to_dict()
218 if components_dict:
219 ret["components"] = components_dict
220 ret = deepupdate(ret, self.options)
221 return ret
222
223 def to_yaml(self):
224 """Render the spec to YAML. Requires PyYAML to be installed."""
225 from .yaml_utils import dict_to_yaml
226
227 return dict_to_yaml(self.to_dict())
228
229 def tag(self, tag):
230 """ Store information about a tag.
231
232 :param dict tag: the dictionary storing information about the tag.
233 """
234 self._tags.append(tag)
235 return self
236
237 def path(
238 self,
239 path=None,
240 *,
241 operations=None,
242 summary=None,
243 description=None,
244 parameters=None,
245 **kwargs
246 ):
247 """Add a new path object to the spec.
248
249 https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#path-item-object
250
251 :param str|None path: URL path component
252 :param dict|None operations: describes the http methods and options for `path`
253 :param str summary: short summary relevant to all operations in this path
254 :param str description: long description relevant to all operations in this path
255 :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`
257 """
258 # operations and parameters must be deepcopied because they are mutated
259 # in clean_operations and operation helpers and path may be called twice
260 operations = deepcopy(operations) or OrderedDict()
261 parameters = deepcopy(parameters) or []
262
263 # Execute path helpers
264 for plugin in self.plugins:
265 try:
266 ret = plugin.path_helper(
267 path=path, operations=operations, parameters=parameters, **kwargs
268 )
269 except PluginMethodNotImplementedError:
270 continue
271 if ret is not None:
272 path = ret
273 if not path:
274 raise APISpecError("Path template is not specified.")
275
276 # Execute operation helpers
277 for plugin in self.plugins:
278 try:
279 plugin.operation_helper(path=path, operations=operations, **kwargs)
280 except PluginMethodNotImplementedError:
281 continue
282
283 self.clean_operations(operations)
284
285 self._paths.setdefault(path, operations).update(operations)
286 if summary is not None:
287 self._paths[path]["summary"] = summary
288 if description is not None:
289 self._paths[path]["description"] = description
290 if parameters:
291 parameters = self.clean_parameters(parameters)
292 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):
310 """Ensure that all parameters with "in" equal to "path" are also required
311 as required by the OpenAPI specification, as well as normalizing any
312 references to global parameters and checking for duplicates parameters
313
314 See https ://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#parameterObject.
315
316 :param list parameters: List of parameters mapping
317 """
318 seen = set()
319 for parameter in [p for p in parameters if isinstance(p, dict)]:
320
321 # check missing name / location
322 missing_attrs = [attr for attr in ("name", "in") if attr not in parameter]
323 if missing_attrs:
324 raise InvalidParameterError(
325 "Missing keys {} for parameter".format(missing_attrs)
326 )
327
328 # OpenAPI Spec 3 and 2 don't allow for duplicated parameters
329 # A unique parameter is defined by a combination of a name and location
330 unique_key = (parameter["name"], parameter["in"])
331 if unique_key in seen:
332 raise DuplicateParameterError(
333 "Duplicate parameter with name {} and location {}".format(
334 parameter["name"], parameter["in"]
335 )
336 )
337 seen.add(unique_key)
338
339 # Add "required" attribute to path parameters
340 if parameter["in"] == "path":
341 parameter["required"] = True
342
343 return [self.get_ref("parameter", p) for p in parameters]
344
345 def clean_operations(self, operations):
346 """Ensure that all parameters with "in" equal to "path" are also required
347 as required by the OpenAPI specification, as well as normalizing any
348 references to global parameters. Also checks for invalid HTTP methods.
349
350 See https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#parameterObject.
351
352 :param dict operations: Dict mapping status codes to operations
353 """
354 operation_names = set(operations)
355 valid_methods = set(VALID_METHODS[self.openapi_version.major])
356 invalid = {
357 key for key in operation_names - valid_methods if not key.startswith("x-")
358 }
359 if invalid:
360 raise APISpecError(
361 "One or more HTTP methods are invalid: {}".format(", ".join(invalid))
362 )
363
364 for operation in (operations or {}).values():
365 if "parameters" in operation:
366 operation["parameters"] = self.clean_parameters(operation["parameters"])
367 if "responses" in operation:
368 responses = OrderedDict()
369 for code, response in operation["responses"].items():
370 try:
371 code = int(code) # handles IntEnums like http.HTTPStatus
372 except (TypeError, ValueError):
373 if self.openapi_version.major < 3 and code != "default":
374 warnings.warn("Non-integer code not allowed in OpenAPI < 3")
375
376 responses[str(code)] = self.get_ref("response", response)
377 operation["responses"] = responses
0 """Exception classes."""
1
2
3 class APISpecError(Exception):
4 """Base class for all apispec-related errors."""
5
6
7 class PluginMethodNotImplementedError(APISpecError, NotImplementedError):
8 """Raised when calling an unimplemented helper method in a plugin"""
9
10
11 class DuplicateComponentNameError(APISpecError):
12 """Raised when registering two components with the same name"""
13
14
15 class DuplicateParameterError(APISpecError):
16 """Raised when registering a parameter already existing in a given scope"""
17
18
19 class InvalidParameterError(APISpecError):
20 """Raised when parameter doesn't contains required keys"""
21
22
23 class OpenAPIError(APISpecError):
24 """Raised when a OpenAPI spec validation fails."""
(New empty file)
0 """marshmallow plugin for apispec. Allows passing a marshmallow
1 `Schema` to `spec.components.schema <apispec.core.Components.schema>`,
2 `spec.components.parameter <apispec.core.Components.parameter>`,
3 `spec.components.response <apispec.core.Components.response>`
4 (for response and headers schemas) and
5 `spec.path <apispec.APISpec.path>` (for responses and response headers).
6
7 Requires marshmallow>=2.15.2.
8
9 ``MarshmallowPlugin`` maps marshmallow ``Field`` classes with OpenAPI types and
10 formats.
11
12 It inspects field attributes to automatically document properties
13 such as read/write-only, range and length constraints, etc.
14
15 OpenAPI properties can also be passed as metadata to the ``Field`` instance
16 if they can't be inferred from the field attributes (`description`,...), or to
17 override automatic documentation (`readOnly`,...). A metadata attribute is used
18 in the documentation either if it is a valid OpenAPI property, or if it starts
19 with `"x-"` (vendor extension).
20
21 .. warning::
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.
27 This may lead to inaccurate documentation in very specific cases.
28 The default value to display in the documentation can be
29 specified explicitly by passing ``doc_default`` as metadata.
30
31 ::
32
33 from pprint import pprint
34 import datetime as dt
35
36 from apispec import APISpec
37 from apispec.ext.marshmallow import MarshmallowPlugin
38 from marshmallow import Schema, fields
39
40 spec = APISpec(
41 title="Example App",
42 version="1.0.0",
43 openapi_version="3.0.2",
44 plugins=[MarshmallowPlugin()],
45 )
46
47
48 class UserSchema(Schema):
49 id = fields.Int(dump_only=True)
50 name = fields.Str(description="The user's name")
51 created = fields.DateTime(
52 dump_only=True, default=dt.datetime.utcnow, doc_default="The current datetime"
53 )
54
55
56 spec.components.schema("User", schema=UserSchema)
57 pprint(spec.to_dict()["components"]["schemas"])
58 # {'User': {'properties': {'created': {'default': 'The current datetime',
59 # 'format': 'date-time',
60 # 'readOnly': True,
61 # 'type': 'string'},
62 # 'id': {'format': 'int32',
63 # 'readOnly': True,
64 # 'type': 'integer'},
65 # 'name': {'description': "The user's name",
66 # 'type': 'string'}},
67 # 'type': 'object'}}
68
69 """
70 import warnings
71
72 from apispec import BasePlugin
73 from .common import resolve_schema_instance, make_schema_key, resolve_schema_cls
74 from .openapi import OpenAPIConverter
75 from .schema_resolver import SchemaResolver
76
77
78 def resolver(schema):
79 """Default schema name resolver function that strips 'Schema' from the end of the class name."""
80 schema_cls = resolve_schema_cls(schema)
81 name = schema_cls.__name__
82 if name.endswith("Schema"):
83 return name[:-6] or name
84 return name
85
86
87 class MarshmallowPlugin(BasePlugin):
88 """APISpec plugin for translating marshmallow schemas to OpenAPI/JSONSchema format.
89
90 :param callable schema_name_resolver: Callable to generate the schema definition name.
91 Receives the `Schema` class and returns the name to be used in refs within
92 the generated spec. When working with circular referencing this function
93 must must not return `None` for schemas in a circular reference chain.
94
95 Example: ::
96
97 from apispec.ext.marshmallow.common import resolve_schema_cls
98
99 def schema_name_resolver(schema):
100 schema_cls = resolve_schema_cls(schema)
101 return schema_cls.__name__
102 """
103
104 Converter = OpenAPIConverter
105 Resolver = SchemaResolver
106
107 def __init__(self, schema_name_resolver=None):
108 super().__init__()
109 self.schema_name_resolver = schema_name_resolver or resolver
110 self.spec = None
111 self.openapi_version = None
112 self.converter = None
113 self.resolver = None
114
115 def init_spec(self, spec):
116 super().init_spec(spec)
117 self.spec = spec
118 self.openapi_version = spec.openapi_version
119 self.converter = self.Converter(
120 openapi_version=spec.openapi_version,
121 schema_name_resolver=self.schema_name_resolver,
122 spec=spec,
123 )
124 self.resolver = self.Resolver(
125 openapi_version=spec.openapi_version, converter=self.converter
126 )
127
128 def map_to_openapi_type(self, *args):
129 """Decorator to set mapping for custom fields.
130
131 ``*args`` can be:
132
133 - a pair of the form ``(type, format)``
134 - a core marshmallow field type (in which case we reuse that type's mapping)
135
136 Examples: ::
137
138 @ma_plugin.map_to_openapi_type('string', 'uuid')
139 class MyCustomField(Integer):
140 # ...
141
142 @ma_plugin.map_to_openapi_type(Integer) # will map to ('integer', 'int32')
143 class MyCustomFieldThatsKindaLikeAnInteger(Integer):
144 # ...
145 """
146 return self.converter.map_to_openapi_type(*args)
147
148 def schema_helper(self, name, _, schema=None, **kwargs):
149 """Definition helper that allows using a marshmallow
150 :class:`Schema <marshmallow.Schema>` to provide OpenAPI
151 metadata.
152
153 :param type|Schema schema: A marshmallow Schema class or instance.
154 """
155 if schema is None:
156 return None
157
158 schema_instance = resolve_schema_instance(schema)
159
160 schema_key = make_schema_key(schema_instance)
161 self.warn_if_schema_already_in_spec(schema_key)
162 self.converter.refs[schema_key] = name
163
164 json_schema = self.converter.schema2jsonschema(schema_instance)
165
166 return json_schema
167
168 def parameter_helper(self, parameter, **kwargs):
169 """Parameter component helper that allows using a marshmallow
170 :class:`Schema <marshmallow.Schema>` in parameter definition.
171
172 :param dict parameter: parameter fields. May contain a marshmallow
173 Schema class or instance.
174 """
175 # In OpenAPIv3, this only works when using the complex form using "content"
176 self.resolver.resolve_schema(parameter)
177 return parameter
178
179 def response_helper(self, response, **kwargs):
180 """Response component helper that allows using a marshmallow
181 :class:`Schema <marshmallow.Schema>` in response definition.
182
183 :param dict parameter: response fields. May contain a marshmallow
184 Schema class or instance.
185 """
186 self.resolver.resolve_response(response)
187 return response
188
189 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)
202
203 def warn_if_schema_already_in_spec(self, schema_key):
204 """Method to warn the user if the schema has already been added to the
205 spec.
206 """
207 if schema_key in self.converter.refs:
208 warnings.warn(
209 "{} has already been added to the spec. Adding it twice may "
210 "cause references to not resolve properly.".format(schema_key[0]),
211 UserWarning,
212 )
0 """Utilities to get schema instances/classes"""
1
2 import copy
3 import warnings
4 from collections import namedtuple, OrderedDict
5
6 import marshmallow
7
8
9 MODIFIERS = ["only", "exclude", "load_only", "dump_only", "partial"]
10
11
12 def resolve_schema_instance(schema):
13 """Return schema instance for given schema (instance or class).
14
15 :param type|Schema|str schema: instance, class or class name of marshmallow.Schema
16 :return: schema instance of given schema (instance or class)
17 """
18 if isinstance(schema, type) and issubclass(schema, marshmallow.Schema):
19 return schema()
20 if isinstance(schema, marshmallow.Schema):
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 )
29
30
31 def resolve_schema_cls(schema):
32 """Return schema class for given schema (instance or class).
33
34 :param type|Schema|str: instance, class or class name of marshmallow.Schema
35 :return: schema class of given schema (instance or class)
36 """
37 if isinstance(schema, type) and issubclass(schema, marshmallow.Schema):
38 return schema
39 if isinstance(schema, marshmallow.Schema):
40 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 )
48
49
50 def get_fields(schema, *, exclude_dump_only=False):
51 """Return fields from schema.
52
53 :param Schema schema: A marshmallow Schema instance or a class object
54 :param bool exclude_dump_only: whether to filter fields in Meta.dump_only
55 :rtype: dict, of field name field object pairs
56 """
57 if hasattr(schema, "fields"):
58 fields = schema.fields
59 elif hasattr(schema, "_declared_fields"):
60 fields = copy.deepcopy(schema._declared_fields)
61 else:
62 raise ValueError(
63 "{!r} doesn't have either `fields` or `_declared_fields`.".format(schema)
64 )
65 Meta = getattr(schema, "Meta", None)
66 warn_if_fields_defined_in_meta(fields, Meta)
67 return filter_excluded_fields(fields, Meta, exclude_dump_only=exclude_dump_only)
68
69
70 def warn_if_fields_defined_in_meta(fields, Meta):
71 """Warns user that fields defined in Meta.fields or Meta.additional will be ignored.
72
73 :param dict fields: A dictionary of fields name field object pairs
74 :param Meta: the schema's Meta class
75 """
76 if getattr(Meta, "fields", None) or getattr(Meta, "additional", None):
77 declared_fields = set(fields.keys())
78 if (
79 set(getattr(Meta, "fields", set())) > declared_fields
80 or set(getattr(Meta, "additional", set())) > declared_fields
81 ):
82 warnings.warn(
83 "Only explicitly-declared fields will be included in the Schema Object. "
84 "Fields defined in Meta.fields or Meta.additional are ignored."
85 )
86
87
88 def filter_excluded_fields(fields, Meta, *, exclude_dump_only):
89 """Filter fields that should be ignored in the OpenAPI spec.
90
91 :param dict fields: A dictionary of fields name field object pairs
92 :param Meta: the schema's Meta class
93 :param bool exclude_dump_only: whether to filter fields in Meta.dump_only
94 """
95 exclude = list(getattr(Meta, "exclude", []))
96 if exclude_dump_only:
97 exclude.extend(getattr(Meta, "dump_only", []))
98
99 filtered_fields = OrderedDict(
100 (key, value) for key, value in fields.items() if key not in exclude
101 )
102
103 return filtered_fields
104
105
106 def make_schema_key(schema):
107 if not isinstance(schema, marshmallow.Schema):
108 raise TypeError("can only make a schema key based on a Schema instance.")
109 modifiers = []
110 for modifier in MODIFIERS:
111 attribute = getattr(schema, modifier)
112 try:
113 # Hashable (string, tuple)
114 hash(attribute)
115 except TypeError:
116 # Unhashable iterable (list, set)
117 attribute = frozenset(attribute)
118 modifiers.append(attribute)
119 return SchemaKey(schema.__class__, *modifiers)
120
121
122 SchemaKey = namedtuple("SchemaKey", ["SchemaClass"] + MODIFIERS)
123
124
125 def get_unique_schema_name(components, name, counter=0):
126 """Function to generate a unique name based on the provided name and names
127 already in the spec. Will append a number to the name to make it unique if
128 the name is already in the spec.
129
130 :param Components components: instance of the components of the spec
131 :param string name: the name to use as a basis for the unique name
132 :param int counter: the counter of the number of recursions
133 :return: the unique name
134 """
135 if name not in components._schemas:
136 return name
137 if not counter: # first time through recursion
138 warnings.warn(
139 "Multiple schemas resolved to the name {}. The name has been modified. "
140 "Either manually add each of the schemas with a different name or "
141 "provide a custom schema_name_resolver.".format(name),
142 UserWarning,
143 )
144 else: # subsequent recursions
145 name = name[: -len(str(counter))]
146 counter += 1
147 return get_unique_schema_name(components, name + str(counter), counter)
0 """Utilities for generating OpenAPI Specification (fka Swagger) entities from
1 :class:`Fields <marshmallow.fields.Field>`.
2
3 .. warning::
4
5 This module is treated as private API.
6 Users should not need to use this module directly.
7 """
8 import re
9 import functools
10 import operator
11 import warnings
12
13 import marshmallow
14 from marshmallow.orderedset import OrderedSet
15
16
17 RegexType = type(re.compile(""))
18
19 MARSHMALLOW_VERSION_INFO = tuple(
20 [int(part) for part in marshmallow.__version__.split(".") if part.isdigit()]
21 )
22
23
24 # marshmallow field => (JSON Schema type, format)
25 DEFAULT_FIELD_MAPPING = {
26 marshmallow.fields.Integer: ("integer", "int32"),
27 marshmallow.fields.Number: ("number", None),
28 marshmallow.fields.Float: ("number", "float"),
29 marshmallow.fields.Decimal: ("number", None),
30 marshmallow.fields.String: ("string", None),
31 marshmallow.fields.Boolean: ("boolean", None),
32 marshmallow.fields.UUID: ("string", "uuid"),
33 marshmallow.fields.DateTime: ("string", "date-time"),
34 marshmallow.fields.Date: ("string", "date"),
35 marshmallow.fields.Time: ("string", None),
36 marshmallow.fields.Email: ("string", "email"),
37 marshmallow.fields.URL: ("string", "url"),
38 marshmallow.fields.Dict: ("object", None),
39 marshmallow.fields.Field: (None, None),
40 marshmallow.fields.Raw: (None, None),
41 marshmallow.fields.List: ("array", None),
42 }
43
44
45 # Properties that may be defined in a field's metadata that will be added to the output
46 # of field2property
47 # https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#schemaObject
48 _VALID_PROPERTIES = {
49 "format",
50 "title",
51 "description",
52 "default",
53 "multipleOf",
54 "maximum",
55 "exclusiveMaximum",
56 "minimum",
57 "exclusiveMinimum",
58 "maxLength",
59 "minLength",
60 "pattern",
61 "maxItems",
62 "minItems",
63 "uniqueItems",
64 "maxProperties",
65 "minProperties",
66 "required",
67 "enum",
68 "type",
69 "items",
70 "allOf",
71 "properties",
72 "additionalProperties",
73 "readOnly",
74 "xml",
75 "externalDocs",
76 "example",
77 }
78
79
80 _VALID_PREFIX = "x-"
81
82
83 class FieldConverterMixin:
84 """Adds methods for converting marshmallow fields to an OpenAPI properties."""
85
86 field_mapping = DEFAULT_FIELD_MAPPING
87
88 def init_attribute_functions(self):
89 self.attribute_functions = [
90 self.field2type_and_format,
91 self.field2default,
92 self.field2choices,
93 self.field2read_only,
94 self.field2write_only,
95 self.field2nullable,
96 self.field2range,
97 self.field2length,
98 self.field2pattern,
99 self.metadata2properties,
100 self.nested2properties,
101 self.list2properties,
102 self.dict2properties,
103 ]
104
105 def map_to_openapi_type(self, *args):
106 """Decorator to set mapping for custom fields.
107
108 ``*args`` can be:
109
110 - a pair of the form ``(type, format)``
111 - a core marshmallow field type (in which case we reuse that type's mapping)
112 """
113 if len(args) == 1 and args[0] in self.field_mapping:
114 openapi_type_field = self.field_mapping[args[0]]
115 elif len(args) == 2:
116 openapi_type_field = args
117 else:
118 raise TypeError("Pass core marshmallow field type or (type, fmt) pair.")
119
120 def inner(field_type):
121 self.field_mapping[field_type] = openapi_type_field
122 return field_type
123
124 return inner
125
126 def add_attribute_function(self, func):
127 """Method to add an attribute function to the list of attribute functions
128 that will be called on a field to convert it from a field to an OpenAPI
129 property.
130
131 :param func func: the attribute function to add
132 The attribute function will be bound to the
133 `OpenAPIConverter <apispec.ext.marshmallow.openapi.OpenAPIConverter>`
134 instance.
135 It will be called for each field in a schema with
136 `self <apispec.ext.marshmallow.openapi.OpenAPIConverter>` and a
137 `field <marshmallow.fields.Field>` instance
138 positional arguments and `ret <dict>` keyword argument.
139 Must return a dictionary of OpenAPI properties that will be shallow
140 merged with the return values of all other attribute functions called on the field.
141 User added attribute functions will be called after all built-in attribute
142 functions in the order they were added. The merged results of all
143 previously called attribute functions are accessable via the `ret`
144 argument.
145 """
146 bound_func = func.__get__(self)
147 setattr(self, func.__name__, bound_func)
148 self.attribute_functions.append(bound_func)
149
150 def field2property(self, field):
151 """Return the JSON Schema property definition given a marshmallow
152 :class:`Field <marshmallow.fields.Field>`.
153
154 Will include field metadata that are valid properties of OpenAPI schema objects
155 (e.g. "description", "enum", "example").
156
157 https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#schemaObject
158
159 :param Field field: A marshmallow field.
160 :rtype: dict, a Property Object
161 """
162 ret = {}
163
164 for attr_func in self.attribute_functions:
165 ret.update(attr_func(field, ret=ret))
166
167 return ret
168
169 def field2type_and_format(self, field, **kwargs):
170 """Return the dictionary of OpenAPI type and format based on the field type.
171
172 :param Field field: A marshmallow field.
173 :rtype: dict
174 """
175 # If this type isn't directly in the field mapping then check the
176 # hierarchy until we find something that does.
177 for field_class in type(field).__mro__:
178 if field_class in self.field_mapping:
179 type_, fmt = self.field_mapping[field_class]
180 break
181 else:
182 warnings.warn(
183 "Field of type {} does not inherit from marshmallow.Field.".format(
184 type(field)
185 ),
186 UserWarning,
187 )
188 type_, fmt = "string", None
189
190 ret = {}
191 if type_:
192 ret["type"] = type_
193 if fmt:
194 ret["format"] = fmt
195
196 return ret
197
198 def field2default(self, field, **kwargs):
199 """Return the dictionary containing the field's default value.
200
201 Will first look for a `doc_default` key in the field's metadata and then
202 fall back on the field's `missing` parameter. A callable passed to the
203 field's missing parameter will be ignored.
204
205 :param Field field: A marshmallow field.
206 :rtype: dict
207 """
208 ret = {}
209 if "doc_default" in field.metadata:
210 ret["default"] = field.metadata["doc_default"]
211 else:
212 default = field.missing
213 if default is not marshmallow.missing and not callable(default):
214 if MARSHMALLOW_VERSION_INFO[0] >= 3:
215 default = field._serialize(default, None, None)
216 ret["default"] = default
217 return ret
218
219 def field2choices(self, field, **kwargs):
220 """Return the dictionary of OpenAPI field attributes for valid choices definition.
221
222 :param Field field: A marshmallow field.
223 :rtype: dict
224 """
225 attributes = {}
226
227 comparable = [
228 validator.comparable
229 for validator in field.validators
230 if hasattr(validator, "comparable")
231 ]
232 if comparable:
233 attributes["enum"] = comparable
234 else:
235 choices = [
236 OrderedSet(validator.choices)
237 for validator in field.validators
238 if hasattr(validator, "choices")
239 ]
240 if choices:
241 attributes["enum"] = list(functools.reduce(operator.and_, choices))
242
243 return attributes
244
245 def field2read_only(self, field, **kwargs):
246 """Return the dictionary of OpenAPI field attributes for a dump_only field.
247
248 :param Field field: A marshmallow field.
249 :rtype: dict
250 """
251 attributes = {}
252 if field.dump_only:
253 attributes["readOnly"] = True
254 return attributes
255
256 def field2write_only(self, field, **kwargs):
257 """Return the dictionary of OpenAPI field attributes for a load_only field.
258
259 :param Field field: A marshmallow field.
260 :rtype: dict
261 """
262 attributes = {}
263 if field.load_only and self.openapi_version.major >= 3:
264 attributes["writeOnly"] = True
265 return attributes
266
267 def field2nullable(self, field, **kwargs):
268 """Return the dictionary of OpenAPI field attributes for a nullable field.
269
270 :param Field field: A marshmallow field.
271 :rtype: dict
272 """
273 attributes = {}
274 if field.allow_none:
275 attributes[
276 "x-nullable" if self.openapi_version.major < 3 else "nullable"
277 ] = True
278 return attributes
279
280 def field2range(self, field, **kwargs):
281 """Return the dictionary of OpenAPI field attributes for a set of
282 :class:`Range <marshmallow.validators.Range>` validators.
283
284 :param Field field: A marshmallow field.
285 :rtype: dict
286 """
287 validators = [
288 validator
289 for validator in field.validators
290 if (
291 hasattr(validator, "min")
292 and hasattr(validator, "max")
293 and not hasattr(validator, "equal")
294 )
295 ]
296
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
310
311 def field2length(self, field, **kwargs):
312 """Return the dictionary of OpenAPI field attributes for a set of
313 :class:`Length <marshmallow.validators.Length>` validators.
314
315 :param Field field: A marshmallow field.
316 :rtype: dict
317 """
318 attributes = {}
319
320 validators = [
321 validator
322 for validator in field.validators
323 if (
324 hasattr(validator, "min")
325 and hasattr(validator, "max")
326 and hasattr(validator, "equal")
327 )
328 ]
329
330 is_array = isinstance(
331 field, (marshmallow.fields.Nested, marshmallow.fields.List)
332 )
333 min_attr = "minItems" if is_array else "minLength"
334 max_attr = "maxItems" if is_array else "maxLength"
335
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
353
354 def field2pattern(self, field, **kwargs):
355 """Return the dictionary of OpenAPI field attributes for a set of
356 :class:`Range <marshmallow.validators.Regexp>` validators.
357
358 :param Field field: A marshmallow field.
359 :rtype: dict
360 """
361 regex_validators = (
362 v
363 for v in field.validators
364 if isinstance(getattr(v, "regex", None), RegexType)
365 )
366 v = next(regex_validators, None)
367 attributes = {} if v is None else {"pattern": v.regex.pattern}
368
369 if next(regex_validators, None) is not None:
370 warnings.warn(
371 "More than one regex validator defined on {} field. Only the "
372 "first one will be used in the output spec.".format(type(field)),
373 UserWarning,
374 )
375
376 return attributes
377
378 def metadata2properties(self, field, **kwargs):
379 """Return a dictionary of properties extracted from field metadata.
380
381 Will include field metadata that are valid properties of `OpenAPI schema
382 objects
383 <https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#schemaObject>`_
384 (e.g. "description", "enum", "example").
385
386 In addition, `specification extensions
387 <https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#specification-extensions>`_
388 are supported. Prefix `x_` to the desired extension when passing the
389 keyword argument to the field constructor. apispec will convert `x_` to
390 `x-` to comply with OpenAPI.
391
392 :param Field field: A marshmallow field.
393 :rtype: dict
394 """
395 # Dasherize metadata that starts with x_
396 metadata = {
397 key.replace("_", "-") if key.startswith("x_") else key: value
398 for key, value in field.metadata.items()
399 }
400
401 # Avoid validation error with "Additional properties not allowed"
402 ret = {
403 key: value
404 for key, value in metadata.items()
405 if key in _VALID_PROPERTIES or key.startswith(_VALID_PREFIX)
406 }
407 return ret
408
409 def nested2properties(self, field, ret):
410 """Return a dictionary of properties from :class:`Nested <marshmallow.fields.Nested` fields.
411
412 Typically provides a reference object and will add the schema to the spec
413 if it is not already present
414 If a custom `schema_name_resolver` function returns `None` for the nested
415 schema a JSON schema object will be returned
416
417 :param Field field: A marshmallow field.
418 :rtype: dict
419 """
420 if isinstance(field, marshmallow.fields.Nested):
421 schema_dict = self.resolve_nested_schema(field.schema)
422 if ret and "$ref" in schema_dict:
423 ret.update({"allOf": [schema_dict]})
424 else:
425 ret.update(schema_dict)
426 return ret
427
428 def list2properties(self, field, **kwargs):
429 """Return a dictionary of properties from :class:`List <marshmallow.fields.List>` fields.
430
431 Will provide an `items` property based on the field's `inner` attribute
432
433 :param Field field: A marshmallow field.
434 :rtype: dict
435 """
436 ret = {}
437 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)
442 return ret
443
444 def dict2properties(self, field, **kwargs):
445 """Return a dictionary of properties from :class:`Dict <marshmallow.fields.Dict>` fields.
446
447 Only applicable for Marshmallow versions greater than 3. Will provide an
448 `additionalProperties` property based on the field's `value_field` attribute
449
450 :param Field field: A marshmallow field.
451 :rtype: dict
452 """
453 ret = {}
454 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
0 """Utilities for generating OpenAPI Specification (fka Swagger) entities from
1 marshmallow :class:`Schemas <marshmallow.Schema>` and :class:`Fields <marshmallow.fields.Field>`.
2
3 .. warning::
4
5 This module is treated as private API.
6 Users should not need to use this module directly.
7 """
8 from collections import OrderedDict
9
10 import marshmallow
11 from marshmallow.utils import is_collection
12
13 from apispec.utils import OpenAPIVersion, build_reference
14 from apispec.exceptions import APISpecError
15 from .field_converter import FieldConverterMixin
16 from .common import (
17 get_fields,
18 make_schema_key,
19 resolve_schema_instance,
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 )
27
28
29 __location_map__ = {
30 "match_info": "path",
31 "query": "query",
32 "querystring": "query",
33 "json": "body",
34 "headers": "header",
35 "cookies": "cookie",
36 "form": "formData",
37 "files": "formData",
38 }
39
40
41 class OpenAPIConverter(FieldConverterMixin):
42 """Adds methods for generating OpenAPI specification from marshmallow schemas and fields.
43
44 :param str|OpenAPIVersion openapi_version: The OpenAPI version to use.
45 Should be in the form '2.x' or '3.x.x' to comply with the OpenAPI standard.
46 :param callable schema_name_resolver: Callable to generate the schema definition name.
47 Receives the `Schema` class and returns the name to be used in refs within
48 the generated spec. When working with circular referencing this function
49 must must not return `None` for schemas in a circular reference chain.
50 :param APISpec spec: An initalied spec. Nested schemas will be added to the
51 spec
52 """
53
54 def __init__(self, openapi_version, schema_name_resolver, spec):
55 self.openapi_version = OpenAPIVersion(openapi_version)
56 self.schema_name_resolver = schema_name_resolver
57 self.spec = spec
58 self.init_attribute_functions()
59 # Schema references
60 self.refs = {}
61
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 def resolve_nested_schema(self, schema):
78 """Return the OpenAPI representation of a marshmallow Schema.
79
80 Adds the schema to the spec if it isn't already present.
81
82 Typically will return a dictionary with the reference to the schema's
83 path in the spec unless the `schema_name_resolver` returns `None`, in
84 which case the returned dictoinary will contain a JSON Schema Object
85 representation of the schema.
86
87 :param schema: schema to add to the spec
88 """
89 schema_instance = resolve_schema_instance(schema)
90 schema_key = make_schema_key(schema_instance)
91 if schema_key not in self.refs:
92 name = self.schema_name_resolver(schema)
93 if not name:
94 try:
95 json_schema = self.schema2jsonschema(schema_instance)
96 except RuntimeError:
97 raise APISpecError(
98 "Name resolver returned None for schema {schema} which is "
99 "part of a chain of circular referencing schemas. Please"
100 " ensure that the schema_name_resolver passed to"
101 " MarshmallowPlugin returns a string for all circular"
102 " referencing schemas.".format(schema=schema)
103 )
104 if getattr(schema, "many", False):
105 return {"type": "array", "items": json_schema}
106 return json_schema
107 name = get_unique_schema_name(self.spec.components, name)
108 self.spec.components.schema(name, schema=schema)
109 return self.get_ref_dict(schema_instance)
110
111 def schema2parameters(
112 self,
113 schema,
114 *,
115 default_in="body",
116 name="body",
117 required=False,
118 description=None
119 ):
120 """Return an array of OpenAPI parameters given a given marshmallow
121 :class:`Schema <marshmallow.Schema>`. If `default_in` is "body", then return an array
122 of a single parameter; else return an array of a parameter for each included field in
123 the :class:`Schema <marshmallow.Schema>`.
124
125 https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#parameterObject
126 """
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
131 param = {
132 "in": openapi_default_in,
133 "required": required,
134 "name": name,
135 "schema": prop,
136 }
137
138 if description:
139 param["description"] = description
140
141 return [param]
142
143 assert not getattr(
144 schema, "many", False
145 ), "Schemas with many=True are only supported for 'json' location (aka 'in: body')"
146
147 fields = get_fields(schema, exclude_dump_only=True)
148
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(
171 field_obj,
172 name=self._observed_name(field_obj, field_name),
173 default_in=default_in,
174 )
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):
193 """Return an OpenAPI parameter as a `dict`, given a marshmallow
194 :class:`Field <marshmallow.Field>`.
195
196 https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#parameterObject
197 """
198 location = field.metadata.get("location", None)
199 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]
235 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
248 return ret
249
250 def schema2jsonschema(self, schema):
251 """Return the JSON Schema Object for a given marshmallow
252 :class:`Schema <marshmallow.Schema>` instance. Schema may optionally
253 provide the ``title`` and ``description`` class Meta options.
254
255 https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#schemaObject
256
257 :param Schema schema: A marshmallow Schema instance
258 :rtype: dict, a JSON Schema Object
259 """
260 fields = get_fields(schema)
261 Meta = getattr(schema, "Meta", None)
262 partial = getattr(schema, "partial", None)
263 ordered = getattr(schema, "ordered", False)
264
265 jsonschema = self.fields2jsonschema(fields, partial=partial, ordered=ordered)
266
267 if hasattr(Meta, "title"):
268 jsonschema["title"] = Meta.title
269 if hasattr(Meta, "description"):
270 jsonschema["description"] = Meta.description
271
272 return jsonschema
273
274 def fields2jsonschema(self, fields, *, ordered=False, partial=None):
275 """Return the JSON Schema Object given a mapping between field names and
276 :class:`Field <marshmallow.Field>` objects.
277
278 :param dict fields: A dictionary of field name field object pairs
279 :param bool ordered: Whether to preserve the order in which fields were declared
280 :param bool|tuple partial: Whether to override a field's required flag.
281 If `True` no fields will be set as required. If an iterable fields
282 in the iterable will not be marked as required.
283 :rtype: dict, a JSON Schema Object
284 """
285 jsonschema = {"type": "object", "properties": OrderedDict() if ordered else {}}
286
287 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
291
292 if field_obj.required:
293 if not partial or (
294 is_collection(partial) and field_name not in partial
295 ):
296 jsonschema.setdefault("required", []).append(observed_field_name)
297
298 if "required" in jsonschema:
299 jsonschema["required"].sort()
300
301 return jsonschema
302
303 def get_ref_dict(self, schema):
304 """Method to create a dictionary containing a JSON reference to the
305 schema in the spec
306 """
307 schema_key = make_schema_key(schema)
308 ref_schema = build_reference(
309 "schema", self.openapi_version.major, self.refs[schema_key]
310 )
311 if getattr(schema, "many", False):
312 return {"type": "array", "items": ref_schema}
313 return ref_schema
0 from .common import resolve_schema_instance
1
2
3 class SchemaResolver:
4 """Resolve marshmallow Schemas in OpenAPI components and translate to OpenAPI
5 `schema objects
6 <https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#schema-object>`_,
7 `parameter objects
8 <https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#parameter-object>`_
9 or `reference objects
10 <https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#reference-object>`_.
11 """
12
13 def __init__(self, openapi_version, converter):
14 self.openapi_version = openapi_version
15 self.converter = converter
16
17 def resolve_parameters(self, parameters):
18 """Resolve marshmallow Schemas in a list of OpenAPI `Parameter Objects
19 <https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#parameter-object>`_.
20 Each parameter object that contains a Schema will be translated into
21 one or more Parameter Objects.
22
23 If the value of a `schema` key is marshmallow Schema class, instance or
24 a string that resolves to a Schema Class each field in the Schema will
25 be expanded as a separate Parameter Object.
26
27 Example: ::
28
29 #Input
30 class UserSchema(Schema):
31 name = fields.String()
32 id = fields.Int()
33
34 [
35 {"in": "query", "schema": "UserSchema"}
36 ]
37
38 #Output
39 [
40 {"in": "query", "name": "id", "required": False, "schema": {"type": "integer", "format": "int32"}},
41 {"in": "query", "name": "name", "required": False, "schema": {"type": "string"}}
42 ]
43
44 If the Parameter Object contains a `content` key a single Parameter
45 Object is returned with the Schema translated into a Schema Object or
46 Reference Object.
47
48 Example: ::
49
50 #Input
51 [{"in": "query", "name": "pet", "content":{"application/json": {"schema": "PetSchema"}} }]
52
53 #Output
54 [
55 {
56 "in": "query",
57 "name": "pet",
58 "content": {
59 "application/json": {
60 "schema": {"$ref": "#/components/schemas/Pet"}
61 }
62 }
63 }
64 ]
65
66
67 :param list parameters: the list of OpenAPI parameter objects to resolve.
68 """
69 resolved = []
70 for parameter in parameters:
71 if (
72 isinstance(parameter, dict)
73 and not isinstance(parameter.get("schema", {}), dict)
74 and "in" in parameter
75 ):
76 schema_instance = resolve_schema_instance(parameter.pop("schema"))
77 resolved += self.converter.schema2parameters(
78 schema_instance, default_in=parameter.pop("in"), **parameter
79 )
80 else:
81 self.resolve_schema(parameter)
82 resolved.append(parameter)
83 return resolved
84
85 def resolve_response(self, response):
86 """Resolve marshmallow Schemas in OpenAPI `Response Objects
87 <https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#responseObject>`_.
88 Schemas may appear in either a Media Type Object or a Header Object.
89
90 Example: ::
91
92 #Input
93 {
94 "content": {"application/json": {"schema": "PetSchema"}},
95 "description": "successful operation",
96 "headers": {"PetHeader": {"schema": "PetHeaderSchema"}},
97 }
98
99 #Output
100 {
101 "content": {
102 "application/json":{"schema": {"$ref": "#/components/schemas/Pet"}}
103 },
104 "description": "successful operation",
105 "headers": {
106 "PetHeader": {"schema": {"$ref": "#/components/schemas/PetHeader"}}
107 },
108 }
109
110 :param dict response: the response object to resolve.
111 """
112 self.resolve_schema(response)
113 if "headers" in response:
114 for header in response["headers"].values():
115 self.resolve_schema(header)
116
117 def resolve_schema(self, data):
118 """Resolve marshmallow Schemas in an OpenAPI component or header -
119 modifies the input dictionary to translate marshmallow Schemas to OpenAPI
120 Schema Objects or Reference Objects.
121
122 OpenAPIv3 Components: ::
123
124 #Input
125 {
126 "description": "user to add to the system",
127 "content": {
128 "application/json": {
129 "schema": "UserSchema"
130 }
131 }
132 }
133
134 #Output
135 {
136 "description": "user to add to the system",
137 "content": {
138 "application/json": {
139 "schema": {
140 "$ref": "#/components/schemas/User"
141 }
142 }
143 }
144 }
145
146 :param dict|str data: either a parameter or response dictionary that may
147 contain a schema, or a reference provided as string
148 """
149 if not isinstance(data, dict):
150 return
151
152 # OAS 2 component or OAS 3 header
153 if "schema" in data:
154 data["schema"] = self.resolve_schema_dict(data["schema"])
155 # OAS 3 component except header
156 if self.openapi_version.major >= 3:
157 if "content" in data:
158 for content in data["content"].values():
159 if "schema" in content:
160 content["schema"] = self.resolve_schema_dict(content["schema"])
161
162 def resolve_schema_dict(self, schema):
163 """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.
166
167 If the input is a marshmallow Schema class, object or a string that resolves
168 to a Schema class the Schema will be translated to an OpenAPI Schema Object
169 or Reference Object.
170
171 Example: ::
172
173 #Input
174 "PetSchema"
175
176 #Output
177 {"$ref": "#/components/schemas/Pet"}
178
179 If the input is a dictionary representation of an OpenAPI Schema Object
180 recursively search for a marshmallow Schemas to resolve. For `"type": "array"`,
181 marshmallow Schemas may appear as the value of the `items` key. For
182 `"type": "object"` Marshmalow Schemas may appear as values in the `properties`
183 dictionary.
184
185 Examples: ::
186
187 #Input
188 {"type": "array", "items": "PetSchema"}
189
190 #Output
191 {"type": "array", "items": {"$ref": "#/components/schemas/Pet"}}
192
193 #Input
194 {"type": "object", "properties": {"pet": "PetSchcema", "user": "UserSchema"}}
195
196 #Output
197 {
198 "type": "object",
199 "properties": {
200 "pet": {"$ref": "#/components/schemas/Pet"},
201 "user": {"$ref": "#/components/schemas/User"}
202 }
203 }
204
205 :param string|Schema|dict schema: the schema to resolve.
206 """
207 if isinstance(schema, dict):
208 if schema.get("type") == "array" and "items" in schema:
209 schema["items"] = self.resolve_schema_dict(schema["items"])
210 if schema.get("type") == "object" and "properties" in schema:
211 schema["properties"] = {
212 k: self.resolve_schema_dict(v)
213 for k, v in schema["properties"].items()
214 }
215 return schema
216
217 return self.converter.resolve_nested_schema(schema)
0 """Base class for Plugin classes."""
1
2
3 from .exceptions import PluginMethodNotImplementedError
4
5
6 class BasePlugin:
7 """Base class for APISpec plugin classes."""
8
9 def init_spec(self, spec):
10 """Initialize plugin with APISpec object
11
12 :param APISpec spec: APISpec object this plugin instance is attached to
13 """
14
15 def schema_helper(self, name, definition, **kwargs):
16 """May return definition as a dict.
17
18 :param str name: Identifier by which schema may be referenced
19 :param dict definition: Schema definition
20 :param dict kwargs: All additional keywords arguments sent to `APISpec.schema()`
21 """
22 raise PluginMethodNotImplementedError
23
24 def response_helper(self, response, **kwargs):
25 """May return response component description as a dict.
26
27 :param dict response: Response fields
28 :param dict kwargs: All additional keywords arguments sent to `APISpec.response()`
29 """
30 raise PluginMethodNotImplementedError
31
32 def parameter_helper(self, parameter, **kwargs):
33 """May return parameter component description as a dict.
34
35 :param dict parameter: Parameter fields
36 :param dict kwargs: All additional keywords arguments sent to `APISpec.parameter()`
37 """
38 raise PluginMethodNotImplementedError
39
40 def path_helper(self, path=None, operations=None, parameters=None, **kwargs):
41 """May return a path as string and mutate operations dict and parameters list.
42
43 :param str path: Path to the resource
44 :param dict operations: A `dict` mapping HTTP methods to operation object. See
45 https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#operationObject
46 :param list parameters: A `list` of parameters objects or references for the path. See
47 https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#parameterObject
48 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()`
50
51 Return value should be a string or None. If a string is returned, it
52 is set as the path.
53
54 The last path helper returning a string sets the path value. Therefore,
55 the order of plugin registration matters. However, generally, registering
56 several plugins that return a path does not make sense.
57 """
58 raise PluginMethodNotImplementedError
59
60 def operation_helper(self, path=None, operations=None, **kwargs):
61 """May mutate operations.
62
63 :param str path: Path to the resource
64 :param dict operations: A `dict` mapping HTTP methods to operation object.
65 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()`
67 """
68 raise PluginMethodNotImplementedError
0 """Various utilities for parsing OpenAPI operations from docstrings and validating against
1 the OpenAPI spec.
2 """
3 import re
4 import json
5
6 from distutils import version
7
8 from apispec import exceptions
9
10
11 COMPONENT_SUBSECTIONS = {
12 2: {
13 "schema": "definitions",
14 "response": "responses",
15 "parameter": "parameters",
16 "security_scheme": "securityDefinitions",
17 },
18 3: {
19 "schema": "schemas",
20 "response": "responses",
21 "parameter": "parameters",
22 "example": "examples",
23 "security_scheme": "securitySchemes",
24 },
25 }
26
27
28 def build_reference(component_type, openapi_major_version, component_name):
29 """Return path to reference
30
31 :param str component_type: Component type (schema, parameter, response, security_scheme)
32 :param int openapi_major_version: OpenAPI major version (2 or 3)
33 :param str component_name: Name of component to reference
34 """
35 return {
36 "$ref": "#/{}{}/{}".format(
37 "components/" if openapi_major_version >= 3 else "",
38 COMPONENT_SUBSECTIONS[openapi_major_version][component_type],
39 component_name,
40 )
41 }
42
43
44 def validate_spec(spec):
45 """Validate the output of an :class:`APISpec` object against the
46 OpenAPI specification.
47
48 Note: Requires installing apispec with the ``[validation]`` extras.
49 ::
50
51 pip install 'apispec[validation]'
52
53 :raise: apispec.exceptions.OpenAPIError if validation fails.
54 """
55 try:
56 import prance
57 except ImportError as error: # re-raise with a more verbose message
58 exc_class = type(error)
59 raise exc_class(
60 "validate_spec requires prance to be installed. "
61 "You can install all validation requirements using:\n"
62 " pip install 'apispec[validation]'"
63 )
64 parser_kwargs = {}
65 if spec.openapi_version.version[0] == 3:
66 parser_kwargs["backend"] = "openapi-spec-validator"
67 try:
68 prance.BaseParser(spec_string=json.dumps(spec.to_dict()), **parser_kwargs)
69 except prance.ValidationError as err:
70 raise exceptions.OpenAPIError(*err.args)
71 else:
72 return True
73
74
75 class OpenAPIVersion(version.LooseVersion):
76 """OpenAPI version
77
78 :param str|OpenAPIVersion openapi_version: OpenAPI version
79
80 Parses an OpenAPI version expressed as string. Provides shortcut to digits
81 (major, minor, patch).
82
83 Example: ::
84
85 ver = OpenAPIVersion('3.0.2')
86 assert ver.major == 3
87 assert ver.minor == 0
88 assert ver.patch == 1
89 assert ver.vstring == '3.0.2'
90 assert str(ver) == '3.0.2'
91 """
92
93 MIN_INCLUSIVE_VERSION = version.LooseVersion("2.0")
94 MAX_EXCLUSIVE_VERSION = version.LooseVersion("4.0")
95
96 def __init__(self, openapi_version):
97 if isinstance(openapi_version, version.LooseVersion):
98 openapi_version = openapi_version.vstring
99 if (
100 not self.MIN_INCLUSIVE_VERSION
101 <= openapi_version
102 < self.MAX_EXCLUSIVE_VERSION
103 ):
104 raise exceptions.APISpecError(
105 "Not a valid OpenAPI version number: {}".format(openapi_version)
106 )
107 super().__init__(openapi_version)
108
109 @property
110 def major(self):
111 return self.version[0]
112
113 @property
114 def minor(self):
115 return self.version[1]
116
117 @property
118 def patch(self):
119 return self.version[2]
120
121
122 # from django.contrib.admindocs.utils
123 def trim_docstring(docstring):
124 """Uniformly trims leading/trailing whitespace from docstrings.
125
126 Based on http://www.python.org/peps/pep-0257.html#handling-docstring-indentation
127 """
128 if not docstring or not docstring.strip():
129 return ""
130 # Convert tabs to spaces and split into lines
131 lines = docstring.expandtabs().splitlines()
132 indent = min(len(line) - len(line.lstrip()) for line in lines if line.lstrip())
133 trimmed = [lines[0].lstrip()] + [line[indent:].rstrip() for line in lines[1:]]
134 return "\n".join(trimmed).strip()
135
136
137 # from rest_framework.utils.formatting
138 def dedent(content):
139 """
140 Remove leading indent from a block of text.
141 Used when generating descriptions from docstrings.
142 Note that python's `textwrap.dedent` doesn't quite cut it,
143 as it fails to dedent multiline docstrings that include
144 unindented text on the initial line.
145 """
146 whitespace_counts = [
147 len(line) - len(line.lstrip(" "))
148 for line in content.splitlines()[1:]
149 if line.lstrip()
150 ]
151
152 # unindent the content if needed
153 if whitespace_counts:
154 whitespace_pattern = "^" + (" " * min(whitespace_counts))
155 content = re.sub(re.compile(whitespace_pattern, re.MULTILINE), "", content)
156
157 return content.strip()
158
159
160 # http://stackoverflow.com/a/8310229
161 def deepupdate(original, update):
162 """Recursively update a dict.
163
164 Subdict's won't be overwritten but also updated.
165 """
166 for key, value in original.items():
167 if key not in update:
168 update[key] = value
169 elif isinstance(value, dict):
170 deepupdate(value, update[key])
171 return update
0 """YAML utilities"""
1
2 from collections import OrderedDict
3 import yaml
4
5 from apispec.utils import trim_docstring, dedent
6
7
8 class YAMLDumper(yaml.Dumper):
9 @staticmethod
10 def _represent_dict(dumper, instance):
11 return dumper.represent_mapping("tag:yaml.org,2002:map", instance.items())
12
13
14 yaml.add_representer(OrderedDict, YAMLDumper._represent_dict, Dumper=YAMLDumper)
15
16
17 def dict_to_yaml(dic):
18 return yaml.dump(dic, Dumper=YAMLDumper)
19
20
21 def load_yaml_from_docstring(docstring):
22 """Loads YAML from docstring."""
23 split_lines = trim_docstring(docstring).split("\n")
24
25 # Cut YAML from rest of docstring
26 for index, line in enumerate(split_lines):
27 line = line.strip()
28 if line.startswith("---"):
29 cut_from = index
30 break
31 else:
32 return {}
33
34 yaml_string = "\n".join(split_lines[cut_from:])
35 yaml_string = dedent(yaml_string)
36 return yaml.safe_load(yaml_string) or {}
37
38
39 PATH_KEYS = {"get", "put", "post", "delete", "options", "head", "patch"}
40
41
42 def load_operations_from_docstring(docstring):
43 """Return a dictionary of OpenAPI operations parsed from a
44 a docstring.
45 """
46 doc_data = load_yaml_from_docstring(docstring)
47 return {
48 key: val
49 for key, val in doc_data.items()
50 if key in PATH_KEYS or key.startswith("x-")
51 }
(New empty file)
0 from collections import namedtuple
1
2 import pytest
3
4 from apispec import APISpec
5 from apispec.ext.marshmallow import MarshmallowPlugin
6
7
8 def make_spec(openapi_version):
9 ma_plugin = MarshmallowPlugin()
10 spec = APISpec(
11 title="Validation",
12 version="0.1",
13 openapi_version=openapi_version,
14 plugins=(ma_plugin,),
15 )
16 return namedtuple("Spec", ("spec", "marshmallow_plugin", "openapi"))(
17 spec, ma_plugin, ma_plugin.converter
18 )
19
20
21 @pytest.fixture(params=("2.0", "3.0.0"))
22 def spec_fixture(request):
23 return make_spec(request.param)
24
25
26 @pytest.fixture(params=("2.0", "3.0.0"))
27 def spec(request):
28 return make_spec(request.param).spec
29
30
31 @pytest.fixture(params=("2.0", "3.0.0"))
32 def openapi(request):
33 spec = make_spec(request.param)
34 return spec.openapi
(New empty file)
0 def setup(spec):
1 spec.old_plugins["tests.plugins.dummy_plugin"]["foo"] = 42
0 from marshmallow import Schema, fields
1
2 from apispec.ext.marshmallow.openapi import MARSHMALLOW_VERSION_INFO
3
4
5 class PetSchema(Schema):
6 description = dict(id="Pet id", name="Pet name", password="Password")
7 id = fields.Int(dump_only=True, description=description["id"])
8 name = fields.Str(
9 required=True,
10 deprecated=False,
11 allowEmptyValue=False,
12 description=description["name"],
13 )
14 password = fields.Str(load_only=True, description=description["password"])
15
16
17 class SampleSchema(Schema):
18 runs = fields.Nested("RunSchema", many=True)
19
20 count = fields.Int()
21
22
23 class RunSchema(Schema):
24 sample = fields.Nested(SampleSchema)
25
26
27 class AnalysisSchema(Schema):
28 sample = fields.Nested(SampleSchema)
29
30
31 class AnalysisWithListSchema(Schema):
32 samples = fields.List(fields.Nested(SampleSchema))
33
34
35 class PatternedObjectSchema(Schema):
36 count = fields.Int(dump_only=True, **{"x-count": 1})
37 count2 = fields.Int(dump_only=True, x_count2=2)
38
39
40 class SelfReferencingSchema(Schema):
41 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))
48
49
50 class OrderedSchema(Schema):
51 field1 = fields.Int()
52 field2 = fields.Int()
53 field3 = fields.Int()
54 field4 = fields.Int()
55 field5 = fields.Int()
56
57 class Meta:
58 ordered = True
59
60
61 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)
67
68
69 class CategorySchema(Schema):
70 id = fields.Int()
71 name = fields.Str(required=True)
72 breed = fields.Str(dump_only=True)
73
74
75 class CustomList(fields.List):
76 pass
77
78
79 class CustomStringField(fields.String):
80 pass
81
82
83 class CustomIntegerField(fields.Integer):
84 pass
0 from collections import OrderedDict
1 from http import HTTPStatus
2
3 import pytest
4 import yaml
5
6 from apispec import APISpec, BasePlugin
7 from apispec.exceptions import (
8 APISpecError,
9 DuplicateComponentNameError,
10 DuplicateParameterError,
11 InvalidParameterError,
12 )
13
14 from .utils import (
15 get_schemas,
16 get_examples,
17 get_paths,
18 get_parameters,
19 get_responses,
20 get_security_schemes,
21 build_ref,
22 )
23
24
25 description = "This is a sample Petstore server. You can find out more "
26 'about Swagger at <a href="http://swagger.wordnik.com">http://swagger.wordnik.com</a> '
27 "or on irc.freenode.net, #swagger. For this sample, you can use the api "
28 'key "special-key" to test the authorization filters'
29
30
31 @pytest.fixture(params=("2.0", "3.0.0"))
32 def spec(request):
33 openapi_version = request.param
34 if openapi_version == "2.0":
35 security_kwargs = {"security": [{"apiKey": []}]}
36 else:
37 security_kwargs = {
38 "components": {
39 "securitySchemes": {
40 "bearerAuth": dict(type="http", scheme="bearer", bearerFormat="JWT")
41 },
42 "schemas": {
43 "ErrorResponse": {
44 "type": "object",
45 "properties": {
46 "ok": {
47 "type": "boolean",
48 "description": "status indicator",
49 "example": False,
50 }
51 },
52 "required": ["ok"],
53 }
54 },
55 }
56 }
57 return APISpec(
58 title="Swagger Petstore",
59 version="1.0.0",
60 openapi_version=openapi_version,
61 info={"description": description},
62 **security_kwargs
63 )
64
65
66 class TestAPISpecInit:
67 def test_raises_wrong_apispec_version(self):
68 message = "Not a valid OpenAPI version number:"
69 with pytest.raises(APISpecError, match=message):
70 APISpec(
71 "Swagger Petstore",
72 version="1.0.0",
73 openapi_version="4.0", # 4.0 is not supported
74 info={"description": description},
75 security=[{"apiKey": []}],
76 )
77
78
79 class TestMetadata:
80 def test_openapi_metadata(self, spec):
81 metadata = spec.to_dict()
82 assert metadata["info"]["title"] == "Swagger Petstore"
83 assert metadata["info"]["version"] == "1.0.0"
84 assert metadata["info"]["description"] == description
85 if spec.openapi_version.major < 3:
86 assert metadata["swagger"] == spec.openapi_version.vstring
87 assert metadata["security"] == [{"apiKey": []}]
88 else:
89 assert metadata["openapi"] == spec.openapi_version.vstring
90 security_schemes = {
91 "bearerAuth": dict(type="http", scheme="bearer", bearerFormat="JWT")
92 }
93 assert metadata["components"]["securitySchemes"] == security_schemes
94 assert metadata["components"]["schemas"].get("ErrorResponse", False)
95 assert metadata["info"]["title"] == "Swagger Petstore"
96 assert metadata["info"]["version"] == "1.0.0"
97 assert metadata["info"]["description"] == description
98
99 @pytest.mark.parametrize("spec", ("3.0.0",), indirect=True)
100 def test_openapi_metadata_merge_v3(self, spec):
101 properties = {
102 "ok": {
103 "type": "boolean",
104 "description": "property description",
105 "example": True,
106 }
107 }
108 spec.components.schema(
109 "definition", {"properties": properties, "description": "description"}
110 )
111 metadata = spec.to_dict()
112 assert metadata["components"]["schemas"].get("ErrorResponse", False)
113 assert metadata["components"]["schemas"].get("definition", False)
114
115
116 class TestTags:
117
118 tag = {
119 "name": "MyTag",
120 "description": "This tag gathers all API endpoints which are mine.",
121 }
122
123 def test_tag(self, spec):
124 spec.tag(self.tag)
125 tags_json = spec.to_dict()["tags"]
126 assert self.tag in tags_json
127
128 def test_tag_is_chainable(self, spec):
129 spec.tag({"name": "tag1"}).tag({"name": "tag2"})
130 assert spec.to_dict()["tags"] == [{"name": "tag1"}, {"name": "tag2"}]
131
132
133 class TestComponents:
134
135 properties = {
136 "id": {"type": "integer", "format": "int64"},
137 "name": {"type": "string", "example": "doggie"},
138 }
139
140 def test_schema(self, spec):
141 spec.components.schema("Pet", {"properties": self.properties})
142 schemas = get_schemas(spec)
143 assert "Pet" in schemas
144 assert schemas["Pet"]["properties"] == self.properties
145
146 def test_schema_is_chainable(self, spec):
147 spec.components.schema("Pet", {"properties": {}}).schema(
148 "Plant", {"properties": {}}
149 )
150 schemas = get_schemas(spec)
151 assert "Pet" in schemas
152 assert "Plant" in schemas
153
154 def test_schema_description(self, spec):
155 model_description = "An animal which lives with humans."
156 spec.components.schema(
157 "Pet", {"properties": self.properties, "description": model_description}
158 )
159 schemas = get_schemas(spec)
160 assert schemas["Pet"]["description"] == model_description
161
162 def test_schema_stores_enum(self, spec):
163 enum = ["name", "photoUrls"]
164 spec.components.schema("Pet", {"properties": self.properties, "enum": enum})
165 schemas = get_schemas(spec)
166 assert schemas["Pet"]["enum"] == enum
167
168 def test_schema_discriminator(self, spec):
169 spec.components.schema(
170 "Pet", {"properties": self.properties, "discriminator": "name"}
171 )
172 schemas = get_schemas(spec)
173 assert schemas["Pet"]["discriminator"] == "name"
174
175 def test_schema_duplicate_name(self, spec):
176 spec.components.schema("Pet", {"properties": self.properties})
177 with pytest.raises(
178 DuplicateComponentNameError,
179 match='Another schema with name "Pet" is already registered.',
180 ):
181 spec.components.schema("Pet", properties=self.properties)
182
183 def test_response(self, spec):
184 response = {"description": "Pet not found"}
185 spec.components.response("NotFound", response)
186 responses = get_responses(spec)
187 assert responses["NotFound"] == response
188
189 def test_response_is_chainable(self, spec):
190 spec.components.response("resp1").response("resp2")
191 responses = get_responses(spec)
192 assert "resp1" in responses
193 assert "resp2" in responses
194
195 def test_response_duplicate_name(self, spec):
196 spec.components.response("test_response")
197 with pytest.raises(
198 DuplicateComponentNameError,
199 match='Another response with name "test_response" is already registered.',
200 ):
201 spec.components.response("test_response")
202
203 def test_parameter(self, spec):
204 parameter = {"format": "int64", "type": "integer"}
205 spec.components.parameter("PetId", "path", parameter)
206 params = get_parameters(spec)
207 assert params["PetId"] == {
208 "format": "int64",
209 "type": "integer",
210 "in": "path",
211 "name": "PetId",
212 "required": True,
213 }
214
215 def test_parameter_is_chainable(self, spec):
216 spec.components.parameter("param1", "path").parameter("param2", "path")
217 params = get_parameters(spec)
218 assert "param1" in params
219 assert "param2" in params
220
221 def test_parameter_duplicate_name(self, spec):
222 spec.components.parameter("test_parameter", "path")
223 with pytest.raises(
224 DuplicateComponentNameError,
225 match='Another parameter with name "test_parameter" is already registered.',
226 ):
227 spec.components.parameter("test_parameter", "path")
228
229 # Referenced examples are only supported in OAS 3.x
230 @pytest.mark.parametrize("spec", ("3.0.0",), indirect=True)
231 def test_example(self, spec):
232 spec.components.example("test_example", {"value": {"a": "b"}})
233 examples = get_examples(spec)
234 assert examples["test_example"]["value"] == {"a": "b"}
235
236 @pytest.mark.parametrize("spec", ("3.0.0",), indirect=True)
237 def test_example_is_chainable(self, spec):
238 spec.components.example("test_example_1", {}).example("test_example_2", {})
239 examples = get_examples(spec)
240 assert "test_example_1" in examples
241 assert "test_example_2" in examples
242
243 @pytest.mark.parametrize("spec", ("3.0.0",), indirect=True)
244 def test_example_duplicate_name(self, spec):
245 spec.components.example("test_example", {})
246 with pytest.raises(
247 DuplicateComponentNameError,
248 match='Another example with name "test_example" is already registered.',
249 ):
250 spec.components.example("test_example", {})
251
252 def test_security_scheme(self, spec):
253 sec_scheme = {"type": "apiKey", "in": "header", "name": "X-API-Key"}
254 spec.components.security_scheme("ApiKeyAuth", sec_scheme)
255 assert get_security_schemes(spec)["ApiKeyAuth"] == sec_scheme
256
257 def test_security_scheme_is_chainable(self, spec):
258 spec.components.security_scheme("sec_1", {}).security_scheme("sec_2", {})
259 security_schemes = get_security_schemes(spec)
260 assert "sec_1" in security_schemes
261 assert "sec_2" in security_schemes
262
263 def test_security_scheme_duplicate_name(self, spec):
264 sec_scheme_1 = {"type": "apiKey", "in": "header", "name": "X-API-Key"}
265 sec_scheme_2 = {"type": "apiKey", "in": "header", "name": "X-API-Key-2"}
266 spec.components.security_scheme("ApiKeyAuth", sec_scheme_1)
267 with pytest.raises(
268 DuplicateComponentNameError,
269 match='Another security scheme with name "ApiKeyAuth" is already registered.',
270 ):
271 spec.components.security_scheme("ApiKeyAuth", sec_scheme_2)
272
273 def test_to_yaml(self, spec):
274 enum = ["name", "photoUrls"]
275 spec.components.schema("Pet", properties=self.properties, enum=enum)
276 assert spec.to_dict() == yaml.safe_load(spec.to_yaml())
277
278 def test_components_can_be_accessed_by_plugin_in_init_spec(self):
279 class TestPlugin(BasePlugin):
280 def init_spec(self, spec):
281 spec.components.schema(
282 "TestSchema",
283 {"properties": {"key": {"type": "string"}}, "type": "object"},
284 )
285
286 spec = APISpec(
287 "Test API", version="0.0.1", openapi_version="2.0", plugins=[TestPlugin()]
288 )
289 assert get_schemas(spec) == {
290 "TestSchema": {"properties": {"key": {"type": "string"}}, "type": "object"}
291 }
292
293
294 class TestPath:
295 paths = {
296 "/pet/{petId}": {
297 "get": {
298 "parameters": [
299 {
300 "required": True,
301 "format": "int64",
302 "name": "petId",
303 "in": "path",
304 "type": "integer",
305 "description": "ID of pet that needs to be fetched",
306 }
307 ],
308 "responses": {
309 "200": {"schema": "Pet", "description": "successful operation"},
310 "400": {"description": "Invalid ID supplied"},
311 "404": {"description": "Pet not found"},
312 },
313 "produces": ["application/json", "application/xml"],
314 "operationId": "getPetById",
315 "summary": "Find pet by ID",
316 "description": (
317 "Returns a pet when ID < 10. "
318 "ID > 10 or nonintegers will simulate API error conditions"
319 ),
320 "tags": ["pet"],
321 }
322 }
323 }
324
325 def test_path(self, spec):
326 route_spec = self.paths["/pet/{petId}"]["get"]
327 spec.path(
328 path="/pet/{petId}",
329 operations=dict(
330 get=dict(
331 parameters=route_spec["parameters"],
332 responses=route_spec["responses"],
333 produces=route_spec["produces"],
334 operationId=route_spec["operationId"],
335 summary=route_spec["summary"],
336 description=route_spec["description"],
337 tags=route_spec["tags"],
338 )
339 ),
340 )
341
342 p = get_paths(spec)["/pet/{petId}"]["get"]
343 assert p["parameters"] == route_spec["parameters"]
344 assert p["responses"] == route_spec["responses"]
345 assert p["operationId"] == route_spec["operationId"]
346 assert p["summary"] == route_spec["summary"]
347 assert p["description"] == route_spec["description"]
348 assert p["tags"] == route_spec["tags"]
349
350 def test_paths_maintain_order(self, spec):
351 spec.path(path="/path1")
352 spec.path(path="/path2")
353 spec.path(path="/path3")
354 spec.path(path="/path4")
355 assert list(spec.to_dict()["paths"].keys()) == [
356 "/path1",
357 "/path2",
358 "/path3",
359 "/path4",
360 ]
361
362 def test_paths_is_chainable(self, spec):
363 spec.path(path="/path1").path("/path2")
364 assert list(spec.to_dict()["paths"].keys()) == ["/path1", "/path2"]
365
366 def test_methods_maintain_order(self, spec):
367 methods = ["get", "post", "put", "patch", "delete", "head", "options"]
368 for method in methods:
369 spec.path(path="/path", operations=OrderedDict({method: {}}))
370 assert list(spec.to_dict()["paths"]["/path"]) == methods
371
372 def test_path_merges_paths(self, spec):
373 """Test that adding a second HTTP method to an existing path performs
374 a merge operation instead of an overwrite"""
375 path = "/pet/{petId}"
376 route_spec = self.paths[path]["get"]
377 spec.path(path=path, operations=dict(get=route_spec))
378 spec.path(
379 path=path,
380 operations=dict(
381 put=dict(
382 parameters=route_spec["parameters"],
383 responses=route_spec["responses"],
384 produces=route_spec["produces"],
385 operationId="updatePet",
386 summary="Updates an existing Pet",
387 description="Use this method to make changes to Pet `petId`",
388 tags=route_spec["tags"],
389 )
390 ),
391 )
392
393 p = get_paths(spec)[path]
394 assert "get" in p
395 assert "put" in p
396
397 @pytest.mark.parametrize("openapi_version", ("2.0", "3.0.0"))
398 def test_path_called_twice_with_same_operations_parameters(self, openapi_version):
399 """Test calling path twice with same operations or parameters
400
401 operations and parameters being mutated by clean_operations and plugin helpers
402 should not make path fail on second call
403 """
404
405 class TestPlugin(BasePlugin):
406 def path_helper(self, path, operations, parameters, **kwargs):
407 """Mutate operations and parameters"""
408 operations.update({"post": {"responses": {"201": "201ResponseRef"}}})
409 parameters.append("ParamRef_3")
410 return path
411
412 spec = APISpec(
413 title="Swagger Petstore",
414 version="1.0.0",
415 openapi_version=openapi_version,
416 plugins=[TestPlugin()],
417 )
418
419 path = "/pet/{petId}"
420 parameters = ["ParamRef_1"]
421 operation = {
422 "parameters": ["ParamRef_2"],
423 "responses": {"200": "200ResponseRef"},
424 }
425
426 spec.path(path=path, operations={"get": operation}, parameters=parameters)
427 spec.path(path=path, operations={"put": operation}, parameters=parameters)
428 operations = (get_paths(spec))[path]
429 assert (
430 operations["get"]
431 == operations["put"]
432 == {
433 "parameters": [build_ref(spec, "parameter", "ParamRef_2")],
434 "responses": {"200": build_ref(spec, "response", "200ResponseRef")},
435 }
436 )
437 assert operations["parameters"] == [
438 build_ref(spec, "parameter", "ParamRef_1"),
439 build_ref(spec, "parameter", "ParamRef_3"),
440 ]
441
442 def test_path_ensures_path_parameters_required(self, spec):
443 path = "/pet/{petId}"
444 spec.path(
445 path=path,
446 operations=dict(put=dict(parameters=[{"name": "petId", "in": "path"}])),
447 )
448 assert get_paths(spec)[path]["put"]["parameters"][0]["required"] is True
449
450 def test_path_with_no_path_raises_error(self, spec):
451 message = "Path template is not specified"
452 with pytest.raises(APISpecError, match=message):
453 spec.path()
454
455 def test_path_summary_description(self, spec):
456 summary = "Operations on a Pet"
457 description = "Operations on a Pet identified by its ID"
458 spec.path(path="/pet/{petId}", summary=summary, description=description)
459
460 p = get_paths(spec)["/pet/{petId}"]
461 assert p["summary"] == summary
462 assert p["description"] == description
463
464 def test_parameter(self, spec):
465 route_spec = self.paths["/pet/{petId}"]["get"]
466
467 spec.components.parameter("test_parameter", "path", route_spec["parameters"][0])
468
469 spec.path(
470 path="/pet/{petId}", operations={"get": {"parameters": ["test_parameter"]}}
471 )
472
473 metadata = spec.to_dict()
474 p = get_paths(spec)["/pet/{petId}"]["get"]
475
476 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
487 @pytest.mark.parametrize(
488 "parameters",
489 ([{"name": "petId"}], [{"in": "path"}]), # missing "in" # missing "name"
490 )
491 def test_invalid_parameter(self, spec, parameters):
492 path = "/pet/{petId}"
493
494 with pytest.raises(InvalidParameterError):
495 spec.path(path=path, operations=dict(put={}, get={}), parameters=parameters)
496
497 def test_parameter_duplicate(self, spec):
498 spec.path(
499 path="/pet/{petId}",
500 operations={
501 "get": {
502 "parameters": [
503 {"name": "petId", "in": "path"},
504 {"name": "petId", "in": "query"},
505 ]
506 }
507 },
508 )
509
510 with pytest.raises(DuplicateParameterError):
511 spec.path(
512 path="/pet/{petId}",
513 operations={
514 "get": {
515 "parameters": [
516 {"name": "petId", "in": "path"},
517 {"name": "petId", "in": "path"},
518 ]
519 }
520 },
521 )
522
523 def test_global_parameters(self, spec):
524 path = "/pet/{petId}"
525 route_spec = self.paths["/pet/{petId}"]["get"]
526
527 spec.components.parameter("test_parameter", "path", route_spec["parameters"][0])
528 spec.path(
529 path=path,
530 operations=dict(put={}, get={}),
531 parameters=[{"name": "petId", "in": "path"}, "test_parameter"],
532 )
533
534 assert get_paths(spec)[path]["parameters"] == [
535 {"name": "petId", "in": "path", "required": True},
536 build_ref(spec, "parameter", "test_parameter"),
537 ]
538
539 def test_global_parameter_duplicate(self, spec):
540 path = "/pet/{petId}"
541 spec.path(
542 path=path,
543 operations=dict(put={}, get={}),
544 parameters=[
545 {"name": "petId", "in": "path"},
546 {"name": "petId", "in": "query"},
547 ],
548 )
549
550 assert get_paths(spec)[path]["parameters"] == [
551 {"name": "petId", "in": "path", "required": True},
552 {"name": "petId", "in": "query"},
553 ]
554
555 with pytest.raises(DuplicateParameterError):
556 spec.path(
557 path=path,
558 operations=dict(put={}, get={}),
559 parameters=[
560 {"name": "petId", "in": "path"},
561 {"name": "petId", "in": "path"},
562 "test_parameter",
563 ],
564 )
565
566 def test_response(self, spec):
567 route_spec = self.paths["/pet/{petId}"]["get"]
568
569 spec.components.response("test_response", route_spec["responses"]["200"])
570
571 spec.path(
572 path="/pet/{petId}",
573 operations={"get": {"responses": {"200": "test_response"}}},
574 )
575
576 metadata = spec.to_dict()
577 p = get_paths(spec)["/pet/{petId}"]["get"]
578
579 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):
591 code = HTTPStatus(200)
592 spec.path(
593 path="/pet/{petId}",
594 operations={"get": {"responses": {code: "test_response"}}},
595 )
596
597 assert "200" in get_paths(spec)["/pet/{petId}"]["get"]["responses"]
598
599 def test_response_with_status_code_range(self, spec, recwarn):
600 status_code = "2XX"
601
602 spec.path(
603 path="/pet/{petId}",
604 operations={"get": {"responses": {status_code: "test_response"}}},
605 )
606
607 if spec.openapi_version.major < 3:
608 assert len(recwarn) == 1
609 assert recwarn.pop(UserWarning)
610
611 assert status_code in get_paths(spec)["/pet/{petId}"]["get"]["responses"]
612
613 def test_path_check_invalid_http_method(self, spec):
614 spec.path("/pet/{petId}", operations={"get": {}})
615 spec.path("/pet/{petId}", operations={"x-dummy": {}})
616 message = "One or more HTTP methods are invalid"
617 with pytest.raises(APISpecError, match=message):
618 spec.path("/pet/{petId}", operations={"dummy": {}})
619
620
621 class TestPlugins:
622 @staticmethod
623 def test_plugin_factory(return_none=False):
624 class TestPlugin(BasePlugin):
625 def schema_helper(self, name, definition, **kwargs):
626 if not return_none:
627 return {"properties": {"name": {"type": "string"}}}
628
629 def parameter_helper(self, parameter, **kwargs):
630 if not return_none:
631 return {"description": "some parameter"}
632
633 def response_helper(self, response, **kwargs):
634 if not return_none:
635 return {"description": "42"}
636
637 def path_helper(self, path, operations, parameters, **kwargs):
638 if not return_none:
639 if path == "/path_1":
640 operations.update({"get": {"responses": {"200": {}}}})
641 parameters.append({"name": "page", "in": "query"})
642 return "/path_1_modified"
643
644 def operation_helper(self, path, operations, **kwargs):
645 if path == "/path_2":
646 operations["post"] = {"responses": {"201": {}}}
647
648 return TestPlugin()
649
650 @pytest.mark.parametrize("openapi_version", ("2.0", "3.0.0"))
651 @pytest.mark.parametrize("return_none", (True, False))
652 def test_plugin_schema_helper_is_used(self, openapi_version, return_none):
653 spec = APISpec(
654 title="Swagger Petstore",
655 version="1.0.0",
656 openapi_version=openapi_version,
657 plugins=(self.test_plugin_factory(return_none),),
658 )
659 spec.components.schema("Pet")
660 definitions = get_schemas(spec)
661 if return_none:
662 assert definitions["Pet"] == {}
663 else:
664 assert definitions["Pet"] == {"properties": {"name": {"type": "string"}}}
665
666 @pytest.mark.parametrize("openapi_version", ("2.0", "3.0.0"))
667 @pytest.mark.parametrize("return_none", (True, False))
668 def test_plugin_parameter_helper_is_used(self, openapi_version, return_none):
669 spec = APISpec(
670 title="Swagger Petstore",
671 version="1.0.0",
672 openapi_version=openapi_version,
673 plugins=(self.test_plugin_factory(return_none),),
674 )
675 spec.components.parameter("Pet", "body", {})
676 parameters = get_parameters(spec)
677 if return_none:
678 assert parameters["Pet"] == {"in": "body", "name": "Pet"}
679 else:
680 assert parameters["Pet"] == {
681 "in": "body",
682 "name": "Pet",
683 "description": "some parameter",
684 }
685
686 @pytest.mark.parametrize("openapi_version", ("2.0", "3.0.0"))
687 @pytest.mark.parametrize("return_none", (True, False))
688 def test_plugin_response_helper_is_used(self, openapi_version, return_none):
689 spec = APISpec(
690 title="Swagger Petstore",
691 version="1.0.0",
692 openapi_version=openapi_version,
693 plugins=(self.test_plugin_factory(return_none),),
694 )
695 spec.components.response("Pet", {})
696 responses = get_responses(spec)
697 if return_none:
698 assert responses["Pet"] == {}
699 else:
700 assert responses["Pet"] == {"description": "42"}
701
702 @pytest.mark.parametrize("openapi_version", ("2.0", "3.0.0"))
703 @pytest.mark.parametrize("return_none", (True, False))
704 def test_plugin_path_helper_is_used(self, openapi_version, return_none):
705 spec = APISpec(
706 title="Swagger Petstore",
707 version="1.0.0",
708 openapi_version=openapi_version,
709 plugins=(self.test_plugin_factory(return_none),),
710 )
711 spec.path("/path_1")
712 paths = get_paths(spec)
713 assert len(paths) == 1
714 if return_none:
715 assert paths["/path_1"] == {}
716 else:
717 assert paths["/path_1_modified"] == {
718 "get": {"responses": {"200": {}}},
719 "parameters": [{"in": "query", "name": "page"}],
720 }
721
722 @pytest.mark.parametrize("openapi_version", ("2.0", "3.0.0"))
723 def test_plugin_operation_helper_is_used(self, openapi_version):
724 spec = APISpec(
725 title="Swagger Petstore",
726 version="1.0.0",
727 openapi_version=openapi_version,
728 plugins=(self.test_plugin_factory(),),
729 )
730 spec.path("/path_2", operations={"post": {"responses": {"200": {}}}})
731 paths = get_paths(spec)
732 assert len(paths) == 1
733 assert paths["/path_2"] == {"post": {"responses": {"201": {}}}}
734
735
736 class TestPluginsOrder:
737 class OrderedPlugin(BasePlugin):
738 def __init__(self, index, output):
739 super(TestPluginsOrder.OrderedPlugin, self).__init__()
740 self.index = index
741 self.output = output
742
743 def path_helper(self, path, operations, **kwargs):
744 self.output.append("plugin_{}_path".format(self.index))
745
746 def operation_helper(self, path, operations, **kwargs):
747 self.output.append("plugin_{}_operations".format(self.index))
748
749 def test_plugins_order(self):
750 """Test plugins execution order in APISpec.path
751
752 - All path helpers are called, then all operation helpers, then all response helpers.
753 - At each step, helpers are executed in the order the plugins are passed to APISpec.
754 """
755 output = []
756 spec = APISpec(
757 title="Swagger Petstore",
758 version="1.0.0",
759 openapi_version="3.0.0",
760 plugins=(self.OrderedPlugin(1, output), self.OrderedPlugin(2, output)),
761 )
762 spec.path("/path", operations={"get": {"responses": {200: {}}}})
763 assert output == [
764 "plugin_1_path",
765 "plugin_2_path",
766 "plugin_1_operations",
767 "plugin_2_operations",
768 ]
0 import json
1
2 import pytest
3
4 from marshmallow.fields import Field, DateTime, Dict, String, Nested, List
5 from marshmallow import Schema
6
7 from apispec import APISpec
8 from apispec.ext.marshmallow import MarshmallowPlugin
9 from apispec.ext.marshmallow.openapi import MARSHMALLOW_VERSION_INFO
10 from apispec.ext.marshmallow import common
11 from apispec.exceptions import APISpecError
12 from .schemas import (
13 PetSchema,
14 AnalysisSchema,
15 RunSchema,
16 SelfReferencingSchema,
17 OrderedSchema,
18 PatternedObjectSchema,
19 DefaultValuesSchema,
20 AnalysisWithListSchema,
21 )
22
23 from .utils import get_schemas, get_parameters, get_responses, get_paths, build_ref
24
25
26 class TestDefinitionHelper:
27 @pytest.mark.parametrize("schema", [PetSchema, PetSchema()])
28 def test_can_use_schema_as_definition(self, spec, schema):
29 spec.components.schema("Pet", schema=schema)
30 definitions = get_schemas(spec)
31 props = definitions["Pet"]["properties"]
32
33 assert props["id"]["type"] == "integer"
34 assert props["name"]["type"] == "string"
35
36 def test_schema_helper_without_schema(self, spec):
37 spec.components.schema("Pet", {"properties": {"key": {"type": "integer"}}})
38 definitions = get_schemas(spec)
39 assert definitions["Pet"]["properties"] == {"key": {"type": "integer"}}
40
41 @pytest.mark.parametrize("schema", [AnalysisSchema, AnalysisSchema()])
42 def test_resolve_schema_dict_auto_reference(self, schema):
43 def resolver(schema):
44 schema_cls = common.resolve_schema_cls(schema)
45 return schema_cls.__name__
46
47 spec = APISpec(
48 title="Test auto-reference",
49 version="0.1",
50 openapi_version="2.0",
51 plugins=(MarshmallowPlugin(schema_name_resolver=resolver),),
52 )
53 with pytest.raises(KeyError):
54 get_schemas(spec)
55
56 spec.components.schema("analysis", schema=schema)
57 spec.path(
58 "/test",
59 operations={
60 "get": {
61 "responses": {
62 "200": {"schema": build_ref(spec, "schema", "analysis")}
63 }
64 }
65 },
66 )
67 definitions = get_schemas(spec)
68 assert 3 == len(definitions)
69
70 assert "analysis" in definitions
71 assert "SampleSchema" in definitions
72 assert "RunSchema" in definitions
73
74 @pytest.mark.parametrize(
75 "schema", [AnalysisWithListSchema, AnalysisWithListSchema()]
76 )
77 def test_resolve_schema_dict_auto_reference_in_list(self, schema):
78 def resolver(schema):
79 schema_cls = common.resolve_schema_cls(schema)
80 return schema_cls.__name__
81
82 spec = APISpec(
83 title="Test auto-reference",
84 version="0.1",
85 openapi_version="2.0",
86 plugins=(MarshmallowPlugin(schema_name_resolver=resolver),),
87 )
88 with pytest.raises(KeyError):
89 get_schemas(spec)
90
91 spec.components.schema("analysis", schema=schema)
92 spec.path(
93 "/test",
94 operations={
95 "get": {
96 "responses": {
97 "200": {"schema": build_ref(spec, "schema", "analysis")}
98 }
99 }
100 },
101 )
102 definitions = get_schemas(spec)
103 assert 3 == len(definitions)
104
105 assert "analysis" in definitions
106 assert "SampleSchema" in definitions
107 assert "RunSchema" in definitions
108
109 @pytest.mark.parametrize("schema", [AnalysisSchema, AnalysisSchema()])
110 def test_resolve_schema_dict_auto_reference_return_none(self, schema):
111 def resolver(schema):
112 return None
113
114 spec = APISpec(
115 title="Test auto-reference",
116 version="0.1",
117 openapi_version="2.0",
118 plugins=(MarshmallowPlugin(schema_name_resolver=resolver),),
119 )
120 with pytest.raises(KeyError):
121 get_schemas(spec)
122
123 with pytest.raises(
124 APISpecError, match="Name resolver returned None for schema"
125 ):
126 spec.components.schema("analysis", schema=schema)
127
128 @pytest.mark.parametrize("schema", [AnalysisSchema, AnalysisSchema()])
129 def test_warning_when_schema_added_twice(self, spec, schema):
130 spec.components.schema("Analysis", schema=schema)
131 with pytest.warns(UserWarning, match="has already been added to the spec"):
132 spec.components.schema("DuplicateAnalysis", schema=schema)
133
134 def test_schema_instances_with_different_modifiers_added(self, spec):
135 class MultiModifierSchema(Schema):
136 pet_unmodified = Nested(PetSchema)
137 pet_exclude = Nested(PetSchema, exclude=("name",))
138
139 spec.components.schema("Pet", schema=PetSchema())
140 spec.components.schema("Pet_Exclude", schema=PetSchema(exclude=("name",)))
141
142 spec.components.schema("MultiModifierSchema", schema=MultiModifierSchema)
143
144 definitions = get_schemas(spec)
145 pet_unmodified_ref = definitions["MultiModifierSchema"]["properties"][
146 "pet_unmodified"
147 ]
148 assert pet_unmodified_ref == build_ref(spec, "schema", "Pet")
149
150 pet_exclude = definitions["MultiModifierSchema"]["properties"]["pet_exclude"]
151 assert pet_exclude == build_ref(spec, "schema", "Pet_Exclude")
152
153 def test_schema_instance_with_different_modifers_custom_resolver(self):
154 class MultiModifierSchema(Schema):
155 pet_unmodified = Nested(PetSchema)
156 pet_exclude = Nested(PetSchema(partial=True))
157
158 def resolver(schema):
159 schema_instance = common.resolve_schema_instance(schema)
160 prefix = "Partial-" if schema_instance.partial else ""
161 schema_cls = common.resolve_schema_cls(schema)
162 name = prefix + schema_cls.__name__
163 if name.endswith("Schema"):
164 return name[:-6] or name
165 return name
166
167 spec = APISpec(
168 title="Test Custom Resolver for Partial",
169 version="0.1",
170 openapi_version="2.0",
171 plugins=(MarshmallowPlugin(schema_name_resolver=resolver),),
172 )
173
174 with pytest.warns(None) as record:
175 spec.components.schema("NameClashSchema", schema=MultiModifierSchema)
176
177 assert len(record) == 0
178
179 def test_schema_with_clashing_names(self, spec):
180 class Pet(PetSchema):
181 another_field = String()
182
183 class NameClashSchema(Schema):
184 pet_1 = Nested(PetSchema)
185 pet_2 = Nested(Pet)
186
187 with pytest.warns(
188 UserWarning, match="Multiple schemas resolved to the name Pet"
189 ):
190 spec.components.schema("NameClashSchema", schema=NameClashSchema)
191
192 definitions = get_schemas(spec)
193
194 assert "Pet" in definitions
195 assert "Pet1" in definitions
196
197 def test_resolve_nested_schema_many_true_resolver_return_none(self):
198 def resolver(schema):
199 return None
200
201 class PetFamilySchema(Schema):
202 pets_1 = Nested(PetSchema, many=True)
203 pets_2 = List(Nested(PetSchema))
204
205 spec = APISpec(
206 title="Test auto-reference",
207 version="0.1",
208 openapi_version="2.0",
209 plugins=(MarshmallowPlugin(schema_name_resolver=resolver),),
210 )
211
212 spec.components.schema("PetFamily", schema=PetFamilySchema)
213 props = get_schemas(spec)["PetFamily"]["properties"]
214 pets_1 = props["pets_1"]
215 pets_2 = props["pets_2"]
216 assert pets_1["type"] == pets_2["type"] == "array"
217
218
219 class TestComponentParameterHelper:
220 @pytest.mark.parametrize("schema", [PetSchema, PetSchema()])
221 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}}}
226 spec.components.parameter("Pet", "body", param)
227 parameter = get_parameters(spec)["Pet"]
228 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 assert reference == build_ref(spec, "schema", "Pet")
234
235 resolved_schema = spec.components._schemas["Pet"]
236 assert resolved_schema["properties"]["name"]["type"] == "string"
237 assert resolved_schema["properties"]["password"]["type"] == "string"
238 assert resolved_schema["properties"]["id"]["type"] == "integer"
239
240
241 class TestComponentResponseHelper:
242 @pytest.mark.parametrize("schema", [PetSchema, PetSchema()])
243 def test_can_use_schema_in_response(self, spec, schema):
244 if spec.openapi_version.major < 3:
245 resp = {"schema": schema}
246 else:
247 resp = {"content": {"application/json": {"schema": schema}}}
248 spec.components.response("GetPetOk", resp)
249 response = get_responses(spec)["GetPetOk"]
250 if spec.openapi_version.major < 3:
251 reference = response["schema"]
252 else:
253 reference = response["content"]["application/json"]["schema"]
254 assert reference == build_ref(spec, "schema", "Pet")
255
256 resolved_schema = spec.components._schemas["Pet"]
257 assert resolved_schema["properties"]["id"]["type"] == "integer"
258 assert resolved_schema["properties"]["name"]["type"] == "string"
259 assert resolved_schema["properties"]["password"]["type"] == "string"
260
261 @pytest.mark.parametrize("schema", [PetSchema, PetSchema()])
262 def test_can_use_schema_in_response_header(self, spec, schema):
263 resp = {"headers": {"PetHeader": {"schema": schema}}}
264 spec.components.response("GetPetOk", resp)
265 response = get_responses(spec)["GetPetOk"]
266 reference = response["headers"]["PetHeader"]["schema"]
267 assert reference == build_ref(spec, "schema", "Pet")
268
269 resolved_schema = spec.components._schemas["Pet"]
270 assert resolved_schema["properties"]["id"]["type"] == "integer"
271 assert resolved_schema["properties"]["name"]["type"] == "string"
272 assert resolved_schema["properties"]["password"]["type"] == "string"
273
274 @pytest.mark.parametrize("spec", ("3.0.0",), indirect=True)
275 def test_content_without_schema(self, spec):
276 resp = {"content": {"application/json": {"example": {"name": "Example"}}}}
277 spec.components.response("GetPetOk", resp)
278 response = get_responses(spec)["GetPetOk"]
279 assert response == resp
280
281
282 class TestCustomField:
283 def test_can_use_custom_field_decorator(self, spec_fixture):
284 @spec_fixture.marshmallow_plugin.map_to_openapi_type(DateTime)
285 class CustomNameA(Field):
286 pass
287
288 @spec_fixture.marshmallow_plugin.map_to_openapi_type("integer", "int32")
289 class CustomNameB(Field):
290 pass
291
292 with pytest.raises(TypeError):
293
294 @spec_fixture.marshmallow_plugin.map_to_openapi_type("integer")
295 class BadCustomField(Field):
296 pass
297
298 class CustomPetASchema(PetSchema):
299 name = CustomNameA()
300
301 class CustomPetBSchema(PetSchema):
302 name = CustomNameB()
303
304 spec_fixture.spec.components.schema("Pet", schema=PetSchema)
305 spec_fixture.spec.components.schema("CustomPetA", schema=CustomPetASchema)
306 spec_fixture.spec.components.schema("CustomPetB", schema=CustomPetBSchema)
307
308 props_0 = get_schemas(spec_fixture.spec)["Pet"]["properties"]
309 props_a = get_schemas(spec_fixture.spec)["CustomPetA"]["properties"]
310 props_b = get_schemas(spec_fixture.spec)["CustomPetB"]["properties"]
311
312 assert props_0["name"]["type"] == "string"
313 assert "format" not in props_0["name"]
314
315 assert props_a["name"]["type"] == "string"
316 assert props_a["name"]["format"] == "date-time"
317
318 assert props_b["name"]["type"] == "integer"
319 assert props_b["name"]["format"] == "int32"
320
321
322 def get_nested_schema(schema, field_name):
323 try:
324 return schema._declared_fields[field_name]._schema
325 except AttributeError:
326 return schema._declared_fields[field_name]._Nested__schema
327
328
329 class TestOperationHelper:
330 @pytest.mark.parametrize(
331 "pet_schema",
332 (PetSchema, PetSchema(), PetSchema(many=True), "tests.schemas.PetSchema"),
333 )
334 @pytest.mark.parametrize("spec_fixture", ("2.0",), indirect=True)
335 def test_schema_v2(self, spec_fixture, pet_schema):
336 spec_fixture.spec.path(
337 path="/pet",
338 operations={
339 "get": {
340 "responses": {
341 200: {
342 "schema": pet_schema,
343 "description": "successful operation",
344 "headers": {"PetHeader": {"schema": pet_schema}},
345 }
346 }
347 }
348 },
349 )
350 get = get_paths(spec_fixture.spec)["/pet"]["get"]
351 if isinstance(pet_schema, Schema) and pet_schema.many is True:
352 assert get["responses"]["200"]["schema"]["type"] == "array"
353 schema_reference = get["responses"]["200"]["schema"]["items"]
354 assert (
355 get["responses"]["200"]["headers"]["PetHeader"]["schema"]["type"]
356 == "array"
357 )
358 header_reference = get["responses"]["200"]["headers"]["PetHeader"][
359 "schema"
360 ]["items"]
361 else:
362 schema_reference = get["responses"]["200"]["schema"]
363 header_reference = get["responses"]["200"]["headers"]["PetHeader"]["schema"]
364 assert schema_reference == build_ref(spec_fixture.spec, "schema", "Pet")
365 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"]
368 assert resolved_schema == spec_fixture.openapi.schema2jsonschema(PetSchema)
369 assert get["responses"]["200"]["description"] == "successful operation"
370
371 @pytest.mark.parametrize(
372 "pet_schema",
373 (PetSchema, PetSchema(), PetSchema(many=True), "tests.schemas.PetSchema"),
374 )
375 @pytest.mark.parametrize("spec_fixture", ("3.0.0",), indirect=True)
376 def test_schema_v3(self, spec_fixture, pet_schema):
377 spec_fixture.spec.path(
378 path="/pet",
379 operations={
380 "get": {
381 "responses": {
382 200: {
383 "content": {"application/json": {"schema": pet_schema}},
384 "description": "successful operation",
385 "headers": {"PetHeader": {"schema": pet_schema}},
386 }
387 }
388 }
389 },
390 )
391 get = get_paths(spec_fixture.spec)["/pet"]["get"]
392 if isinstance(pet_schema, Schema) and pet_schema.many is True:
393 assert (
394 get["responses"]["200"]["content"]["application/json"]["schema"]["type"]
395 == "array"
396 )
397 schema_reference = get["responses"]["200"]["content"]["application/json"][
398 "schema"
399 ]["items"]
400 assert (
401 get["responses"]["200"]["headers"]["PetHeader"]["schema"]["type"]
402 == "array"
403 )
404 header_reference = get["responses"]["200"]["headers"]["PetHeader"][
405 "schema"
406 ]["items"]
407 else:
408 schema_reference = get["responses"]["200"]["content"]["application/json"][
409 "schema"
410 ]
411 header_reference = get["responses"]["200"]["headers"]["PetHeader"]["schema"]
412
413 assert schema_reference == build_ref(spec_fixture.spec, "schema", "Pet")
414 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"]
417 assert resolved_schema == spec_fixture.openapi.schema2jsonschema(PetSchema)
418 assert get["responses"]["200"]["description"] == "successful operation"
419
420 @pytest.mark.parametrize("spec_fixture", ("2.0",), indirect=True)
421 def test_schema_expand_parameters_v2(self, spec_fixture):
422 spec_fixture.spec.path(
423 path="/pet",
424 operations={
425 "get": {"parameters": [{"in": "query", "schema": PetSchema}]},
426 "post": {
427 "parameters": [
428 {
429 "in": "body",
430 "description": "a pet schema",
431 "required": True,
432 "name": "pet",
433 "schema": PetSchema,
434 }
435 ]
436 },
437 },
438 )
439 p = get_paths(spec_fixture.spec)["/pet"]
440 get = p["get"]
441 assert get["parameters"] == spec_fixture.openapi.schema2parameters(
442 PetSchema(), default_in="query"
443 )
444 post = p["post"]
445 assert post["parameters"] == spec_fixture.openapi.schema2parameters(
446 PetSchema,
447 default_in="body",
448 required=True,
449 name="pet",
450 description="a pet schema",
451 )
452
453 @pytest.mark.parametrize("spec_fixture", ("3.0.0",), indirect=True)
454 def test_schema_expand_parameters_v3(self, spec_fixture):
455 spec_fixture.spec.path(
456 path="/pet",
457 operations={
458 "get": {"parameters": [{"in": "query", "schema": PetSchema}]},
459 "post": {
460 "requestBody": {
461 "description": "a pet schema",
462 "required": True,
463 "content": {"application/json": {"schema": PetSchema}},
464 }
465 },
466 },
467 )
468 p = get_paths(spec_fixture.spec)["/pet"]
469 get = p["get"]
470 assert get["parameters"] == spec_fixture.openapi.schema2parameters(
471 PetSchema(), default_in="query"
472 )
473 for parameter in get["parameters"]:
474 description = parameter.get("description", False)
475 assert description
476 name = parameter["name"]
477 assert description == PetSchema.description[name]
478 post = p["post"]
479 post_schema = spec_fixture.marshmallow_plugin.resolver.resolve_schema_dict(
480 PetSchema
481 )
482 assert (
483 post["requestBody"]["content"]["application/json"]["schema"] == post_schema
484 )
485 assert post["requestBody"]["description"] == "a pet schema"
486 assert post["requestBody"]["required"]
487
488 @pytest.mark.parametrize("spec_fixture", ("2.0",), indirect=True)
489 def test_schema_uses_ref_if_available_v2(self, spec_fixture):
490 spec_fixture.spec.components.schema("Pet", schema=PetSchema)
491 spec_fixture.spec.path(
492 path="/pet", operations={"get": {"responses": {200: {"schema": PetSchema}}}}
493 )
494 get = get_paths(spec_fixture.spec)["/pet"]["get"]
495 assert get["responses"]["200"]["schema"] == build_ref(
496 spec_fixture.spec, "schema", "Pet"
497 )
498
499 @pytest.mark.parametrize("spec_fixture", ("3.0.0",), indirect=True)
500 def test_schema_uses_ref_if_available_v3(self, spec_fixture):
501 spec_fixture.spec.components.schema("Pet", schema=PetSchema)
502 spec_fixture.spec.path(
503 path="/pet",
504 operations={
505 "get": {
506 "responses": {
507 200: {"content": {"application/json": {"schema": PetSchema}}}
508 }
509 }
510 },
511 )
512 get = get_paths(spec_fixture.spec)["/pet"]["get"]
513 assert get["responses"]["200"]["content"]["application/json"][
514 "schema"
515 ] == build_ref(spec_fixture.spec, "schema", "Pet")
516
517 def test_schema_uses_ref_if_available_name_resolver_returns_none_v2(self):
518 def resolver(schema):
519 return None
520
521 spec = APISpec(
522 title="Test auto-reference",
523 version="0.1",
524 openapi_version="2.0",
525 plugins=(MarshmallowPlugin(schema_name_resolver=resolver),),
526 )
527 spec.components.schema("Pet", schema=PetSchema)
528 spec.path(
529 path="/pet", operations={"get": {"responses": {200: {"schema": PetSchema}}}}
530 )
531 get = get_paths(spec)["/pet"]["get"]
532 assert get["responses"]["200"]["schema"] == build_ref(spec, "schema", "Pet")
533
534 def test_schema_uses_ref_if_available_name_resolver_returns_none_v3(self):
535 def resolver(schema):
536 return None
537
538 spec = APISpec(
539 title="Test auto-reference",
540 version="0.1",
541 openapi_version="3.0.0",
542 plugins=(MarshmallowPlugin(schema_name_resolver=resolver),),
543 )
544 spec.components.schema("Pet", schema=PetSchema)
545 spec.path(
546 path="/pet",
547 operations={
548 "get": {
549 "responses": {
550 200: {"content": {"application/json": {"schema": PetSchema}}}
551 }
552 }
553 },
554 )
555 get = get_paths(spec)["/pet"]["get"]
556 assert get["responses"]["200"]["content"]["application/json"][
557 "schema"
558 ] == build_ref(spec, "schema", "Pet")
559
560 @pytest.mark.parametrize(
561 "pet_schema", (PetSchema, PetSchema(), "tests.schemas.PetSchema"),
562 )
563 def test_schema_name_resolver_returns_none_v2(self, pet_schema):
564 def resolver(schema):
565 return None
566
567 spec = APISpec(
568 title="Test resolver returns None",
569 version="0.1",
570 openapi_version="2.0",
571 plugins=(MarshmallowPlugin(schema_name_resolver=resolver),),
572 )
573 spec.path(
574 path="/pet",
575 operations={"get": {"responses": {200: {"schema": pet_schema}}}},
576 )
577 get = get_paths(spec)["/pet"]["get"]
578 assert "properties" in get["responses"]["200"]["schema"]
579
580 @pytest.mark.parametrize(
581 "pet_schema", (PetSchema, PetSchema(), "tests.schemas.PetSchema"),
582 )
583 def test_schema_name_resolver_returns_none_v3(self, pet_schema):
584 def resolver(schema):
585 return None
586
587 spec = APISpec(
588 title="Test resolver returns None",
589 version="0.1",
590 openapi_version="3.0.0",
591 plugins=(MarshmallowPlugin(schema_name_resolver=resolver),),
592 )
593 spec.path(
594 path="/pet",
595 operations={
596 "get": {
597 "responses": {
598 200: {"content": {"application/json": {"schema": pet_schema}}}
599 }
600 }
601 },
602 )
603 get = get_paths(spec)["/pet"]["get"]
604 assert (
605 "properties"
606 in get["responses"]["200"]["content"]["application/json"]["schema"]
607 )
608
609 @pytest.mark.parametrize("spec_fixture", ("2.0",), indirect=True)
610 def test_schema_uses_ref_in_parameters_and_request_body_if_available_v2(
611 self, spec_fixture
612 ):
613 spec_fixture.spec.components.schema("Pet", schema=PetSchema)
614 spec_fixture.spec.path(
615 path="/pet",
616 operations={
617 "get": {"parameters": [{"in": "query", "schema": PetSchema}]},
618 "post": {"parameters": [{"in": "body", "schema": PetSchema}]},
619 },
620 )
621 p = get_paths(spec_fixture.spec)["/pet"]
622 assert "schema" not in p["get"]["parameters"][0]
623 post = p["post"]
624 assert len(post["parameters"]) == 1
625 assert post["parameters"][0]["schema"] == build_ref(
626 spec_fixture.spec, "schema", "Pet"
627 )
628
629 @pytest.mark.parametrize("spec_fixture", ("3.0.0",), indirect=True)
630 def test_schema_uses_ref_in_parameters_and_request_body_if_available_v3(
631 self, spec_fixture
632 ):
633 spec_fixture.spec.components.schema("Pet", schema=PetSchema)
634 spec_fixture.spec.path(
635 path="/pet",
636 operations={
637 "get": {"parameters": [{"in": "query", "schema": PetSchema}]},
638 "post": {
639 "requestBody": {
640 "content": {"application/json": {"schema": PetSchema}}
641 }
642 },
643 },
644 )
645 p = get_paths(spec_fixture.spec)["/pet"]
646 assert "schema" in p["get"]["parameters"][0]
647 post = p["post"]
648 schema_ref = post["requestBody"]["content"]["application/json"]["schema"]
649 assert schema_ref == build_ref(spec_fixture.spec, "schema", "Pet")
650
651 @pytest.mark.parametrize("spec_fixture", ("2.0",), indirect=True)
652 def test_schema_array_uses_ref_if_available_v2(self, spec_fixture):
653 spec_fixture.spec.components.schema("Pet", schema=PetSchema)
654 spec_fixture.spec.path(
655 path="/pet",
656 operations={
657 "get": {
658 "parameters": [
659 {
660 "name": "petSchema",
661 "in": "body",
662 "schema": {"type": "array", "items": PetSchema},
663 }
664 ],
665 "responses": {
666 200: {"schema": {"type": "array", "items": PetSchema}}
667 },
668 }
669 },
670 )
671 get = get_paths(spec_fixture.spec)["/pet"]["get"]
672 assert len(get["parameters"]) == 1
673 resolved_schema = {
674 "type": "array",
675 "items": build_ref(spec_fixture.spec, "schema", "Pet"),
676 }
677 assert get["parameters"][0]["schema"] == resolved_schema
678 assert get["responses"]["200"]["schema"] == resolved_schema
679
680 @pytest.mark.parametrize("spec_fixture", ("3.0.0",), indirect=True)
681 def test_schema_array_uses_ref_if_available_v3(self, spec_fixture):
682 spec_fixture.spec.components.schema("Pet", schema=PetSchema)
683 spec_fixture.spec.path(
684 path="/pet",
685 operations={
686 "get": {
687 "parameters": [
688 {
689 "name": "Pet",
690 "in": "query",
691 "content": {
692 "application/json": {
693 "schema": {"type": "array", "items": PetSchema}
694 }
695 },
696 }
697 ],
698 "responses": {
699 200: {
700 "content": {
701 "application/json": {
702 "schema": {"type": "array", "items": PetSchema}
703 }
704 }
705 }
706 },
707 }
708 },
709 )
710 get = get_paths(spec_fixture.spec)["/pet"]["get"]
711 assert len(get["parameters"]) == 1
712 resolved_schema = {
713 "type": "array",
714 "items": build_ref(spec_fixture.spec, "schema", "Pet"),
715 }
716 request_schema = get["parameters"][0]["content"]["application/json"]["schema"]
717 assert request_schema == resolved_schema
718 response_schema = get["responses"]["200"]["content"]["application/json"][
719 "schema"
720 ]
721 assert response_schema == resolved_schema
722
723 @pytest.mark.parametrize("spec_fixture", ("2.0",), indirect=True)
724 def test_schema_partially_v2(self, spec_fixture):
725 spec_fixture.spec.components.schema("Pet", schema=PetSchema)
726 spec_fixture.spec.path(
727 path="/parents",
728 operations={
729 "get": {
730 "responses": {
731 200: {
732 "schema": {
733 "type": "object",
734 "properties": {
735 "mother": PetSchema,
736 "father": PetSchema,
737 },
738 }
739 }
740 }
741 }
742 },
743 )
744 get = get_paths(spec_fixture.spec)["/parents"]["get"]
745 assert get["responses"]["200"]["schema"] == {
746 "type": "object",
747 "properties": {
748 "mother": build_ref(spec_fixture.spec, "schema", "Pet"),
749 "father": build_ref(spec_fixture.spec, "schema", "Pet"),
750 },
751 }
752
753 @pytest.mark.parametrize("spec_fixture", ("3.0.0",), indirect=True)
754 def test_schema_partially_v3(self, spec_fixture):
755 spec_fixture.spec.components.schema("Pet", schema=PetSchema)
756 spec_fixture.spec.path(
757 path="/parents",
758 operations={
759 "get": {
760 "responses": {
761 200: {
762 "content": {
763 "application/json": {
764 "schema": {
765 "type": "object",
766 "properties": {
767 "mother": PetSchema,
768 "father": PetSchema,
769 },
770 }
771 }
772 }
773 }
774 }
775 }
776 },
777 )
778 get = get_paths(spec_fixture.spec)["/parents"]["get"]
779 assert get["responses"]["200"]["content"]["application/json"]["schema"] == {
780 "type": "object",
781 "properties": {
782 "mother": build_ref(spec_fixture.spec, "schema", "Pet"),
783 "father": build_ref(spec_fixture.spec, "schema", "Pet"),
784 },
785 }
786
787 def test_parameter_reference(self, spec_fixture):
788 if spec_fixture.spec.openapi_version.major < 3:
789 param = {"schema": PetSchema}
790 else:
791 param = {"content": {"application/json": {"schema": PetSchema}}}
792 spec_fixture.spec.components.parameter("Pet", "body", param)
793 spec_fixture.spec.path(
794 path="/parents", operations={"get": {"parameters": ["Pet"]}}
795 )
796 get = get_paths(spec_fixture.spec)["/parents"]["get"]
797 assert get["parameters"] == [build_ref(spec_fixture.spec, "parameter", "Pet")]
798
799 def test_response_reference(self, spec_fixture):
800 if spec_fixture.spec.openapi_version.major < 3:
801 resp = {"schema": PetSchema}
802 else:
803 resp = {"content": {"application/json": {"schema": PetSchema}}}
804 spec_fixture.spec.components.response("Pet", resp)
805 spec_fixture.spec.path(
806 path="/parents", operations={"get": {"responses": {"200": "Pet"}}}
807 )
808 get = get_paths(spec_fixture.spec)["/parents"]["get"]
809 assert get["responses"] == {
810 "200": build_ref(spec_fixture.spec, "response", "Pet")
811 }
812
813 def test_schema_global_state_untouched_2json(self, spec_fixture):
814 assert get_nested_schema(RunSchema, "sample") is None
815 data = spec_fixture.openapi.schema2jsonschema(RunSchema)
816 json.dumps(data)
817 assert get_nested_schema(RunSchema, "sample") is None
818
819 def test_schema_global_state_untouched_2parameters(self, spec_fixture):
820 assert get_nested_schema(RunSchema, "sample") is None
821 data = spec_fixture.openapi.schema2parameters(RunSchema)
822 json.dumps(data)
823 assert get_nested_schema(RunSchema, "sample") is None
824
825
826 class TestCircularReference:
827 def test_circular_referencing_schemas(self, spec):
828 spec.components.schema("Analysis", schema=AnalysisSchema)
829 definitions = get_schemas(spec)
830 ref = definitions["Analysis"]["properties"]["sample"]
831 assert ref == build_ref(spec, "schema", "Sample")
832
833
834 # Regression tests for issue #55
835 class TestSelfReference:
836 def test_self_referencing_field_single(self, spec):
837 spec.components.schema("SelfReference", schema=SelfReferencingSchema)
838 definitions = get_schemas(spec)
839 ref = definitions["SelfReference"]["properties"]["single"]
840 assert ref == build_ref(spec, "schema", "SelfReference")
841
842 def test_self_referencing_field_many(self, spec):
843 spec.components.schema("SelfReference", schema=SelfReferencingSchema)
844 definitions = get_schemas(spec)
845 result = definitions["SelfReference"]["properties"]["many"]
846 assert result == {
847 "type": "array",
848 "items": build_ref(spec, "schema", "SelfReference"),
849 }
850
851
852 class TestOrderedSchema:
853 def test_ordered_schema(self, spec):
854 spec.components.schema("Ordered", schema=OrderedSchema)
855 result = get_schemas(spec)["Ordered"]["properties"]
856 assert list(result.keys()) == ["field1", "field2", "field3", "field4", "field5"]
857
858
859 class TestFieldWithCustomProps:
860 def test_field_with_custom_props(self, spec):
861 spec.components.schema("PatternedObject", schema=PatternedObjectSchema)
862 result = get_schemas(spec)["PatternedObject"]["properties"]["count"]
863 assert "x-count" in result
864 assert result["x-count"] == 1
865
866 def test_field_with_custom_props_passed_as_snake_case(self, spec):
867 spec.components.schema("PatternedObject", schema=PatternedObjectSchema)
868 result = get_schemas(spec)["PatternedObject"]["properties"]["count2"]
869 assert "x-count2" in result
870 assert result["x-count2"] == 2
871
872
873 class TestSchemaWithDefaultValues:
874 def test_schema_with_default_values(self, spec):
875 spec.components.schema("DefaultValuesSchema", schema=DefaultValuesSchema)
876 definitions = get_schemas(spec)
877 props = definitions["DefaultValuesSchema"]["properties"]
878 assert props["number_auto_default"]["default"] == 12
879 assert props["number_manual_default"]["default"] == 42
880 assert "default" not in props["string_callable_default"]
881 assert props["string_manual_default"]["default"] == "Manual"
882 assert "default" not in props["numbers"]
883
884
885 @pytest.mark.skipif(
886 MARSHMALLOW_VERSION_INFO[0] < 3, reason="Values ignored in marshmallow 2"
887 )
888 class TestDictValues:
889 def test_dict_values_resolve_to_additional_properties(self, spec):
890 class SchemaWithDict(Schema):
891 dict_field = Dict(values=String())
892
893 spec.components.schema("SchemaWithDict", schema=SchemaWithDict)
894 result = get_schemas(spec)["SchemaWithDict"]["properties"]["dict_field"]
895 assert result == {"type": "object", "additionalProperties": {"type": "string"}}
896
897 def test_dict_with_empty_values_field(self, spec):
898 class SchemaWithDict(Schema):
899 dict_field = Dict()
900
901 spec.components.schema("SchemaWithDict", schema=SchemaWithDict)
902 result = get_schemas(spec)["SchemaWithDict"]["properties"]["dict_field"]
903 assert result == {"type": "object"}
904
905 def test_dict_with_nested(self, spec):
906 class SchemaWithDict(Schema):
907 dict_field = Dict(values=Nested(PetSchema))
908
909 spec.components.schema("SchemaWithDict", schema=SchemaWithDict)
910
911 assert len(get_schemas(spec)) == 2
912
913 result = get_schemas(spec)["SchemaWithDict"]["properties"]["dict_field"]
914 assert result == {
915 "additionalProperties": build_ref(spec, "schema", "Pet"),
916 "type": "object",
917 }
918
919
920 class TestList:
921 def test_list_with_nested(self, spec):
922 class SchemaWithList(Schema):
923 list_field = List(Nested(PetSchema))
924
925 spec.components.schema("SchemaWithList", schema=SchemaWithList)
926
927 assert len(get_schemas(spec)) == 2
928
929 result = get_schemas(spec)["SchemaWithList"]["properties"]["list_field"]
930 assert result == {"items": build_ref(spec, "schema", "Pet"), "type": "array"}
0 import pytest
1
2 from marshmallow import Schema, fields
3
4 from apispec.ext.marshmallow.common import (
5 make_schema_key,
6 get_unique_schema_name,
7 get_fields,
8 )
9 from .schemas import PetSchema, SampleSchema
10
11
12 class TestMakeSchemaKey:
13 def test_raise_if_schema_class_passed(self):
14 with pytest.raises(TypeError, match="based on a Schema instance"):
15 make_schema_key(PetSchema)
16
17 def test_same_schemas_instances_equal(self):
18 assert make_schema_key(PetSchema()) == make_schema_key(PetSchema())
19
20 @pytest.mark.parametrize("structure", (list, set))
21 def test_same_schemas_instances_unhashable_modifiers_equal(self, structure):
22 modifier = [str(i) for i in range(1000)]
23 assert make_schema_key(
24 PetSchema(load_only=structure(modifier))
25 ) == make_schema_key(PetSchema(load_only=structure(modifier[::-1])))
26
27 def test_different_schemas_not_equal(self):
28 assert make_schema_key(PetSchema()) != make_schema_key(SampleSchema())
29
30 def test_instances_with_different_modifiers_not_equal(self):
31 assert make_schema_key(PetSchema()) != make_schema_key(PetSchema(partial=True))
32
33
34 class TestUniqueName:
35 def test_unique_name(self, spec):
36 properties = {
37 "id": {"type": "integer", "format": "int64"},
38 "name": {"type": "string", "example": "doggie"},
39 }
40
41 name = get_unique_schema_name(spec.components, "Pet")
42 assert name == "Pet"
43
44 spec.components.schema("Pet", properties=properties)
45 with pytest.warns(
46 UserWarning, match="Multiple schemas resolved to the name Pet"
47 ):
48 name_1 = get_unique_schema_name(spec.components, "Pet")
49 assert name_1 == "Pet1"
50
51 spec.components.schema("Pet1", properties=properties)
52 with pytest.warns(
53 UserWarning, match="Multiple schemas resolved to the name Pet"
54 ):
55 name_2 = get_unique_schema_name(spec.components, "Pet")
56 assert name_2 == "Pet2"
57
58
59 class TestGetFields:
60 @pytest.mark.parametrize("exclude_type", (tuple, list))
61 @pytest.mark.parametrize("dump_only_type", (tuple, list))
62 def test_get_fields_meta_exclude_dump_only_as_list_and_tuple(
63 self, exclude_type, dump_only_type
64 ):
65 class ExcludeSchema(Schema):
66 field1 = fields.Int()
67 field2 = fields.Int()
68 field3 = fields.Int()
69 field4 = fields.Int()
70 field5 = fields.Int()
71
72 class Meta:
73 ordered = True
74 exclude = exclude_type(("field1", "field2"))
75 dump_only = dump_only_type(("field3", "field4"))
76
77 assert list(get_fields(ExcludeSchema).keys()) == ["field3", "field4", "field5"]
78 assert list(get_fields(ExcludeSchema, exclude_dump_only=True).keys()) == [
79 "field5"
80 ]
0 import datetime as dt
1 import re
2
3 import pytest
4 from marshmallow import fields, validate
5
6 from apispec.ext.marshmallow.openapi import MARSHMALLOW_VERSION_INFO
7
8 from .schemas import CategorySchema, CustomList, CustomStringField, CustomIntegerField
9 from .utils import build_ref
10
11
12 def test_field2choices_preserving_order(openapi):
13 choices = ["a", "b", "c", "aa", "0", "cc"]
14 field = fields.String(validate=validate.OneOf(choices))
15 assert openapi.field2choices(field) == {"enum": choices}
16
17
18 @pytest.mark.parametrize(
19 ("FieldClass", "jsontype"),
20 [
21 (fields.Integer, "integer"),
22 (fields.Number, "number"),
23 (fields.Float, "number"),
24 (fields.String, "string"),
25 (fields.Str, "string"),
26 (fields.Boolean, "boolean"),
27 (fields.Bool, "boolean"),
28 (fields.UUID, "string"),
29 (fields.DateTime, "string"),
30 (fields.Date, "string"),
31 (fields.Time, "string"),
32 (fields.Email, "string"),
33 (fields.URL, "string"),
34 # Custom fields inherit types from their parents
35 (CustomStringField, "string"),
36 (CustomIntegerField, "integer"),
37 ],
38 )
39 def test_field2property_type(FieldClass, jsontype, spec_fixture):
40 field = FieldClass()
41 res = spec_fixture.openapi.field2property(field)
42 assert res["type"] == jsontype
43
44
45 @pytest.mark.parametrize("FieldClass", [fields.Field, fields.Raw])
46 def test_field2property_no_type_(FieldClass, spec_fixture):
47 field = FieldClass()
48 res = spec_fixture.openapi.field2property(field)
49 assert "type" not in res
50
51
52 @pytest.mark.parametrize("ListClass", [fields.List, CustomList])
53 def test_formatted_field_translates_to_array(ListClass, spec_fixture):
54 field = ListClass(fields.String)
55 res = spec_fixture.openapi.field2property(field)
56 assert res["type"] == "array"
57 assert res["items"] == spec_fixture.openapi.field2property(fields.String())
58
59
60 @pytest.mark.parametrize(
61 ("FieldClass", "expected_format"),
62 [
63 (fields.Integer, "int32"),
64 (fields.Float, "float"),
65 (fields.UUID, "uuid"),
66 (fields.DateTime, "date-time"),
67 (fields.Date, "date"),
68 (fields.Email, "email"),
69 (fields.URL, "url"),
70 ],
71 )
72 def test_field2property_formats(FieldClass, expected_format, spec_fixture):
73 field = FieldClass()
74 res = spec_fixture.openapi.field2property(field)
75 assert res["format"] == expected_format
76
77
78 def test_field_with_description(spec_fixture):
79 field = fields.Str(description="a username")
80 res = spec_fixture.openapi.field2property(field)
81 assert res["description"] == "a username"
82
83
84 def test_field_with_missing(spec_fixture):
85 field = fields.Str(default="foo", missing="bar")
86 res = spec_fixture.openapi.field2property(field)
87 assert res["default"] == "bar"
88
89
90 def test_boolean_field_with_false_missing(spec_fixture):
91 field = fields.Boolean(default=None, missing=False)
92 res = spec_fixture.openapi.field2property(field)
93 assert res["default"] is False
94
95
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))
101 res = spec_fixture.openapi.field2property(field)
102 assert res["default"] == dt.date(2014, 7, 18).isoformat()
103
104
105 def test_field_with_missing_callable(spec_fixture):
106 field = fields.Str(missing=lambda: "dummy")
107 res = spec_fixture.openapi.field2property(field)
108 assert "default" not in res
109
110
111 def test_field_with_doc_default(spec_fixture):
112 field = fields.Str(doc_default="Manual default")
113 res = spec_fixture.openapi.field2property(field)
114 assert res["default"] == "Manual default"
115
116
117 def test_field_with_doc_default_and_missing(spec_fixture):
118 field = fields.Int(doc_default=42, missing=12)
119 res = spec_fixture.openapi.field2property(field)
120 assert res["default"] == 42
121
122
123 def test_field_with_choices(spec_fixture):
124 field = fields.Str(validate=validate.OneOf(["freddie", "brian", "john"]))
125 res = spec_fixture.openapi.field2property(field)
126 assert set(res["enum"]) == {"freddie", "brian", "john"}
127
128
129 def test_field_with_equal(spec_fixture):
130 field = fields.Str(validate=validate.Equal("only choice"))
131 res = spec_fixture.openapi.field2property(field)
132 assert res["enum"] == ["only choice"]
133
134
135 def test_only_allows_valid_properties_in_metadata(spec_fixture):
136 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
145 assert "description" in res
146 assert "enum" in res
147 assert "allOf" in res
148 assert "not_valid" not in res
149
150
151 def test_field_with_choices_multiple(spec_fixture):
152 field = fields.Str(
153 validate=[
154 validate.OneOf(["freddie", "brian", "john"]),
155 validate.OneOf(["brian", "john", "roger"]),
156 ]
157 )
158 res = spec_fixture.openapi.field2property(field)
159 assert set(res["enum"]) == {"brian", "john"}
160
161
162 def test_field_with_additional_metadata(spec_fixture):
163 field = fields.Str(minLength=6, maxLength=100)
164 res = spec_fixture.openapi.field2property(field)
165 assert res["maxLength"] == 100
166 assert res["minLength"] == 6
167
168
169 def test_field_with_allow_none(spec_fixture):
170 field = fields.Str(allow_none=True)
171 res = spec_fixture.openapi.field2property(field)
172 if spec_fixture.openapi.openapi_version.major < 3:
173 assert res["x-nullable"] is True
174 else:
175 assert res["nullable"] is True
176
177
178 def test_field_with_str_regex(spec_fixture):
179 regex_str = "^[a-zA-Z0-9]$"
180 field = fields.Str(validate=validate.Regexp(regex_str))
181 ret = spec_fixture.openapi.field2property(field)
182 assert ret["pattern"] == regex_str
183
184
185 def test_field_with_pattern_obj_regex(spec_fixture):
186 regex_str = "^[a-zA-Z0-9]$"
187 field = fields.Str(validate=validate.Regexp(re.compile(regex_str)))
188 ret = spec_fixture.openapi.field2property(field)
189 assert ret["pattern"] == regex_str
190
191
192 def test_field_with_no_pattern(spec_fixture):
193 field = fields.Str()
194 ret = spec_fixture.openapi.field2property(field)
195 assert "pattern" not in ret
196
197
198 def test_field_with_multiple_patterns(recwarn, spec_fixture):
199 regex_validators = [validate.Regexp("winner"), validate.Regexp("loser")]
200 field = fields.Str(validate=regex_validators)
201 with pytest.warns(UserWarning, match="More than one regex validator"):
202 ret = spec_fixture.openapi.field2property(field)
203 assert ret["pattern"] == "winner"
204
205
206 def test_field2property_nested_spec_metadatas(spec_fixture):
207 spec_fixture.spec.components.schema("Category", schema=CategorySchema)
208 category = fields.Nested(
209 CategorySchema,
210 description="A category",
211 invalid_property="not in the result",
212 x_extension="A great extension",
213 )
214 result = spec_fixture.openapi.field2property(category)
215 assert result == {
216 "allOf": [build_ref(spec_fixture.spec, "schema", "Category")],
217 "description": "A category",
218 "x-extension": "A great extension",
219 }
220
221
222 def test_field2property_nested_spec(spec_fixture):
223 spec_fixture.spec.components.schema("Category", schema=CategorySchema)
224 category = fields.Nested(CategorySchema)
225 assert spec_fixture.openapi.field2property(category) == build_ref(
226 spec_fixture.spec, "schema", "Category"
227 )
228
229
230 def test_field2property_nested_many_spec(spec_fixture):
231 spec_fixture.spec.components.schema("Category", schema=CategorySchema)
232 category = fields.Nested(CategorySchema, many=True)
233 ret = spec_fixture.openapi.field2property(category)
234 assert ret["type"] == "array"
235 assert ret["items"] == build_ref(spec_fixture.spec, "schema", "Category")
236
237
238 def test_field2property_nested_ref(spec_fixture):
239 category = fields.Nested(CategorySchema)
240 ref = spec_fixture.openapi.field2property(category)
241 assert ref == build_ref(spec_fixture.spec, "schema", "Category")
242
243
244 def test_field2property_nested_many(spec_fixture):
245 categories = fields.Nested(CategorySchema, many=True)
246 res = spec_fixture.openapi.field2property(categories)
247 assert res["type"] == "array"
248 assert res["items"] == build_ref(spec_fixture.spec, "schema", "Category")
249
250
251 def test_nested_field_with_property(spec_fixture):
252 category_1 = fields.Nested(CategorySchema)
253 category_2 = fields.Nested(CategorySchema, dump_only=True)
254 category_3 = fields.Nested(CategorySchema, many=True)
255 category_4 = fields.Nested(CategorySchema, many=True, dump_only=True)
256 spec_fixture.spec.components.schema("Category", schema=CategorySchema)
257
258 assert spec_fixture.openapi.field2property(category_1) == build_ref(
259 spec_fixture.spec, "schema", "Category"
260 )
261 assert spec_fixture.openapi.field2property(category_2) == {
262 "allOf": [build_ref(spec_fixture.spec, "schema", "Category")],
263 "readOnly": True,
264 }
265 assert spec_fixture.openapi.field2property(category_3) == {
266 "items": build_ref(spec_fixture.spec, "schema", "Category"),
267 "type": "array",
268 }
269 assert spec_fixture.openapi.field2property(category_4) == {
270 "items": build_ref(spec_fixture.spec, "schema", "Category"),
271 "readOnly": True,
272 "type": "array",
273 }
274
275
276 def test_custom_properties_for_custom_fields(spec_fixture):
277 def custom_string2properties(self, field, **kwargs):
278 ret = {}
279 if isinstance(field, CustomStringField):
280 if self.openapi_version.major == 2:
281 ret["x-customString"] = True
282 else:
283 ret["x-customString"] = False
284 return ret
285
286 spec_fixture.marshmallow_plugin.converter.add_attribute_function(
287 custom_string2properties
288 )
289 properties = spec_fixture.marshmallow_plugin.converter.field2property(
290 CustomStringField()
291 )
292 assert properties["x-customString"] == (
293 spec_fixture.openapi.openapi_version == "2.0"
294 )
0 import pytest
1
2 from marshmallow import fields, Schema, validate
3
4 from apispec.ext.marshmallow import MarshmallowPlugin
5 from apispec.ext.marshmallow.openapi import MARSHMALLOW_VERSION_INFO
6 from apispec import exceptions, utils, APISpec
7
8 from .schemas import CustomList, CustomStringField
9 from .utils import get_schemas, build_ref
10
11
12 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")
16 if openapi.openapi_version.major < 3:
17 assert res[0]["default"] == "bar"
18 else:
19 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
64 # json/body is invalid for OpenAPI 3
65 @pytest.mark.parametrize("openapi", ("2.0",), indirect=True)
66 def test_fields_default_location_mapping_if_schema_many(self, openapi):
67 class ExampleSchema(Schema):
68 id = fields.Int()
69
70 schema = ExampleSchema(many=True)
71 res = openapi.schema2parameters(schema=schema, default_in="json")
72 assert res[0]["in"] == "body"
73
74 def test_fields_with_dump_only(self, openapi):
75 class UserSchema(Schema):
76 name = fields.Str(dump_only=True)
77
78 res = openapi.fields2parameters(UserSchema._declared_fields, default_in="query")
79 assert len(res) == 0
80 res = openapi.fields2parameters(UserSchema().fields, default_in="query")
81 assert len(res) == 0
82
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
93 class TestMarshmallowSchemaToModelDefinition:
94 def test_invalid_schema(self, openapi):
95 with pytest.raises(ValueError):
96 openapi.schema2jsonschema(None)
97
98 def test_schema2jsonschema_with_explicit_fields(self, openapi):
99 class UserSchema(Schema):
100 _id = fields.Int()
101 email = fields.Email(description="email address of the user")
102 name = fields.Str()
103
104 class Meta:
105 title = "User"
106
107 res = openapi.schema2jsonschema(UserSchema)
108 assert res["title"] == "User"
109 assert res["type"] == "object"
110 props = res["properties"]
111 assert props["_id"]["type"] == "integer"
112 assert props["email"]["type"] == "string"
113 assert props["email"]["format"] == "email"
114 assert props["email"]["description"] == "email address of the user"
115
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):
146 class ExampleSchema(Schema):
147 _id = fields.Int(data_key="id")
148 _global = fields.Int(data_key="global")
149
150 class Meta:
151 exclude = ("_global",)
152
153 res = openapi.schema2jsonschema(ExampleSchema)
154 assert res["type"] == "object"
155 props = res["properties"]
156 # `_id` renamed to `id`
157 assert "_id" not in props and props["id"]["type"] == "integer"
158 # `_global` excluded correctly
159 assert "_global" not in props and "global" not in props
160
161 def test_required_fields(self, openapi):
162 class BandSchema(Schema):
163 drummer = fields.Str(required=True)
164 bassist = fields.Str()
165
166 res = openapi.schema2jsonschema(BandSchema)
167 assert res["required"] == ["drummer"]
168
169 def test_partial(self, openapi):
170 class BandSchema(Schema):
171 drummer = fields.Str(required=True)
172 bassist = fields.Str(required=True)
173
174 res = openapi.schema2jsonschema(BandSchema(partial=True))
175 assert "required" not in res
176
177 res = openapi.schema2jsonschema(BandSchema(partial=("drummer",)))
178 assert res["required"] == ["bassist"]
179
180 def test_no_required_fields(self, openapi):
181 class BandSchema(Schema):
182 drummer = fields.Str()
183 bassist = fields.Str()
184
185 res = openapi.schema2jsonschema(BandSchema)
186 assert "required" not in res
187
188 def test_title_and_description_may_be_added(self, openapi):
189 class UserSchema(Schema):
190 class Meta:
191 title = "User"
192 description = "A registered user"
193
194 res = openapi.schema2jsonschema(UserSchema)
195 assert res["description"] == "A registered user"
196 assert res["title"] == "User"
197
198 def test_excluded_fields(self, openapi):
199 class WhiteStripesSchema(Schema):
200 class Meta:
201 exclude = ("bassist",)
202
203 guitarist = fields.Str()
204 drummer = fields.Str()
205 bassist = fields.Str()
206
207 res = openapi.schema2jsonschema(WhiteStripesSchema)
208 assert set(res["properties"].keys()) == {"guitarist", "drummer"}
209
210 def test_only_explicitly_declared_fields_are_translated(self, openapi):
211 class UserSchema(Schema):
212 _id = fields.Int()
213
214 class Meta:
215 title = "User"
216 fields = ("_id", "email")
217
218 with pytest.warns(
219 UserWarning,
220 match="Only explicitly-declared fields will be included in the Schema Object.",
221 ):
222 res = openapi.schema2jsonschema(UserSchema)
223 assert res["type"] == "object"
224 props = res["properties"]
225 assert "_id" in props
226 assert "email" not in props
227
228 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
236 res = openapi.fields2jsonschema(fields_dict)
237 assert res["required"] == ["id"]
238
239 @pytest.mark.parametrize("many", (True, False))
240 def test_schema_instance_inspection(self, openapi, many):
241 class UserSchema(Schema):
242 _id = fields.Int()
243
244 res = openapi.schema2jsonschema(UserSchema(many=many))
245 assert res["type"] == "object"
246 props = res["properties"]
247 assert "_id" in props
248
249 def test_raises_error_if_no_declared_fields(self, openapi):
250 class NotASchema:
251 pass
252
253 expected_error = "{!r} doesn't have either `fields` or `_declared_fields`.".format(
254 NotASchema
255 )
256 with pytest.raises(ValueError, match=expected_error):
257 openapi.schema2jsonschema(NotASchema)
258
259
260 class TestMarshmallowSchemaToParameters:
261 @pytest.mark.parametrize("ListClass", [fields.List, CustomList])
262 def test_field_multiple(self, ListClass, openapi):
263 field = ListClass(fields.Str, location="querystring")
264 res = openapi.field2parameter(field, name="field", default_in=None)
265 assert res["in"] == "query"
266 if openapi.openapi_version.major < 3:
267 assert res["type"] == "array"
268 assert res["items"]["type"] == "string"
269 assert res["collectionFormat"] == "multi"
270 else:
271 assert res["schema"]["type"] == "array"
272 assert res["schema"]["items"]["type"] == "string"
273 assert res["style"] == "form"
274 assert res["explode"] is True
275
276 def test_field_required(self, openapi):
277 field = fields.Str(required=True, location="query")
278 res = openapi.field2parameter(field, name="field", default_in=None)
279 assert res["required"] is True
280
281 def test_invalid_schema(self, openapi):
282 with pytest.raises(ValueError):
283 openapi.schema2parameters(None)
284
285 # json/body is invalid for OpenAPI 3
286 @pytest.mark.parametrize("openapi", ("2.0",), indirect=True)
287 def test_schema_body(self, openapi):
288 class UserSchema(Schema):
289 name = fields.Str()
290 email = fields.Email()
291
292 res = openapi.schema2parameters(UserSchema, default_in="body")
293 assert len(res) == 1
294 param = res[0]
295 assert param["in"] == "body"
296 assert param["schema"] == {"$ref": "#/definitions/User"}
297
298 # json/body is invalid for OpenAPI 3
299 @pytest.mark.parametrize("openapi", ("2.0",), indirect=True)
300 def test_schema_body_with_dump_only(self, openapi):
301 class UserSchema(Schema):
302 name = fields.Str()
303 email = fields.Email(dump_only=True)
304
305 res_nodump = openapi.schema2parameters(UserSchema, default_in="body")
306 assert len(res_nodump) == 1
307 param = res_nodump[0]
308 assert param["in"] == "body"
309 assert param["schema"] == build_ref(openapi.spec, "schema", "User")
310
311 # json/body is invalid for OpenAPI 3
312 @pytest.mark.parametrize("openapi", ("2.0",), indirect=True)
313 def test_schema_body_many(self, openapi):
314 class UserSchema(Schema):
315 name = fields.Str()
316 email = fields.Email()
317
318 res = openapi.schema2parameters(UserSchema(many=True), default_in="body")
319 assert len(res) == 1
320 param = res[0]
321 assert param["in"] == "body"
322 assert param["schema"]["type"] == "array"
323 assert param["schema"]["items"] == {"$ref": "#/definitions/User"}
324
325 def test_schema_query(self, openapi):
326 class UserSchema(Schema):
327 name = fields.Str()
328 email = fields.Email()
329
330 res = openapi.schema2parameters(UserSchema, default_in="query")
331 assert len(res) == 2
332 res.sort(key=lambda param: param["name"])
333 assert res[0]["name"] == "email"
334 assert res[0]["in"] == "query"
335 assert res[1]["name"] == "name"
336 assert res[1]["in"] == "query"
337
338 def test_schema_query_instance(self, openapi):
339 class UserSchema(Schema):
340 name = fields.Str()
341 email = fields.Email()
342
343 res = openapi.schema2parameters(UserSchema(), default_in="query")
344 assert len(res) == 2
345 res.sort(key=lambda param: param["name"])
346 assert res[0]["name"] == "email"
347 assert res[0]["in"] == "query"
348 assert res[1]["name"] == "name"
349 assert res[1]["in"] == "query"
350
351 def test_schema_query_instance_many_should_raise_exception(self, openapi):
352 class UserSchema(Schema):
353 name = fields.Str()
354 email = fields.Email()
355
356 with pytest.raises(AssertionError):
357 openapi.schema2parameters(UserSchema(many=True), default_in="query")
358
359 def test_fields_query(self, openapi):
360 field_dict = {"name": fields.Str(), "email": fields.Email()}
361 res = openapi.fields2parameters(field_dict, default_in="query")
362 assert len(res) == 2
363 res.sort(key=lambda param: param["name"])
364 assert res[0]["name"] == "email"
365 assert res[0]["in"] == "query"
366 assert res[1]["name"] == "name"
367 assert res[1]["in"] == "query"
368
369 def test_raises_error_if_not_a_schema(self, openapi):
370 class NotASchema:
371 pass
372
373 expected_error = "{!r} doesn't have either `fields` or `_declared_fields`".format(
374 NotASchema
375 )
376 with pytest.raises(ValueError, match=expected_error):
377 openapi.schema2jsonschema(NotASchema)
378
379
380 class CategorySchema(Schema):
381 id = fields.Int()
382 name = fields.Str(required=True)
383 breed = fields.Str(dump_only=True)
384
385
386 class PageSchema(Schema):
387 offset = fields.Int()
388 limit = fields.Int()
389
390
391 class PetSchema(Schema):
392 category = fields.Nested(CategorySchema, many=True)
393 name = fields.Str()
394
395
396 class TestNesting:
397 def test_schema2jsonschema_with_nested_fields(self, spec_fixture):
398 res = spec_fixture.openapi.schema2jsonschema(PetSchema)
399 props = res["properties"]
400
401 assert props["category"]["items"] == build_ref(
402 spec_fixture.spec, "schema", "Category"
403 )
404
405 @pytest.mark.parametrize("modifier", ("only", "exclude"))
406 def test_schema2jsonschema_with_nested_fields_only_exclude(
407 self, spec_fixture, modifier
408 ):
409 class Child(Schema):
410 i = fields.Int()
411 j = fields.Int()
412
413 class Parent(Schema):
414 child = fields.Nested(Child, **{modifier: ("i",)})
415
416 spec_fixture.openapi.schema2jsonschema(Parent)
417 props = get_schemas(spec_fixture.spec)["Child"]["properties"]
418 assert ("i" in props) == (modifier == "only")
419 assert ("j" not in props) == (modifier == "only")
420
421 def test_schema2jsonschema_with_nested_fields_with_adhoc_changes(
422 self, spec_fixture
423 ):
424 category_schema = CategorySchema()
425 category_schema.fields["id"].required = True
426
427 class PetSchema(Schema):
428 category = fields.Nested(category_schema, many=True)
429 name = fields.Str()
430
431 spec_fixture.spec.components.schema("Pet", schema=PetSchema)
432 props = get_schemas(spec_fixture.spec)
433
434 assert props["Category"] == spec_fixture.openapi.schema2jsonschema(
435 category_schema
436 )
437 assert set(props["Category"]["required"]) == {"id", "name"}
438
439 props["Category"]["required"] = ["name"]
440 assert props["Category"] == spec_fixture.openapi.schema2jsonschema(
441 CategorySchema
442 )
443
444 def test_schema2jsonschema_with_nested_excluded_fields(self, spec):
445 category_schema = CategorySchema(exclude=("breed",))
446
447 class PetSchema(Schema):
448 category = fields.Nested(category_schema)
449
450 spec.components.schema("Pet", schema=PetSchema)
451
452 category_props = get_schemas(spec)["Category"]["properties"]
453 assert "breed" not in category_props
454
455
456 def test_openapi_tools_validate_v2():
457 ma_plugin = MarshmallowPlugin()
458 spec = APISpec(
459 title="Pets", version="0.1", plugins=(ma_plugin,), openapi_version="2.0"
460 )
461 openapi = ma_plugin.converter
462
463 spec.components.schema("Category", schema=CategorySchema)
464 spec.components.schema("Pet", {"discriminator": "name"}, schema=PetSchema)
465
466 spec.path(
467 view=None,
468 path="/category/{category_id}",
469 operations={
470 "get": {
471 "parameters": [
472 {"name": "q", "in": "query", "type": "string"},
473 {
474 "name": "category_id",
475 "in": "path",
476 "required": True,
477 "type": "string",
478 },
479 openapi.field2parameter(
480 field=fields.List(
481 fields.Str(),
482 validate=validate.OneOf(["freddie", "roger"]),
483 location="querystring",
484 ),
485 default_in=None,
486 name="body",
487 ),
488 ]
489 + openapi.schema2parameters(PageSchema, default_in="query"),
490 "responses": {200: {"schema": PetSchema, "description": "A pet"}},
491 },
492 "post": {
493 "parameters": (
494 [
495 {
496 "name": "category_id",
497 "in": "path",
498 "required": True,
499 "type": "string",
500 }
501 ]
502 + openapi.schema2parameters(CategorySchema, default_in="body")
503 ),
504 "responses": {201: {"schema": PetSchema, "description": "A pet"}},
505 },
506 },
507 )
508 try:
509 utils.validate_spec(spec)
510 except exceptions.OpenAPIError as error:
511 pytest.fail(str(error))
512
513
514 def test_openapi_tools_validate_v3():
515 ma_plugin = MarshmallowPlugin()
516 spec = APISpec(
517 title="Pets", version="0.1", plugins=(ma_plugin,), openapi_version="3.0.0"
518 )
519 openapi = ma_plugin.converter
520
521 spec.components.schema("Category", schema=CategorySchema)
522 spec.components.schema("Pet", schema=PetSchema)
523
524 spec.path(
525 view=None,
526 path="/category/{category_id}",
527 operations={
528 "get": {
529 "parameters": [
530 {"name": "q", "in": "query", "schema": {"type": "string"}},
531 {
532 "name": "category_id",
533 "in": "path",
534 "required": True,
535 "schema": {"type": "string"},
536 },
537 openapi.field2parameter(
538 field=fields.List(
539 fields.Str(),
540 validate=validate.OneOf(["freddie", "roger"]),
541 location="querystring",
542 ),
543 default_in=None,
544 name="body",
545 ),
546 ]
547 + openapi.schema2parameters(PageSchema, default_in="query"),
548 "responses": {
549 200: {
550 "description": "success",
551 "content": {"application/json": {"schema": PetSchema}},
552 }
553 },
554 },
555 "post": {
556 "parameters": (
557 [
558 {
559 "name": "category_id",
560 "in": "path",
561 "required": True,
562 "schema": {"type": "string"},
563 }
564 ]
565 ),
566 "requestBody": {
567 "content": {"application/json": {"schema": CategorySchema}}
568 },
569 "responses": {
570 201: {
571 "description": "created",
572 "content": {"application/json": {"schema": PetSchema}},
573 }
574 },
575 },
576 },
577 )
578 try:
579 utils.validate_spec(spec)
580 except exceptions.OpenAPIError as error:
581 pytest.fail(str(error))
582
583
584 class TestFieldValidation:
585 class ValidationSchema(Schema):
586 id = fields.Int(dump_only=True)
587 range = fields.Int(validate=validate.Range(min=1, max=10))
588 multiple_ranges = fields.Int(
589 validate=[
590 validate.Range(min=1),
591 validate.Range(min=3),
592 validate.Range(max=10),
593 validate.Range(max=7),
594 ]
595 )
596 list_length = fields.List(fields.Str, validate=validate.Length(min=1, max=10))
597 custom_list_length = CustomList(
598 fields.Str, validate=validate.Length(min=1, max=10)
599 )
600 string_length = fields.Str(validate=validate.Length(min=1, max=10))
601 custom_field_length = CustomStringField(validate=validate.Length(min=1, max=10))
602 multiple_lengths = fields.Str(
603 validate=[
604 validate.Length(min=1),
605 validate.Length(min=3),
606 validate.Length(max=10),
607 validate.Length(max=7),
608 ]
609 )
610 equal_length = fields.Str(
611 validate=[validate.Length(equal=5), validate.Length(min=1, max=10)]
612 )
613
614 @pytest.mark.parametrize(
615 ("field", "properties"),
616 [
617 ("range", {"minimum": 1, "maximum": 10}),
618 ("multiple_ranges", {"minimum": 3, "maximum": 7}),
619 ("list_length", {"minItems": 1, "maxItems": 10}),
620 ("custom_list_length", {"minItems": 1, "maxItems": 10}),
621 ("string_length", {"minLength": 1, "maxLength": 10}),
622 ("custom_field_length", {"minLength": 1, "maxLength": 10}),
623 ("multiple_lengths", {"minLength": 3, "maxLength": 7}),
624 ("equal_length", {"minLength": 5, "maxLength": 5}),
625 ],
626 )
627 def test_properties(self, field, properties, spec):
628 spec.components.schema("Validation", schema=self.ValidationSchema)
629 result = get_schemas(spec)["Validation"]["properties"][field]
630
631 for attr, expected_value in properties.items():
632 assert attr in result
633 assert result[attr] == expected_value
0 import pytest
1
2 from apispec import utils
3 from apispec.exceptions import APISpecError
4
5
6 class TestOpenAPIVersion:
7 @pytest.mark.parametrize("version", ("1.0", "4.0"))
8 def test_openapi_version_invalid_version(self, version):
9 message = "Not a valid OpenAPI version number:"
10 with pytest.raises(APISpecError, match=message):
11 utils.OpenAPIVersion(version)
12
13 @pytest.mark.parametrize("version", ("3.0.1", utils.OpenAPIVersion("3.0.1")))
14 def test_openapi_version_string_or_openapi_version_param(self, version):
15 assert utils.OpenAPIVersion(version) == utils.OpenAPIVersion("3.0.1")
16
17 def test_openapi_version_digits(self):
18 ver = utils.OpenAPIVersion("3.0.1")
19 assert ver.major == 3
20 assert ver.minor == 0
21 assert ver.patch == 1
22 assert ver.vstring == "3.0.1"
23 assert str(ver) == "3.0.1"
24
25
26 def test_build_reference():
27 assert utils.build_reference("schema", 2, "Test") == {"$ref": "#/definitions/Test"}
28 assert utils.build_reference("parameter", 2, "Test") == {
29 "$ref": "#/parameters/Test"
30 }
31 assert utils.build_reference("response", 2, "Test") == {"$ref": "#/responses/Test"}
32 assert utils.build_reference("security_scheme", 2, "Test") == {
33 "$ref": "#/securityDefinitions/Test"
34 }
35 assert utils.build_reference("schema", 3, "Test") == {
36 "$ref": "#/components/schemas/Test"
37 }
38 assert utils.build_reference("parameter", 3, "Test") == {
39 "$ref": "#/components/parameters/Test"
40 }
41 assert utils.build_reference("response", 3, "Test") == {
42 "$ref": "#/components/responses/Test"
43 }
44 assert utils.build_reference("security_scheme", 3, "Test") == {
45 "$ref": "#/components/securitySchemes/Test"
46 }
0 import pytest
1 from apispec import yaml_utils
2
3
4 def test_load_yaml_from_docstring():
5 def f():
6 """
7 Foo
8 bar
9 baz quux
10
11 ---
12 herp: 1
13 derp: 2
14 """
15
16 result = yaml_utils.load_yaml_from_docstring(f.__doc__)
17 assert result == {"herp": 1, "derp": 2}
18
19
20 @pytest.mark.parametrize("docstring", (None, "", "---"))
21 def test_load_yaml_from_docstring_empty_docstring(docstring):
22 assert yaml_utils.load_yaml_from_docstring(docstring) == {}
23
24
25 @pytest.mark.parametrize("docstring", (None, "", "---"))
26 def test_load_operations_from_docstring_empty_docstring(docstring):
27 assert yaml_utils.load_operations_from_docstring(docstring) == {}
0 """Utilities to get elements of generated spec"""
1
2 from apispec.utils import build_reference
3
4
5 def get_schemas(spec):
6 if spec.openapi_version.major < 3:
7 return spec.to_dict()["definitions"]
8 return spec.to_dict()["components"]["schemas"]
9
10
11 def get_responses(spec):
12 if spec.openapi_version.major < 3:
13 return spec.to_dict()["responses"]
14 return spec.to_dict()["components"]["responses"]
15
16
17 def get_parameters(spec):
18 if spec.openapi_version.major < 3:
19 return spec.to_dict()["parameters"]
20 return spec.to_dict()["components"]["parameters"]
21
22
23 def get_examples(spec):
24 return spec.to_dict()["components"]["examples"]
25
26
27 def get_security_schemes(spec):
28 if spec.openapi_version.major < 3:
29 return spec.to_dict()["securityDefinitions"]
30 return spec.to_dict()["components"]["securitySchemes"]
31
32
33 def get_paths(spec):
34 return spec.to_dict()["paths"]
35
36
37 def build_ref(spec, component_type, obj):
38 return build_reference(component_type, spec.openapi_version.major, obj)
0 [tox]
1 envlist=
2 lint
3 py{35,36,37,38}-marshmallow2
4 py{35,36,37,38}-marshmallow3
5 py38-marshmallowdev
6 docs
7
8 [testenv]
9 extras = tests
10 deps =
11 marshmallow2: marshmallow>=2.0.0,<3.0.0
12 marshmallow3: marshmallow>=3.0.0,<4.0.0
13 marshmallowdev: https://github.com/marshmallow-code/marshmallow/archive/dev.tar.gz
14 commands = pytest {posargs}
15
16 [testenv:lint]
17 deps = pre-commit~=2.4
18 skip_install = true
19 commands = pre-commit run --all-files --show-diff-on-failure
20
21 [testenv:docs]
22 extras = docs
23 commands = sphinx-build docs/ docs/_build {posargs}
24
25 ; Below tasks are for development only (not run in CI)
26
27 [testenv:watch-docs]
28 deps = sphinx-autobuild
29 extras = docs
30 commands = sphinx-autobuild --open-browser docs/ docs/_build {posargs} -z src/apispec -s 2
31
32 [testenv:watch-readme]
33 deps = restview
34 skip_install = true
35 commands = restview README.rst