New upstream version 3.3.1
Sophie Brun
3 years ago
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 | @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.""" |
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 | } |
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 |
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 |