Codebase list apispec / f085b56e-9ac7-4d14-9062-63987ee1e5da/upstream
Import upstream version 4.4.2 Kali Janitor 2 years ago
30 changed file(s) with 1019 addition(s) and 486 deletion(s). Raw diff Collapse all Expand all
00 repos:
11 - repo: https://github.com/asottile/pyupgrade
2 rev: v2.4.1
2 rev: v2.11.0
33 hooks:
44 - id: pyupgrade
5 args: [--py3-plus]
5 args: [--py36-plus]
66 - repo: https://github.com/python/black
7 rev: 19.10b0
7 rev: 20.8b1
88 hooks:
99 - id: black
1010 language_version: python3
1111 - repo: https://gitlab.com/pycqa/flake8
12 rev: 3.8.1
12 rev: 3.9.0
1313 hooks:
1414 - id: flake8
15 additional_dependencies: [flake8-bugbear==20.1.4]
15 additional_dependencies: [flake8-bugbear==21.4.3]
1616 - repo: https://github.com/asottile/blacken-docs
17 rev: v1.7.0
17 rev: v1.10.0
1818 hooks:
1919 - id: blacken-docs
20 additional_dependencies: [black==19.10b0]
20 additional_dependencies: [black==20.8b1]
6262 - Ashutosh Chaudhary `@codeasashu <https://github.com/codeasashu>`_
6363 - Fedor Fominykh `@fedorfo <https://github.com/fedorfo>`_
6464 - Colin Bounouar `@Colin-b <https://github.com/Colin-b>`_
65 - Mikko Kortelainen `@kortsi <https://github.com/kortsi>`_
66 - David Bishop `@teancom <https://github.com/teancom>`_
67 - Andrea Ghensi `@sanzoghenzo <https://github.com/sanzoghenzo>`_
68 - `@timsilvers <https://github.com/timsilvers>`_
69 - Kangwook Lee `@pbzweihander <https://github.com/pbzweihander>`_
70 - Martijn Pieters `@mjpieters <https://github.com/mjpieters>`_
71 - Duncan Booth `@kupuguy <https://github.com/kupuguy>`_
72 - Luke Whitehorn `<https://github.com/Anti-Distinctlyminty>`_
00 Changelog
11 ---------
2
3 4.4.2 (2020-05-24)
4 ******************
5
6 Bug fixes:
7
8 - Respect ``partial`` marshmallow schema parameter: don't document the field as
9 required. (:issue: `627`). Thanks :user:`Anti-Distinctlyminty` for the PR.
10
11 4.4.1 (2020-05-07)
12 ******************
13
14 Bug fixes:
15
16 - Don't set ``additionalProperties`` if ``Meta.unknown`` is ``EXCLUDE``
17 (:issue:`659`). Thanks :user:`kupuguy` for the PR.
18
19 4.4.0 (2020-03-31)
20 ******************
21
22 Features:
23
24 - Populate ``additionalProperties`` from ``Meta.unknown`` (:pr:`635`).
25 Thanks :user:`timsilvers` for the PR.
26 - Allow ``to_yaml`` to pass kwargs to ``yaml.dump`` (:pr:`648`).
27 - Resolve header references in responses (:pr:`650`).
28 - Resolve example references in parameters, request bodies and responses
29 (:pr:`#651`).
30
31 4.3.0 (2021-02-10)
32 ******************
33
34 Features:
35
36 - Add `apispec.core.Components.header` to register header components
37 (:pr:`637`).
38
39 4.2.0 (2021-02-06)
40 ******************
41
42 Features:
43
44 - Make components public attributes of ``Components`` class (:pr:`634`).
45
46 4.1.0 (2021-01-26)
47 ******************
48
49 Features:
50
51 - Resolve schemas in callbacks (:pr:`544`). Thanks :user:`kortsi` for the PR.
52
53 Bug fixes:
54
55 - Fix docstrings documenting kwargs type as dict (:issue:`534`).
56 - Use ``x-minimum`` and ``x-maximum`` extensions to document ranges that are
57 not of number type (e.g. datetime) (:issue:`614`).
58
59 Other changes:
60
61 - Test against Python 3.9.
62
63 4.0.0 (2020-09-30)
64 ******************
65
66 Features:
67
68 - *Backwards-incompatible*: Automatically generate references for schemas
69 passed as strings in responses and request bodies. When using
70 ``MarshmallowPlugin``, if a schema is passed as string, the marshmallow
71 registry is looked up for this schema name and if none is found, the name is
72 assumed to be a reference to a manually created schema and a reference is
73 generated. No exception is raised anymore if the schema name can't be found
74 in the registry. (:pr:`554`)
75
76 4.0.0b1 (2020-09-06)
77 ********************
78
79 Features:
80
81 - *Backwards-incompatible*: Ignore ``location`` field metadata. This attribute
82 was used in webargs but it has now been dropped. A ``Schema`` can now only
83 have a single location. This simplifies the logic in ``OpenAPIConverter``
84 methods, where ``default_in`` argument now becomes ``location``. (:pr:`526`)
85 - *Backwards-incompatible*: Don't document ``int`` format as ``"int32"`` and
86 ``float`` format as ``"float"``, as those are platform-dependent (:pr:`595`).
87
88 Refactoring:
89
90 - ``OpenAPIConverter.field2parameters`` and
91 ``OpenAPIConverter.property2parameter`` are removed.
92 ``OpenAPIConverter.field2parameter`` becomes private. (:pr:`581`)
93
94 Other changes:
95
96 - Drop support for marshmallow 2. Marshmallow 3.x is required. (:pr:`583`)
97 - Drop support for Python 3.5. Python 3.6+ is required. (:pr:`582`)
98
99
100 3.3.2 (2020-08-29)
101 ******************
102
103 Bug fixes:
104
105 - Fix crash when field metadata contains non-string keys (:pr:`596`).
106 Thanks :user:`sanzoghenzo` for the fix.
2107
3108 3.3.1 (2020-06-06)
4109 ******************
8113 - Fix ``MarshmallowPlugin`` crash when ``resolve_schema_dict`` is passed a
9114 schema as string and ``schema_name_resolver`` returns ``None``
10115 (:issue:`566`). Thanks :user:`black3r` for reporting and thanks
11 :user:`Bangterm` for the PR.
116 :user:`Bangertm` for the PR.
12117
13118 3.3.0 (2020-02-14)
14119 ******************
1313 :target: https://apispec.readthedocs.io/
1414 :alt: Documentation
1515
16 .. image:: https://badgen.net/badge/marshmallow/2,3?list=1
16 .. image:: https://badgen.net/badge/marshmallow/3?list=1
1717 :target: https://marshmallow.readthedocs.io/en/latest/upgrading.html
18 :alt: marshmallow 2/3 compatible
18 :alt: marshmallow 3 only
1919
2020 .. image:: https://badgen.net/badge/OAS/2,3?list=1&color=cyan
2121 :target: https://github.com/OAI/OpenAPI-Specification
6666 name = fields.Str()
6767
6868
69 # Optional security scheme support
70 api_key_scheme = {"type": "apiKey", "in": "header", "name": "X-API-Key"}
71 spec.components.security_scheme("ApiKeyAuth", api_key_scheme)
72
73
6974 # Optional Flask support
7075 app = Flask(__name__)
7176
7681 ---
7782 get:
7883 description: Get a random pet
84 security:
85 - ApiKeyAuth: []
7986 responses:
8087 200:
8188 content:
104111 # "/random": {
105112 # "get": {
106113 # "description": "Get a random pet",
114 # "security": [
115 # {
116 # "ApiKeyAuth": []
117 # }
118 # ],
107119 # "responses": {
108120 # "200": {
109121 # "content": {
157169 # }
158170 # }
159171 # }
172 # "securitySchemes": {
173 # "ApiKeyAuth": {
174 # "type": "apiKey",
175 # "in": "header",
176 # "name": "X-API-Key"
177 # }
178 # }
160179 # }
161180 # }
162181 # }
179198 # type: array
180199 # name: {type: string}
181200 # type: object
201 # securitySchemes:
202 # ApiKeyAuth:
203 # in: header
204 # name: X-API-KEY
205 # type: apiKey
182206 # info: {title: Swagger Petstore, version: 1.0.0}
183207 # openapi: 3.0.2
184208 # paths:
190214 # content:
191215 # application/json:
192216 # schema: {$ref: '#/components/schemas/Pet'}
217 # security:
218 # - ApiKeyAuth: []
193219 # tags: []
194220
195221
2626 toxenvs:
2727 - lint
2828
29 - py35-marshmallow2
30 - py35-marshmallow3
29 - py36-marshmallow3
30 - py37-marshmallow3
31 - py38-marshmallow3
32 - py39-marshmallow3
3133
32 - py36-marshmallow3
33
34 - py37-marshmallow3
35
36 - py38-marshmallow2
37 - py38-marshmallow3
38
39 - py38-marshmallowdev
34 - py39-marshmallowdev
4035
4136 - docs
4237 os: linux
4338 - template: job--pypi-release.yml@sloria
4439 parameters:
45 python: "3.8"
40 python: "3.9"
4641 distributions: "sdist bdist_wheel"
4742 dependsOn:
4843 - tox_linux
2525 source_suffix = ".rst"
2626 master_doc = "index"
2727 project = "apispec"
28 copyright = "Steven Loria {:%Y}".format(dt.datetime.utcnow())
28 copyright = f"Steven Loria {dt.datetime.utcnow():%Y}"
2929
3030 version = release = apispec.__version__
3131
4646 name = fields.Str()
4747
4848
49 # Optional security scheme support
50 api_key_scheme = {"type": "apiKey", "in": "header", "name": "X-API-Key"}
51 spec.components.security_scheme("ApiKeyAuth", api_key_scheme)
52
53
4954 # Optional Flask support
5055 app = Flask(__name__)
5156
5661 ---
5762 get:
5863 description: Get a random pet
64 security:
65 - ApiKeyAuth: []
5966 responses:
6067 200:
6168 description: Return a pet
120127 # "type": "string"
121128 # }
122129 # }
123 # }
130 # },
131 # }
132 # },
133 # "securitySchemes": {
134 # "ApiKeyAuth": {
135 # "type": "apiKey",
136 # "in": "header",
137 # "name": "X-API-Key"
124138 # }
125139 # },
126140 # "paths": {
127141 # "/random": {
128142 # "get": {
129143 # "description": "Get a random pet",
144 # "security": [
145 # {
146 # "ApiKeyAuth": []
147 # }
148 # ],
130149 # "responses": {
131150 # "200": {
132151 # "description": "Return a pet",
170189 # name:
171190 # type: string
172191 # type: object
192 # securitySchemes:
193 # ApiKeyAuth:
194 # in: header
195 # name: X-API-KEY
196 # type: apiKey
173197 # paths:
174198 # /random:
175199 # get:
181205 # schema:
182206 # $ref: '#/components/schemas/Pet'
183207 # description: Return a pet
208 # security:
209 # - ApiKeyAuth: []
184210
185211 User Guide
186212 ==========
00 Install
11 =======
22
3 **apispec** requires Python >= 3.5.
3 **apispec** requires Python >= 3.6.
44
55 From the PyPI
66 -------------
279279 .. code-block:: python
280280
281281 def my_custom_field2properties(self, field, **kwargs):
282 """Add an OpenAPI extension flag to MyCustomField instances
283 """
282 """Add an OpenAPI extension flag to MyCustomField instances"""
284283 ret = {}
285284 if isinstance(field, MyCustomField):
286285 if self.openapi_version.major > 2:
2424 .. code-block:: python
2525
2626 from apispec import Path, BasePlugin
27 from apispec.utils import load_operations_from_docstring
27 from apispec.yaml_utils import load_operations_from_docstring
2828
2929
3030 class MyPlugin(BasePlugin):
31 def path_helper(self, path, func, **kwargs):
31 def path_helper(self, path, operations, func, **kwargs):
3232 """Path helper that parses docstrings for operations. Adds a
3333 ``func`` parameter to `apispec.APISpec.path`.
3434 """
35 operations = load_operations_from_docstring(func.__doc__)
36 return Path(path=path, operations=operations)
35 operations.update(load_operations_from_docstring(func.__doc__))
3736
3837
3938 All plugin helpers must accept extra `**kwargs`, allowing custom plugins to define new arguments if required.
5049
5150 class DeprecatedPlugin(BasePlugin):
5251 def operation_helper(self, path, operations, **kwargs):
53 """Operation helper that add `deprecated` flag if in `kwargs`
54 """
52 """Operation helper that add `deprecated` flag if in `kwargs`"""
5553 if kwargs.pop("deprecated", False) is True:
5654 for key, value in operations.items():
5755 value["deprecated"] = True
0 [tool.black]
1 line-length = 88
2 target-version = ['py36', 'py37', 'py38']
33 EXTRAS_REQUIRE = {
44 "yaml": ["PyYAML>=3.10"],
55 "validation": ["prance[osv]>=0.11"],
6 "lint": ["flake8==3.8.2", "flake8-bugbear==20.1.4", "pre-commit~=2.4"],
6 "lint": ["flake8==3.9.2", "flake8-bugbear==21.4.3", "pre-commit~=2.4"],
77 "docs": [
8 "marshmallow>=2.19.2",
9 "pyyaml==5.3.1",
10 "sphinx==3.0.4",
8 "marshmallow>=3.0.0",
9 "pyyaml==5.4.1",
10 "sphinx==4.0.2",
1111 "sphinx-issues==1.2.0",
12 "sphinx-rtd-theme==0.4.3",
12 "sphinx-rtd-theme==0.5.2",
1313 ],
1414 }
1515 EXTRAS_REQUIRE["tests"] = (
1616 EXTRAS_REQUIRE["yaml"]
1717 + EXTRAS_REQUIRE["validation"]
18 + ["marshmallow>=2.19.2", "pytest", "mock"]
18 + ["marshmallow>=3.0.0", "pytest", "mock"]
1919 )
2020 EXTRAS_REQUIRE["dev"] = EXTRAS_REQUIRE["tests"] + EXTRAS_REQUIRE["lint"] + ["tox"]
2121
5959 license="MIT",
6060 zip_safe=False,
6161 keywords="apispec swagger openapi specification oas documentation spec rest api",
62 python_requires=">=3.5",
62 python_requires=">=3.6",
6363 classifiers=[
6464 "License :: OSI Approved :: MIT License",
6565 "Programming Language :: Python :: 3",
66 "Programming Language :: Python :: 3.5",
6766 "Programming Language :: Python :: 3.6",
6867 "Programming Language :: Python :: 3.7",
6968 "Programming Language :: Python :: 3.8",
69 "Programming Language :: Python :: 3.9",
7070 "Programming Language :: Python :: 3 :: Only",
7171 ],
7272 test_suite="tests",
7373 project_urls={
7474 "Funding": "https://opencollective.com/marshmallow",
7575 "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
76 "Tidelift": "https://tidelift.com/subscription/pkg/pypi-apispec?utm_source=pypi-apispec&utm_medium=pypi", # noqa: B950,E501
7777 },
7878 )
22 from .core import APISpec
33 from .plugin import BasePlugin
44
5 __version__ = "3.3.1"
5 __version__ = "4.4.2"
66 __all__ = ["APISpec", "BasePlugin"]
2828 def __init__(self, plugins, openapi_version):
2929 self._plugins = plugins
3030 self.openapi_version = openapi_version
31 self._schemas = {}
32 self._responses = {}
33 self._parameters = {}
34 self._examples = {}
35 self._security_schemes = {}
31 self.schemas = {}
32 self.responses = {}
33 self.parameters = {}
34 self.headers = {}
35 self.examples = {}
36 self.security_schemes = {}
3637
3738 def to_dict(self):
3839 subsections = {
39 "schema": self._schemas,
40 "response": self._responses,
41 "parameter": self._parameters,
42 "example": self._examples,
43 "security_scheme": self._security_schemes,
40 "schema": self.schemas,
41 "response": self.responses,
42 "parameter": self.parameters,
43 "header": self.headers,
44 "example": self.examples,
45 "security_scheme": self.security_schemes,
4446 }
4547 return {
4648 COMPONENT_SUBSECTIONS[self.openapi_version.major][k]: v
6971
7072 https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#schemaObject
7173 """
72 if name in self._schemas:
73 raise DuplicateComponentNameError(
74 'Another schema with name "{}" is already registered.'.format(name)
74 if name in self.schemas:
75 raise DuplicateComponentNameError(
76 f'Another schema with name "{name}" is already registered.'
7577 )
7678 component = component or {}
7779 ret = component.copy()
8183 ret.update(plugin.schema_helper(name, component, **kwargs) or {})
8284 except PluginMethodNotImplementedError:
8385 continue
84 self._schemas[name] = ret
86 self.schemas[name] = ret
8587 return self
8688
8789 def response(self, component_id, component=None, **kwargs):
8991
9092 :param str component_id: ref_id to use as reference
9193 :param dict component: response fields
92 :param dict kwargs: plugin-specific arguments
93 """
94 if component_id in self._responses:
94 :param kwargs: plugin-specific arguments
95 """
96 if component_id in self.responses:
9597 raise DuplicateComponentNameError(
9698 'Another response with name "{}" is already registered.'.format(
9799 component_id
105107 ret.update(plugin.response_helper(component, **kwargs) or {})
106108 except PluginMethodNotImplementedError:
107109 continue
108 self._responses[component_id] = ret
110 self.responses[component_id] = ret
109111 return self
110112
111113 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.
114 """Add a parameter which can be referenced.
115
116 :param str component_id: identifier by which parameter may be referenced.
115117 :param str location: location of the parameter.
116118 :param dict component: parameter fields.
117 :param dict kwargs: plugin-specific arguments
118 """
119 if component_id in self._parameters:
119 :param kwargs: plugin-specific arguments
120 """
121 if component_id in self.parameters:
120122 raise DuplicateComponentNameError(
121123 'Another parameter with name "{}" is already registered.'.format(
122124 component_id
137139 ret.update(plugin.parameter_helper(component, **kwargs) or {})
138140 except PluginMethodNotImplementedError:
139141 continue
140 self._parameters[component_id] = ret
142 self.parameters[component_id] = ret
143 return self
144
145 def header(self, component_id, component):
146 """Add a header which can be referenced.
147
148 :param str component_id: identifier by which header may be referenced.
149 :param dict component: header fields.
150
151 https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.1.md#headerObject
152 """
153 if component_id in self.headers:
154 raise DuplicateComponentNameError(
155 f'Another header with name "{component_id}" is already registered.'
156 )
157 self.headers[component_id] = component
141158 return self
142159
143160 def example(self, name, component, **kwargs):
148165
149166 https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.1.md#exampleObject
150167 """
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
168 if name in self.examples:
169 raise DuplicateComponentNameError(
170 f'Another example with name "{name}" is already registered.'
171 )
172 self.examples[name] = component
156173 return self
157174
158175 def security_scheme(self, component_id, component):
159176 """Add a security scheme which can be referenced.
160177
161178 :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:
179 :param dict component: security scheme fields
180 """
181 if component_id in self.security_schemes:
165182 raise DuplicateComponentNameError(
166183 'Another security scheme with name "{}" is already registered.'.format(
167184 component_id
168185 )
169186 )
170 self._security_schemes[component_id] = component
187 self.security_schemes[component_id] = component
171188 return self
172189
173190
180197 See https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#infoObject
181198 :param str|OpenAPIVersion openapi_version: OpenAPI Specification version.
182199 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
200 :param options: Optional top-level keys
184201 See https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#openapi-object
185202 """
186203
220237 ret = deepupdate(ret, self.options)
221238 return ret
222239
223 def to_yaml(self):
224 """Render the spec to YAML. Requires PyYAML to be installed."""
240 def to_yaml(self, yaml_dump_kwargs=None):
241 """Render the spec to YAML. Requires PyYAML to be installed.
242
243 :param dict yaml_dump_kwargs: Additional keyword arguments to pass to `yaml.dump`
244 """
225245 from .yaml_utils import dict_to_yaml
226246
227 return dict_to_yaml(self.to_dict())
247 return dict_to_yaml(self.to_dict(), yaml_dump_kwargs)
228248
229249 def tag(self, tag):
230 """ Store information about a tag.
250 """Store information about a tag.
231251
232252 :param dict tag: the dictionary storing information about the tag.
233253 """
242262 summary=None,
243263 description=None,
244264 parameters=None,
245 **kwargs
265 **kwargs,
246266 ):
247267 """Add a new path object to the spec.
248268
253273 :param str summary: short summary relevant to all operations in this path
254274 :param str description: long description relevant to all operations in this path
255275 :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`
276 :param kwargs: parameters used by any path helpers see :meth:`register_path_helper`
257277 """
258278 # operations and parameters must be deepcopied because they are mutated
259279 # in clean_operations and operation helpers and path may be called twice
299319 Otherwise, it is assumed to be a reference name as string and the corresponding $ref
300320 string is returned.
301321
302 :param str obj_type: "parameter" or "response"
303 :param dict|str obj: parameter or response in dict form or as ref_id string
322 :param str obj_type: "schema", "parameter", "response" or "security_scheme"
323 :param dict|str obj: object in dict form or as ref_id string
304324 """
305325 if isinstance(obj, dict):
306326 return obj
307327 return build_reference(obj_type, self.openapi_version.major, obj)
328
329 def _resolve_schema(self, obj):
330 """Replace schema reference as string with a $ref if needed."""
331 if not isinstance(obj, dict):
332 return
333 if self.openapi_version.major < 3:
334 if "schema" in obj:
335 obj["schema"] = self.get_ref("schema", obj["schema"])
336 else:
337 if "content" in obj:
338 for content in obj["content"].values():
339 if "schema" in content:
340 content["schema"] = self.get_ref("schema", content["schema"])
341
342 def _resolve_examples(self, obj):
343 """Replace example reference as string with a $ref"""
344 for name, example in obj.get("examples", {}).items():
345 obj["examples"][name] = self.get_ref("example", example)
308346
309347 def clean_parameters(self, parameters):
310348 """Ensure that all parameters with "in" equal to "path" are also required
322360 missing_attrs = [attr for attr in ("name", "in") if attr not in parameter]
323361 if missing_attrs:
324362 raise InvalidParameterError(
325 "Missing keys {} for parameter".format(missing_attrs)
363 f"Missing keys {missing_attrs} for parameter"
326364 )
327365
328366 # OpenAPI Spec 3 and 2 don't allow for duplicated parameters
340378 if parameter["in"] == "path":
341379 parameter["required"] = True
342380
381 self._resolve_examples(parameter)
382
343383 return [self.get_ref("parameter", p) for p in parameters]
344384
345385 def clean_operations(self, operations):
364404 for operation in (operations or {}).values():
365405 if "parameters" in operation:
366406 operation["parameters"] = self.clean_parameters(operation["parameters"])
407 # OAS 3
408 if "requestBody" in operation:
409 self._resolve_schema(operation["requestBody"])
410 for media_type in operation["requestBody"]["content"].values():
411 self._resolve_examples(media_type)
412
367413 if "responses" in operation:
368414 responses = OrderedDict()
369415 for code, response in operation["responses"].items():
372418 except (TypeError, ValueError):
373419 if self.openapi_version.major < 3 and code != "default":
374420 warnings.warn("Non-integer code not allowed in OpenAPI < 3")
375
376 responses[str(code)] = self.get_ref("response", response)
421 self._resolve_schema(response)
422 response = self.get_ref("response", response)
423 for name, header in response.get("headers", {}).items():
424 response["headers"][name] = self.get_ref("header", header)
425 for media_type in response.get("content", {}).values():
426 self._resolve_examples(media_type)
427 responses[str(code)] = response
377428 operation["responses"] = responses
5959 # 'format': 'date-time',
6060 # 'readOnly': True,
6161 # 'type': 'string'},
62 # 'id': {'format': 'int32',
63 # 'readOnly': True,
62 # 'id': {'readOnly': True,
6463 # 'type': 'integer'},
6564 # 'name': {'description': "The user's name",
6665 # 'type': 'string'}},
139138 class MyCustomField(Integer):
140139 # ...
141140
142 @ma_plugin.map_to_openapi_type(Integer) # will map to ('integer', 'int32')
141 @ma_plugin.map_to_openapi_type(Integer) # will map to ('integer', None)
143142 class MyCustomFieldThatsKindaLikeAnInteger(Integer):
144143 # ...
145144 """
187186 return response
188187
189188 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)
189 self.resolver.resolve_operations(operations)
202190
203191 def warn_if_schema_already_in_spec(self, schema_key):
204192 """Method to warn the user if the schema has already been added to the
1919 return schema()
2020 if isinstance(schema, marshmallow.Schema):
2121 return schema
22 try:
23 return marshmallow.class_registry.get_class(schema)()
24 except marshmallow.exceptions.RegistryError:
25 raise ValueError(
26 "{!r} is not a marshmallow.Schema subclass or instance and has not"
27 " been registered in the marshmallow class registry.".format(schema)
28 )
22 return marshmallow.class_registry.get_class(schema)()
2923
3024
3125 def resolve_schema_cls(schema):
3832 return schema
3933 if isinstance(schema, marshmallow.Schema):
4034 return type(schema)
41 try:
42 return marshmallow.class_registry.get_class(schema)
43 except marshmallow.exceptions.RegistryError:
44 raise ValueError(
45 "{!r} is not a marshmallow.Schema subclass or instance and has not"
46 " been registered in the marshmallow class registry.".format(schema)
47 )
35 return marshmallow.class_registry.get_class(schema)
4836
4937
5038 def get_fields(schema, *, exclude_dump_only=False):
6048 fields = copy.deepcopy(schema._declared_fields)
6149 else:
6250 raise ValueError(
63 "{!r} doesn't have either `fields` or `_declared_fields`.".format(schema)
51 f"{schema!r} doesn't have either `fields` or `_declared_fields`."
6452 )
6553 Meta = getattr(schema, "Meta", None)
6654 warn_if_fields_defined_in_meta(fields, Meta)
9078
9179 :param dict fields: A dictionary of fields name field object pairs
9280 :param Meta: the schema's Meta class
93 :param bool exclude_dump_only: whether to filter fields in Meta.dump_only
81 :param bool exclude_dump_only: whether to filter dump_only fields
9482 """
9583 exclude = list(getattr(Meta, "exclude", []))
9684 if exclude_dump_only:
9785 exclude.extend(getattr(Meta, "dump_only", []))
9886
9987 filtered_fields = OrderedDict(
100 (key, value) for key, value in fields.items() if key not in exclude
88 (key, value)
89 for key, value in fields.items()
90 if key not in exclude and not (exclude_dump_only and value.dump_only)
10191 )
10292
10393 return filtered_fields
132122 :param int counter: the counter of the number of recursions
133123 :return: the unique name
134124 """
135 if name not in components._schemas:
125 if name not in components.schemas:
136126 return name
137127 if not counter: # first time through recursion
138128 warnings.warn(
1616
1717 RegexType = type(re.compile(""))
1818
19 MARSHMALLOW_VERSION_INFO = tuple(
20 [int(part) for part in marshmallow.__version__.split(".") if part.isdigit()]
21 )
22
23
2419 # marshmallow field => (JSON Schema type, format)
2520 DEFAULT_FIELD_MAPPING = {
26 marshmallow.fields.Integer: ("integer", "int32"),
21 marshmallow.fields.Integer: ("integer", None),
2722 marshmallow.fields.Number: ("number", None),
28 marshmallow.fields.Float: ("number", "float"),
23 marshmallow.fields.Float: ("number", None),
2924 marshmallow.fields.Decimal: ("number", None),
3025 marshmallow.fields.String: ("string", None),
3126 marshmallow.fields.Boolean: ("boolean", None),
8782
8883 def init_attribute_functions(self):
8984 self.attribute_functions = [
85 # self.field2type_and_format should run first
86 # as other functions may rely on its output
9087 self.field2type_and_format,
9188 self.field2default,
9289 self.field2choices,
211208 else:
212209 default = field.missing
213210 if default is not marshmallow.missing and not callable(default):
214 if MARSHMALLOW_VERSION_INFO[0] >= 3:
215 default = field._serialize(default, None, None)
211 default = field._serialize(default, None, None)
216212 ret["default"] = default
217213 return ret
218214
277273 ] = True
278274 return attributes
279275
280 def field2range(self, field, **kwargs):
276 def field2range(self, field, ret):
281277 """Return the dictionary of OpenAPI field attributes for a set of
282278 :class:`Range <marshmallow.validators.Range>` validators.
283279
294290 )
295291 ]
296292
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
293 min_attr, max_attr = (
294 ("minimum", "maximum")
295 if ret.get("type") in {"number", "integer"}
296 else ("x-minimum", "x-maximum")
297 )
298 return make_min_max_attributes(validators, min_attr, max_attr)
310299
311300 def field2length(self, field, **kwargs):
312301 """Return the dictionary of OpenAPI field attributes for a set of
315304 :param Field field: A marshmallow field.
316305 :rtype: dict
317306 """
318 attributes = {}
319
320307 validators = [
321308 validator
322309 for validator in field.validators
333320 min_attr = "minItems" if is_array else "minLength"
334321 max_attr = "maxItems" if is_array else "maxLength"
335322
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
323 equal_list = [
324 validator.equal for validator in validators if validator.equal is not None
325 ]
326 if equal_list:
327 return {min_attr: equal_list[0], max_attr: equal_list[0]}
328
329 return make_min_max_attributes(validators, min_attr, max_attr)
353330
354331 def field2pattern(self, field, **kwargs):
355 """Return the dictionary of OpenAPI field attributes for a set of
356 :class:`Range <marshmallow.validators.Regexp>` validators.
332 """Return the dictionary of OpenAPI field attributes for a
333 :class:`Regexp <marshmallow.validators.Regexp>` validator.
334
335 If there is more than one such validator, only the first
336 is used in the output spec.
357337
358338 :param Field field: A marshmallow field.
359339 :rtype: dict
396376 metadata = {
397377 key.replace("_", "-") if key.startswith("x_") else key: value
398378 for key, value in field.metadata.items()
379 if isinstance(key, str)
399380 }
400381
401382 # Avoid validation error with "Additional properties not allowed"
435416 """
436417 ret = {}
437418 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)
419 ret["items"] = self.field2property(field.inner)
442420 return ret
443421
444422 def dict2properties(self, field, **kwargs):
452430 """
453431 ret = {}
454432 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
433 value_field = field.value_field
434 if value_field:
435 ret["additionalProperties"] = self.field2property(value_field)
436 return ret
437
438
439 def make_min_max_attributes(validators, min_attr, max_attr):
440 """Return a dictionary of minimum and maximum attributes based on a list
441 of validators. If either minimum or maximum values are not present in any
442 of the validator objects that attribute will be omitted.
443
444 :param validators list: A list of `Marshmallow` validator objects. Each
445 objct is inspected for a minimum and maximum values
446 :param min_attr string: The OpenAPI attribute for the minimum value
447 :param max_attr string: The OpenAPI attribute for the maximum value
448 """
449 attributes = {}
450 min_list = [validator.min for validator in validators if validator.min is not None]
451 max_list = [validator.max for validator in validators if validator.max is not None]
452 if min_list:
453 attributes[min_attr] = max(min_list)
454 if max_list:
455 attributes[max_attr] = min(max_list)
456 return attributes
1818 make_schema_key,
1919 resolve_schema_instance,
2020 get_unique_schema_name,
21 )
22
23
24 MARSHMALLOW_VERSION_INFO = tuple(
25 [int(part) for part in marshmallow.__version__.split(".") if part.isdigit()]
2621 )
2722
2823
5954 # Schema references
6055 self.refs = {}
6156
62 @staticmethod
63 def _observed_name(field, name):
64 """Adjust field name to reflect `dump_to` and `load_from` attributes.
65
66 :param Field field: A marshmallow field.
67 :param str name: Field name
68 :rtype: str
69 """
70 if MARSHMALLOW_VERSION_INFO[0] < 3:
71 # use getattr in case we're running against older versions of marshmallow.
72 dump_to = getattr(field, "dump_to", None)
73 load_from = getattr(field, "load_from", None)
74 return dump_to or load_from or name
75 return field.data_key or name
76
7757 def resolve_nested_schema(self, schema):
7858 """Return the OpenAPI representation of a marshmallow Schema.
7959
8666
8767 :param schema: schema to add to the spec
8868 """
89 schema_instance = resolve_schema_instance(schema)
69 try:
70 schema_instance = resolve_schema_instance(schema)
71 # If schema is a string and is not found in registry,
72 # assume it is a schema reference
73 except marshmallow.exceptions.RegistryError:
74 return build_reference("schema", self.openapi_version.major, schema)
9075 schema_key = make_schema_key(schema_instance)
9176 if schema_key not in self.refs:
9277 name = self.schema_name_resolver(schema)
10994 return self.get_ref_dict(schema_instance)
11095
11196 def schema2parameters(
112 self,
113 schema,
114 *,
115 default_in="body",
116 name="body",
117 required=False,
118 description=None
97 self, schema, *, location, name="body", required=False, description=None
11998 ):
12099 """Return an array of OpenAPI parameters given a given marshmallow
121 :class:`Schema <marshmallow.Schema>`. If `default_in` is "body", then return an array
100 :class:`Schema <marshmallow.Schema>`. If `location` is "body", then return an array
122101 of a single parameter; else return an array of a parameter for each included field in
123102 the :class:`Schema <marshmallow.Schema>`.
124103
104 In OpenAPI 3, only "query", "header", "path" or "cookie" are allowed for the location
105 of parameters. "requestBody" is used when fields are in the body.
106
125107 https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#parameterObject
126108 """
127 openapi_default_in = __location_map__.get(default_in, default_in)
128 if self.openapi_version.major < 3 and openapi_default_in == "body":
129 prop = self.resolve_nested_schema(schema)
130
109 location = __location_map__.get(location, location)
110 # OAS 2 body parameter
111 if location == "body":
131112 param = {
132 "in": openapi_default_in,
113 "in": location,
133114 "required": required,
134115 "name": name,
135 "schema": prop,
116 "schema": self.resolve_nested_schema(schema),
136117 }
137
138118 if description:
139119 param["description"] = description
140
141120 return [param]
142121
143122 assert not getattr(
146125
147126 fields = get_fields(schema, exclude_dump_only=True)
148127
149 return self.fields2parameters(fields, default_in=default_in)
150
151 def fields2parameters(self, fields, *, default_in):
152 """Return an array of OpenAPI parameters given a mapping between field names and
153 :class:`Field <marshmallow.Field>` objects. If `default_in` is "body", then return an array
154 of a single parameter; else return an array of a parameter for each included field in
155 the :class:`Schema <marshmallow.Schema>`.
156
157 In OpenAPI3, only "query", "header", "path" or "cookie" are allowed for the location
158 of parameters. In OpenAPI 3, "requestBody" is used when fields are in the body.
159
160 This function always returns a list, with a parameter
161 for each included field in the :class:`Schema <marshmallow.Schema>`.
162
163 https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#parameterObject
164 """
165 parameters = []
166 body_param = None
167 for field_name, field_obj in fields.items():
168 if field_obj.dump_only:
169 continue
170 param = self.field2parameter(
128 return [
129 self._field2parameter(
171130 field_obj,
172 name=self._observed_name(field_obj, field_name),
173 default_in=default_in,
131 name=field_obj.data_key or field_name,
132 location=location,
174133 )
175 if (
176 self.openapi_version.major < 3
177 and param["in"] == "body"
178 and body_param is not None
179 ):
180 body_param["schema"]["properties"].update(param["schema"]["properties"])
181 required_fields = param["schema"].get("required", [])
182 if required_fields:
183 body_param["schema"].setdefault("required", []).extend(
184 required_fields
185 )
186 else:
187 if self.openapi_version.major < 3 and param["in"] == "body":
188 body_param = param
189 parameters.append(param)
190 return parameters
191
192 def field2parameter(self, field, *, name, default_in):
134 for field_name, field_obj in fields.items()
135 ]
136
137 def _field2parameter(self, field, *, name, location):
193138 """Return an OpenAPI parameter as a `dict`, given a marshmallow
194139 :class:`Field <marshmallow.Field>`.
195140
196141 https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#parameterObject
197142 """
198 location = field.metadata.get("location", None)
143 ret = {"in": location, "name": name}
144
145 partial = getattr(field.parent, "partial", False)
146 ret["required"] = field.required and (
147 not partial or (is_collection(partial) and field.name not in partial)
148 )
149
199150 prop = self.field2property(field)
200 return self.property2parameter(
201 prop,
202 name=name,
203 required=field.required,
204 multiple=isinstance(field, marshmallow.fields.List),
205 location=location,
206 default_in=default_in,
207 )
208
209 def property2parameter(
210 self, prop, *, name, required, multiple, location, default_in
211 ):
212 """Return the Parameter Object definition for a JSON Schema property.
213
214 https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#parameterObject
215
216 :param dict prop: JSON Schema property
217 :param str name: Field name
218 :param bool required: Parameter is required
219 :param bool multiple: Parameter is repeated
220 :param str location: Location to look for ``name``
221 :param str default_in: Default location to look for ``name``
222 :raise: TranslationError if arg object cannot be translated to a Parameter Object schema.
223 :rtype: dict, a Parameter Object
224 """
225 openapi_default_in = __location_map__.get(default_in, default_in)
226 openapi_location = __location_map__.get(location, openapi_default_in)
227 ret = {"in": openapi_location, "name": name}
228
229 if openapi_location == "body":
230 ret["required"] = False
231 ret["name"] = "body"
232 ret["schema"] = {"type": "object", "properties": {name: prop}}
233 if required:
234 ret["schema"]["required"] = [name]
151 multiple = isinstance(field, marshmallow.fields.List)
152
153 if self.openapi_version.major < 3:
154 if multiple:
155 ret["collectionFormat"] = "multi"
156 ret.update(prop)
235157 else:
236 ret["required"] = required
237 if self.openapi_version.major < 3:
238 if multiple:
239 ret["collectionFormat"] = "multi"
240 ret.update(prop)
241 else:
242 if multiple:
243 ret["explode"] = True
244 ret["style"] = "form"
245 if prop.get("description", None):
246 ret["description"] = prop.pop("description")
247 ret["schema"] = prop
158 if multiple:
159 ret["explode"] = True
160 ret["style"] = "form"
161 if prop.get("description", None):
162 ret["description"] = prop.pop("description")
163 ret["schema"] = prop
248164 return ret
249165
250166 def schema2jsonschema(self, schema):
268184 jsonschema["title"] = Meta.title
269185 if hasattr(Meta, "description"):
270186 jsonschema["description"] = Meta.description
187 if hasattr(Meta, "unknown") and Meta.unknown != marshmallow.EXCLUDE:
188 jsonschema["additionalProperties"] = Meta.unknown == marshmallow.INCLUDE
271189
272190 return jsonschema
273191
285203 jsonschema = {"type": "object", "properties": OrderedDict() if ordered else {}}
286204
287205 for field_name, field_obj in fields.items():
288 observed_field_name = self._observed_name(field_obj, field_name)
289 property = self.field2property(field_obj)
290 jsonschema["properties"][observed_field_name] = property
206 observed_field_name = field_obj.data_key or field_name
207 prop = self.field2property(field_obj)
208 jsonschema["properties"][observed_field_name] = prop
291209
292210 if field_obj.required:
293211 if not partial or (
1414 self.openapi_version = openapi_version
1515 self.converter = converter
1616
17 def resolve_operations(self, operations, **kwargs):
18 """Resolve marshmallow Schemas in a dict mapping operation to OpenApi `Operation Object
19 https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#operationObject`_"""
20
21 for operation in operations.values():
22 if not isinstance(operation, dict):
23 continue
24 if "parameters" in operation:
25 operation["parameters"] = self.resolve_parameters(
26 operation["parameters"]
27 )
28 if self.openapi_version.major >= 3:
29 self.resolve_callback(operation.get("callbacks", {}))
30 if "requestBody" in operation:
31 self.resolve_schema(operation["requestBody"])
32 for response in operation.get("responses", {}).values():
33 self.resolve_response(response)
34
35 def resolve_callback(self, callbacks):
36 """Resolve marshmallow Schemas in a dict mapping callback name to OpenApi `Callback Object
37 https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#callbackObject`_.
38
39 This is done recursively, so it is possible to define callbacks in your callbacks.
40
41 Example: ::
42
43 #Input
44 {
45 "userEvent": {
46 "https://my.example/user-callback": {
47 "post": {
48 "requestBody": {
49 "content": {
50 "application/json": {
51 "schema": UserSchema
52 }
53 }
54 }
55 },
56 }
57 }
58 }
59
60 #Output
61 {
62 "userEvent": {
63 "https://my.example/user-callback": {
64 "post": {
65 "requestBody": {
66 "content": {
67 "application/json": {
68 "schema": {
69 "$ref": "#/components/schemas/User"
70 }
71 }
72 }
73 }
74 },
75 }
76 }
77 }
78
79
80 """
81 for callback in callbacks.values():
82 if isinstance(callback, dict):
83 for path in callback.values():
84 self.resolve_operations(path)
85
1786 def resolve_parameters(self, parameters):
1887 """Resolve marshmallow Schemas in a list of OpenAPI `Parameter Objects
1988 <https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#parameter-object>`_.
37106
38107 #Output
39108 [
40 {"in": "query", "name": "id", "required": False, "schema": {"type": "integer", "format": "int32"}},
109 {"in": "query", "name": "id", "required": False, "schema": {"type": "integer"}},
41110 {"in": "query", "name": "name", "required": False, "schema": {"type": "string"}}
42111 ]
43112
75144 ):
76145 schema_instance = resolve_schema_instance(parameter.pop("schema"))
77146 resolved += self.converter.schema2parameters(
78 schema_instance, default_in=parameter.pop("in"), **parameter
147 schema_instance, location=parameter.pop("in"), **parameter
79148 )
80149 else:
81150 self.resolve_schema(parameter)
161230
162231 def resolve_schema_dict(self, schema):
163232 """Resolve a marshmallow Schema class, object, or a string that resolves
164 to a Schema class or an OpenAPI Schema Object containing one of the above
165 to an OpenAPI Schema Object or Reference Object.
233 to a Schema class or a schema reference or an OpenAPI Schema Object
234 containing one of the above to an OpenAPI Schema Object or Reference Object.
166235
167236 If the input is a marshmallow Schema class, object or a string that resolves
168237 to a Schema class the Schema will be translated to an OpenAPI Schema Object
1717
1818 :param str name: Identifier by which schema may be referenced
1919 :param dict definition: Schema definition
20 :param dict kwargs: All additional keywords arguments sent to `APISpec.schema()`
20 :param kwargs: All additional keywords arguments sent to `APISpec.schema()`
2121 """
2222 raise PluginMethodNotImplementedError
2323
2525 """May return response component description as a dict.
2626
2727 :param dict response: Response fields
28 :param dict kwargs: All additional keywords arguments sent to `APISpec.response()`
28 :param kwargs: All additional keywords arguments sent to `APISpec.response()`
2929 """
3030 raise PluginMethodNotImplementedError
3131
3333 """May return parameter component description as a dict.
3434
3535 :param dict parameter: Parameter fields
36 :param dict kwargs: All additional keywords arguments sent to `APISpec.parameter()`
36 :param kwargs: All additional keywords arguments sent to `APISpec.parameter()`
3737 """
3838 raise PluginMethodNotImplementedError
3939
4646 :param list parameters: A `list` of parameters objects or references for the path. See
4747 https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#parameterObject
4848 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()`
49 :param kwargs: All additional keywords arguments sent to `APISpec.path()`
5050
5151 Return value should be a string or None. If a string is returned, it
5252 is set as the path.
6363 :param str path: Path to the resource
6464 :param dict operations: A `dict` mapping HTTP methods to operation object.
6565 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()`
66 :param kwargs: All additional keywords arguments sent to `APISpec.path()`
6767 """
6868 raise PluginMethodNotImplementedError
1919 "schema": "schemas",
2020 "response": "responses",
2121 "parameter": "parameters",
22 "header": "headers",
2223 "example": "examples",
2324 "security_scheme": "securitySchemes",
2425 },
102103 < self.MAX_EXCLUSIVE_VERSION
103104 ):
104105 raise exceptions.APISpecError(
105 "Not a valid OpenAPI version number: {}".format(openapi_version)
106 f"Not a valid OpenAPI version number: {openapi_version}"
106107 )
107108 super().__init__(openapi_version)
108109
1414 yaml.add_representer(OrderedDict, YAMLDumper._represent_dict, Dumper=YAMLDumper)
1515
1616
17 def dict_to_yaml(dic):
18 return yaml.dump(dic, Dumper=YAMLDumper)
17 def dict_to_yaml(dic, yaml_dump_kwargs=None):
18 if yaml_dump_kwargs is None:
19 yaml_dump_kwargs = {}
20 return yaml.dump(dic, Dumper=YAMLDumper, **yaml_dump_kwargs)
1921
2022
2123 def load_yaml_from_docstring(docstring):
00 from marshmallow import Schema, fields
1
2 from apispec.ext.marshmallow.openapi import MARSHMALLOW_VERSION_INFO
31
42
53 class PetSchema(Schema):
3937
4038 class SelfReferencingSchema(Schema):
4139 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))
40 single = fields.Nested(lambda: SelfReferencingSchema())
41 many = fields.Nested(lambda: SelfReferencingSchema(many=True))
4842
4943
5044 class OrderedSchema(Schema):
1616 get_examples,
1717 get_paths,
1818 get_parameters,
19 get_headers,
1920 get_responses,
2021 get_security_schemes,
2122 build_ref,
5960 version="1.0.0",
6061 openapi_version=openapi_version,
6162 info={"description": description},
62 **security_kwargs
63 **security_kwargs,
6364 )
6465
6566
201202 spec.components.response("test_response")
202203
203204 def test_parameter(self, spec):
205 # Note: this is an OpenAPI v2 parameter header
206 # but is does the job for the test even for OpenAPI v3
204207 parameter = {"format": "int64", "type": "integer"}
205208 spec.components.parameter("PetId", "path", parameter)
206209 params = get_parameters(spec)
225228 match='Another parameter with name "test_parameter" is already registered.',
226229 ):
227230 spec.components.parameter("test_parameter", "path")
231
232 # Referenced headers are only supported in OAS 3.x
233 @pytest.mark.parametrize("spec", ("3.0.0",), indirect=True)
234 def test_header(self, spec):
235 header = {"schema": {"type": "string"}}
236 spec.components.header("test_header", header.copy())
237 headers = get_headers(spec)
238 assert headers["test_header"] == header
239
240 # Referenced headers are only supported in OAS 3.x
241 @pytest.mark.parametrize("spec", ("3.0.0",), indirect=True)
242 def test_header_is_chainable(self, spec):
243 header = {"schema": {"type": "string"}}
244 spec.components.header("header1", header).header("header2", header)
245 headers = get_headers(spec)
246 assert "header1" in headers
247 assert "header2" in headers
248
249 # Referenced headers are only supported in OAS 3.x
250 @pytest.mark.parametrize("spec", ("3.0.0",), indirect=True)
251 def test_header_duplicate_name(self, spec):
252 spec.components.header("test_header", {"schema": {"type": "string"}})
253 with pytest.raises(
254 DuplicateComponentNameError,
255 match='Another header with name "test_header" is already registered.',
256 ):
257 spec.components.header("test_header", {"schema": {"type": "integer"}})
228258
229259 # Referenced examples are only supported in OAS 3.x
230260 @pytest.mark.parametrize("spec", ("3.0.0",), indirect=True)
306336 }
307337 ],
308338 "responses": {
309 "200": {"schema": "Pet", "description": "successful operation"},
339 "200": {"description": "successful operation"},
310340 "400": {"description": "Invalid ID supplied"},
311341 "404": {"description": "Pet not found"},
312342 },
617647 with pytest.raises(APISpecError, match=message):
618648 spec.path("/pet/{petId}", operations={"dummy": {}})
619649
650 def test_path_resolve_response_schema(self, spec):
651 schema = {"schema": "PetSchema"}
652 if spec.openapi_version.major >= 3:
653 schema = {"content": {"application/json": schema}}
654 spec.path("/pet/{petId}", operations={"get": {"responses": {"200": schema}}})
655 resp = get_paths(spec)["/pet/{petId}"]["get"]["responses"]["200"]
656 if spec.openapi_version.major < 3:
657 schema = resp["schema"]
658 else:
659 schema = resp["content"]["application/json"]["schema"]
660 assert schema == build_ref(spec, "schema", "PetSchema")
661
662 # requestBody only exists in OAS 3
663 @pytest.mark.parametrize("spec", ("3.0.0",), indirect=True)
664 def test_path_resolve_request_body(self, spec):
665 spec.path(
666 "/pet/{petId}",
667 operations={
668 "get": {
669 "requestBody": {
670 "content": {"application/json": {"schema": "PetSchema"}}
671 }
672 }
673 },
674 )
675 assert get_paths(spec)["/pet/{petId}"]["get"]["requestBody"]["content"][
676 "application/json"
677 ]["schema"] == build_ref(spec, "schema", "PetSchema")
678
679 # "headers" components section only exists in OAS 3
680 @pytest.mark.parametrize("spec", ("3.0.0",), indirect=True)
681 def test_path_resolve_response_header(self, spec):
682 response = {"headers": {"header_1": "Header_1"}}
683 spec.path("/pet/{petId}", operations={"get": {"responses": {"200": response}}})
684 resp = get_paths(spec)["/pet/{petId}"]["get"]["responses"]["200"]
685 header_1 = resp["headers"]["header_1"]
686 assert header_1 == build_ref(spec, "header", "Header_1")
687
688 # "examples" components section only exists in OAS 3
689 @pytest.mark.parametrize("spec", ("3.0.0",), indirect=True)
690 def test_path_resolve_response_examples(self, spec):
691 response = {
692 "content": {"application/json": {"examples": {"example_1": "Example_1"}}}
693 }
694 spec.path("/pet/{petId}", operations={"get": {"responses": {"200": response}}})
695 resp = get_paths(spec)["/pet/{petId}"]["get"]["responses"]["200"]
696 example_1 = resp["content"]["application/json"]["examples"]["example_1"]
697 assert example_1 == build_ref(spec, "example", "Example_1")
698
699 # "examples" components section only exists in OAS 3
700 @pytest.mark.parametrize("spec", ("3.0.0",), indirect=True)
701 def test_path_resolve_request_body_examples(self, spec):
702 request_body = {
703 "content": {"application/json": {"examples": {"example_1": "Example_1"}}}
704 }
705 spec.path("/pet/{petId}", operations={"get": {"requestBody": request_body}})
706 reqbdy = get_paths(spec)["/pet/{petId}"]["get"]["requestBody"]
707 example_1 = reqbdy["content"]["application/json"]["examples"]["example_1"]
708 assert example_1 == build_ref(spec, "example", "Example_1")
709
710 # "examples" components section only exists in OAS 3
711 @pytest.mark.parametrize("spec", ("3.0.0",), indirect=True)
712 def test_path_resolve_parameter_examples(self, spec):
713 parameter = {
714 "name": "test",
715 "in": "query",
716 "examples": {"example_1": "Example_1"},
717 }
718 spec.path("/pet/{petId}", operations={"get": {"parameters": [parameter]}})
719 param = get_paths(spec)["/pet/{petId}"]["get"]["parameters"][0]
720 example_1 = param["examples"]["example_1"]
721 assert example_1 == build_ref(spec, "example", "Example_1")
722
620723
621724 class TestPlugins:
622725 @staticmethod
741844 self.output = output
742845
743846 def path_helper(self, path, operations, **kwargs):
744 self.output.append("plugin_{}_path".format(self.index))
847 self.output.append(f"plugin_{self.index}_path")
745848
746849 def operation_helper(self, path, operations, **kwargs):
747 self.output.append("plugin_{}_operations".format(self.index))
850 self.output.append(f"plugin_{self.index}_operations")
748851
749852 def test_plugins_order(self):
750853 """Test plugins execution order in APISpec.path
66
77 from apispec import APISpec
88 from apispec.ext.marshmallow import MarshmallowPlugin
9 from apispec.ext.marshmallow.openapi import MARSHMALLOW_VERSION_INFO
109 from apispec.ext.marshmallow import common
1110 from apispec.exceptions import APISpecError
1211 from .schemas import (
232231 reference = parameter["content"]["application/json"]["schema"]
233232 assert reference == build_ref(spec, "schema", "Pet")
234233
235 resolved_schema = spec.components._schemas["Pet"]
234 resolved_schema = spec.components.schemas["Pet"]
236235 assert resolved_schema["properties"]["name"]["type"] == "string"
237236 assert resolved_schema["properties"]["password"]["type"] == "string"
238237 assert resolved_schema["properties"]["id"]["type"] == "integer"
253252 reference = response["content"]["application/json"]["schema"]
254253 assert reference == build_ref(spec, "schema", "Pet")
255254
256 resolved_schema = spec.components._schemas["Pet"]
255 resolved_schema = spec.components.schemas["Pet"]
257256 assert resolved_schema["properties"]["id"]["type"] == "integer"
258257 assert resolved_schema["properties"]["name"]["type"] == "string"
259258 assert resolved_schema["properties"]["password"]["type"] == "string"
266265 reference = response["headers"]["PetHeader"]["schema"]
267266 assert reference == build_ref(spec, "schema", "Pet")
268267
269 resolved_schema = spec.components._schemas["Pet"]
268 resolved_schema = spec.components.schemas["Pet"]
270269 assert resolved_schema["properties"]["id"]["type"] == "integer"
271270 assert resolved_schema["properties"]["name"]["type"] == "string"
272271 assert resolved_schema["properties"]["password"]["type"] == "string"
327326
328327
329328 class TestOperationHelper:
329 @pytest.fixture
330 def make_pet_callback_spec(self, spec_fixture):
331 def _make_pet_spec(operations):
332 spec_fixture.spec.path(
333 path="/pet",
334 operations={
335 "post": {"callbacks": {"petEvent": {"petCallbackUrl": operations}}}
336 },
337 )
338 return spec_fixture
339
340 return _make_pet_spec
341
330342 @pytest.mark.parametrize(
331343 "pet_schema",
332344 (PetSchema, PetSchema(), PetSchema(many=True), "tests.schemas.PetSchema"),
363375 header_reference = get["responses"]["200"]["headers"]["PetHeader"]["schema"]
364376 assert schema_reference == build_ref(spec_fixture.spec, "schema", "Pet")
365377 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"]
378 assert len(spec_fixture.spec.components.schemas) == 1
379 resolved_schema = spec_fixture.spec.components.schemas["Pet"]
368380 assert resolved_schema == spec_fixture.openapi.schema2jsonschema(PetSchema)
369381 assert get["responses"]["200"]["description"] == "successful operation"
370382
412424
413425 assert schema_reference == build_ref(spec_fixture.spec, "schema", "Pet")
414426 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"]
427 assert len(spec_fixture.spec.components.schemas) == 1
428 resolved_schema = spec_fixture.spec.components.schemas["Pet"]
429 assert resolved_schema == spec_fixture.openapi.schema2jsonschema(PetSchema)
430 assert get["responses"]["200"]["description"] == "successful operation"
431
432 @pytest.mark.parametrize(
433 "pet_schema",
434 (PetSchema, PetSchema(), PetSchema(many=True), "tests.schemas.PetSchema"),
435 )
436 @pytest.mark.parametrize("spec_fixture", ("3.0.0",), indirect=True)
437 def test_callback_schema_v3(self, make_pet_callback_spec, pet_schema):
438 spec_fixture = make_pet_callback_spec(
439 {
440 "get": {
441 "responses": {
442 "200": {
443 "content": {"application/json": {"schema": pet_schema}},
444 "description": "successful operation",
445 "headers": {"PetHeader": {"schema": pet_schema}},
446 }
447 }
448 }
449 }
450 )
451 p = get_paths(spec_fixture.spec)["/pet"]
452 c = p["post"]["callbacks"]["petEvent"]["petCallbackUrl"]
453 get = c["get"]
454 if isinstance(pet_schema, Schema) and pet_schema.many is True:
455 assert (
456 get["responses"]["200"]["content"]["application/json"]["schema"]["type"]
457 == "array"
458 )
459 schema_reference = get["responses"]["200"]["content"]["application/json"][
460 "schema"
461 ]["items"]
462 assert (
463 get["responses"]["200"]["headers"]["PetHeader"]["schema"]["type"]
464 == "array"
465 )
466 header_reference = get["responses"]["200"]["headers"]["PetHeader"][
467 "schema"
468 ]["items"]
469 else:
470 schema_reference = get["responses"]["200"]["content"]["application/json"][
471 "schema"
472 ]
473 header_reference = get["responses"]["200"]["headers"]["PetHeader"]["schema"]
474
475 assert schema_reference == build_ref(spec_fixture.spec, "schema", "Pet")
476 assert header_reference == build_ref(spec_fixture.spec, "schema", "Pet")
477 assert len(spec_fixture.spec.components.schemas) == 1
478 resolved_schema = spec_fixture.spec.components.schemas["Pet"]
417479 assert resolved_schema == spec_fixture.openapi.schema2jsonschema(PetSchema)
418480 assert get["responses"]["200"]["description"] == "successful operation"
419481
439501 p = get_paths(spec_fixture.spec)["/pet"]
440502 get = p["get"]
441503 assert get["parameters"] == spec_fixture.openapi.schema2parameters(
442 PetSchema(), default_in="query"
504 PetSchema(), location="query"
443505 )
444506 post = p["post"]
445507 assert post["parameters"] == spec_fixture.openapi.schema2parameters(
446508 PetSchema,
447 default_in="body",
509 location="body",
448510 required=True,
449511 name="pet",
450512 description="a pet schema",
468530 p = get_paths(spec_fixture.spec)["/pet"]
469531 get = p["get"]
470532 assert get["parameters"] == spec_fixture.openapi.schema2parameters(
471 PetSchema(), default_in="query"
533 PetSchema(), location="query"
472534 )
473535 for parameter in get["parameters"]:
474536 description = parameter.get("description", False)
485547 assert post["requestBody"]["description"] == "a pet schema"
486548 assert post["requestBody"]["required"]
487549
550 @pytest.mark.parametrize("spec_fixture", ("3.0.0",), indirect=True)
551 def test_callback_schema_expand_parameters_v3(self, make_pet_callback_spec):
552 spec_fixture = make_pet_callback_spec(
553 {
554 "get": {"parameters": [{"in": "query", "schema": PetSchema}]},
555 "post": {
556 "requestBody": {
557 "description": "a pet schema",
558 "required": True,
559 "content": {"application/json": {"schema": PetSchema}},
560 }
561 },
562 }
563 )
564 p = get_paths(spec_fixture.spec)["/pet"]
565 c = p["post"]["callbacks"]["petEvent"]["petCallbackUrl"]
566 get = c["get"]
567 assert get["parameters"] == spec_fixture.openapi.schema2parameters(
568 PetSchema(), location="query"
569 )
570 for parameter in get["parameters"]:
571 description = parameter.get("description", False)
572 assert description
573 name = parameter["name"]
574 assert description == PetSchema.description[name]
575 post = c["post"]
576 post_schema = spec_fixture.marshmallow_plugin.resolver.resolve_schema_dict(
577 PetSchema
578 )
579 assert (
580 post["requestBody"]["content"]["application/json"]["schema"] == post_schema
581 )
582 assert post["requestBody"]["description"] == "a pet schema"
583 assert post["requestBody"]["required"]
584
488585 @pytest.mark.parametrize("spec_fixture", ("2.0",), indirect=True)
489586 def test_schema_uses_ref_if_available_v2(self, spec_fixture):
490587 spec_fixture.spec.components.schema("Pet", schema=PetSchema)
510607 },
511608 )
512609 get = get_paths(spec_fixture.spec)["/pet"]["get"]
610 assert get["responses"]["200"]["content"]["application/json"][
611 "schema"
612 ] == build_ref(spec_fixture.spec, "schema", "Pet")
613
614 @pytest.mark.parametrize("spec_fixture", ("3.0.0",), indirect=True)
615 def test_callback_schema_uses_ref_if_available_v3(self, make_pet_callback_spec):
616 spec_fixture = make_pet_callback_spec(
617 {
618 "get": {
619 "responses": {
620 "200": {"content": {"application/json": {"schema": PetSchema}}}
621 }
622 }
623 }
624 )
625 p = get_paths(spec_fixture.spec)["/pet"]
626 c = p["post"]["callbacks"]["petEvent"]["petCallbackUrl"]
627 get = c["get"]
513628 assert get["responses"]["200"]["content"]["application/json"][
514629 "schema"
515630 ] == build_ref(spec_fixture.spec, "schema", "Pet")
558673 ] == build_ref(spec, "schema", "Pet")
559674
560675 @pytest.mark.parametrize(
561 "pet_schema", (PetSchema, PetSchema(), "tests.schemas.PetSchema"),
676 "pet_schema",
677 (PetSchema, PetSchema(), "tests.schemas.PetSchema"),
562678 )
563679 def test_schema_name_resolver_returns_none_v2(self, pet_schema):
564680 def resolver(schema):
578694 assert "properties" in get["responses"]["200"]["schema"]
579695
580696 @pytest.mark.parametrize(
581 "pet_schema", (PetSchema, PetSchema(), "tests.schemas.PetSchema"),
697 "pet_schema",
698 (PetSchema, PetSchema(), "tests.schemas.PetSchema"),
582699 )
583700 def test_schema_name_resolver_returns_none_v3(self, pet_schema):
584701 def resolver(schema):
605722 "properties"
606723 in get["responses"]["200"]["content"]["application/json"]["schema"]
607724 )
725
726 def test_callback_schema_uses_ref_if_available_name_resolver_returns_none_v3(self):
727 def resolver(schema):
728 return None
729
730 spec = APISpec(
731 title="Test auto-reference",
732 version="0.1",
733 openapi_version="3.0.0",
734 plugins=(MarshmallowPlugin(schema_name_resolver=resolver),),
735 )
736 spec.components.schema("Pet", schema=PetSchema)
737 spec.path(
738 path="/pet",
739 operations={
740 "post": {
741 "callbacks": {
742 "petEvent": {
743 "petCallbackUrl": {
744 "get": {
745 "responses": {
746 "200": {
747 "content": {
748 "application/json": {
749 "schema": PetSchema
750 }
751 }
752 }
753 }
754 }
755 }
756 }
757 }
758 }
759 },
760 )
761 p = get_paths(spec)["/pet"]
762 c = p["post"]["callbacks"]["petEvent"]["petCallbackUrl"]
763 get = c["get"]
764 assert get["responses"]["200"]["content"]["application/json"][
765 "schema"
766 ] == build_ref(spec, "schema", "Pet")
608767
609768 @pytest.mark.parametrize("spec_fixture", ("2.0",), indirect=True)
610769 def test_schema_uses_ref_in_parameters_and_request_body_if_available_v2(
645804 p = get_paths(spec_fixture.spec)["/pet"]
646805 assert "schema" in p["get"]["parameters"][0]
647806 post = p["post"]
807 schema_ref = post["requestBody"]["content"]["application/json"]["schema"]
808 assert schema_ref == build_ref(spec_fixture.spec, "schema", "Pet")
809
810 @pytest.mark.parametrize("spec_fixture", ("3.0.0",), indirect=True)
811 def test_callback_schema_uses_ref_in_parameters_and_request_body_if_available_v3(
812 self, make_pet_callback_spec
813 ):
814 spec_fixture = make_pet_callback_spec(
815 {
816 "get": {"parameters": [{"in": "query", "schema": PetSchema}]},
817 "post": {
818 "requestBody": {
819 "content": {"application/json": {"schema": PetSchema}}
820 }
821 },
822 }
823 )
824 p = get_paths(spec_fixture.spec)["/pet"]
825 c = p["post"]["callbacks"]["petEvent"]["petCallbackUrl"]
826 assert "schema" in c["get"]["parameters"][0]
827 post = c["post"]
648828 schema_ref = post["requestBody"]["content"]["application/json"]["schema"]
649829 assert schema_ref == build_ref(spec_fixture.spec, "schema", "Pet")
650830
720900 ]
721901 assert response_schema == resolved_schema
722902
903 @pytest.mark.parametrize("spec_fixture", ("3.0.0",), indirect=True)
904 def test_callback_schema_array_uses_ref_if_available_v3(
905 self, make_pet_callback_spec
906 ):
907 spec_fixture = make_pet_callback_spec(
908 {
909 "get": {
910 "parameters": [
911 {
912 "name": "Pet",
913 "in": "query",
914 "content": {
915 "application/json": {
916 "schema": {"type": "array", "items": PetSchema}
917 }
918 },
919 }
920 ],
921 "responses": {
922 "200": {
923 "content": {
924 "application/json": {
925 "schema": {"type": "array", "items": PetSchema}
926 }
927 }
928 }
929 },
930 }
931 }
932 )
933 p = get_paths(spec_fixture.spec)["/pet"]
934 c = p["post"]["callbacks"]["petEvent"]["petCallbackUrl"]
935 get = c["get"]
936 assert len(get["parameters"]) == 1
937 resolved_schema = {
938 "type": "array",
939 "items": build_ref(spec_fixture.spec, "schema", "Pet"),
940 }
941 request_schema = get["parameters"][0]["content"]["application/json"]["schema"]
942 assert request_schema == resolved_schema
943 response_schema = get["responses"]["200"]["content"]["application/json"][
944 "schema"
945 ]
946 assert response_schema == resolved_schema
947
723948 @pytest.mark.parametrize("spec_fixture", ("2.0",), indirect=True)
724949 def test_schema_partially_v2(self, spec_fixture):
725950 spec_fixture.spec.components.schema("Pet", schema=PetSchema)
7841009 },
7851010 }
7861011
1012 @pytest.mark.parametrize("spec_fixture", ("3.0.0",), indirect=True)
1013 def test_callback_schema_partially_v3(self, make_pet_callback_spec):
1014 spec_fixture = make_pet_callback_spec(
1015 {
1016 "get": {
1017 "responses": {
1018 "200": {
1019 "content": {
1020 "application/json": {
1021 "schema": {
1022 "type": "object",
1023 "properties": {
1024 "mother": PetSchema,
1025 "father": PetSchema,
1026 },
1027 }
1028 }
1029 }
1030 }
1031 }
1032 }
1033 }
1034 )
1035 p = get_paths(spec_fixture.spec)["/pet"]
1036 c = p["post"]["callbacks"]["petEvent"]["petCallbackUrl"]
1037 get = c["get"]
1038 assert get["responses"]["200"]["content"]["application/json"]["schema"] == {
1039 "type": "object",
1040 "properties": {
1041 "mother": build_ref(spec_fixture.spec, "schema", "Pet"),
1042 "father": build_ref(spec_fixture.spec, "schema", "Pet"),
1043 },
1044 }
1045
7871046 def test_parameter_reference(self, spec_fixture):
7881047 if spec_fixture.spec.openapi_version.major < 3:
7891048 param = {"schema": PetSchema}
8181077
8191078 def test_schema_global_state_untouched_2parameters(self, spec_fixture):
8201079 assert get_nested_schema(RunSchema, "sample") is None
821 data = spec_fixture.openapi.schema2parameters(RunSchema)
1080 data = spec_fixture.openapi.schema2parameters(RunSchema, location="json")
8221081 json.dumps(data)
8231082 assert get_nested_schema(RunSchema, "sample") is None
1083
1084 def test_resolve_schema_dict_ref_as_string(self, spec):
1085 schema = {"schema": "PetSchema"}
1086 if spec.openapi_version.major >= 3:
1087 schema = {"content": {"application/json": schema}}
1088 spec.path("/pet/{petId}", operations={"get": {"responses": {"200": schema}}})
1089 resp = get_paths(spec)["/pet/{petId}"]["get"]["responses"]["200"]
1090 if spec.openapi_version.major < 3:
1091 schema = resp["schema"]
1092 else:
1093 schema = resp["content"]["application/json"]["schema"]
1094 assert schema == build_ref(spec, "schema", "PetSchema")
8241095
8251096
8261097 class TestCircularReference:
8821153 assert "default" not in props["numbers"]
8831154
8841155
885 @pytest.mark.skipif(
886 MARSHMALLOW_VERSION_INFO[0] < 3, reason="Values ignored in marshmallow 2"
887 )
8881156 class TestDictValues:
8891157 def test_dict_values_resolve_to_additional_properties(self, spec):
8901158 class SchemaWithDict(Schema):
22
33 import pytest
44 from marshmallow import fields, validate
5
6 from apispec.ext.marshmallow.openapi import MARSHMALLOW_VERSION_INFO
75
86 from .schemas import CategorySchema, CustomList, CustomStringField, CustomIntegerField
97 from .utils import build_ref
6058 @pytest.mark.parametrize(
6159 ("FieldClass", "expected_format"),
6260 [
63 (fields.Integer, "int32"),
64 (fields.Float, "float"),
6561 (fields.UUID, "uuid"),
6662 (fields.DateTime, "date-time"),
6763 (fields.Date, "date"),
9490
9591
9692 def test_datetime_field_with_missing(spec_fixture):
97 if MARSHMALLOW_VERSION_INFO[0] < 3:
98 field = fields.Date(missing=dt.date(2014, 7, 18).isoformat())
99 else:
100 field = fields.Date(missing=dt.date(2014, 7, 18))
93 field = fields.Date(missing=dt.date(2014, 7, 18))
10194 res = spec_fixture.openapi.field2property(field)
10295 assert res["default"] == dt.date(2014, 7, 18).isoformat()
10396
292285 assert properties["x-customString"] == (
293286 spec_fixture.openapi.openapi_version == "2.0"
294287 )
288
289
290 def test_field2property_with_non_string_metadata_keys(spec_fixture):
291 class _DesertSentinel:
292 pass
293
294 field = fields.Boolean(description="A description")
295 field.metadata[_DesertSentinel()] = "to be ignored"
296 result = spec_fixture.openapi.field2property(field)
297 assert result == {"description": "A description", "type": "boolean"}
00 import pytest
1
2 from marshmallow import fields, Schema, validate
1 from datetime import datetime
2
3 from marshmallow import EXCLUDE, fields, INCLUDE, RAISE, Schema, validate
34
45 from apispec.ext.marshmallow import MarshmallowPlugin
5 from apispec.ext.marshmallow.openapi import MARSHMALLOW_VERSION_INFO
66 from apispec import exceptions, utils, APISpec
77
88 from .schemas import CustomList, CustomStringField
1111
1212 class TestMarshmallowFieldToOpenAPI:
1313 def test_fields_with_missing_load(self, openapi):
14 field_dict = {"field": fields.Str(default="foo", missing="bar")}
15 res = openapi.fields2parameters(field_dict, default_in="query")
14 class MySchema(Schema):
15 field = fields.Str(default="foo", missing="bar")
16
17 res = openapi.schema2parameters(MySchema, location="query")
1618 if openapi.openapi_version.major < 3:
1719 assert res[0]["default"] == "bar"
1820 else:
1921 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"
6322
6423 # json/body is invalid for OpenAPI 3
6524 @pytest.mark.parametrize("openapi", ("2.0",), indirect=True)
6827 id = fields.Int()
6928
7029 schema = ExampleSchema(many=True)
71 res = openapi.schema2parameters(schema=schema, default_in="json")
30 res = openapi.schema2parameters(schema=schema, location="json")
7231 assert res[0]["in"] == "body"
7332
7433 def test_fields_with_dump_only(self, openapi):
7534 class UserSchema(Schema):
7635 name = fields.Str(dump_only=True)
7736
78 res = openapi.fields2parameters(UserSchema._declared_fields, default_in="query")
37 res = openapi.schema2parameters(schema=UserSchema(), location="query")
7938 assert len(res) == 0
80 res = openapi.fields2parameters(UserSchema().fields, default_in="query")
39
40 class UserSchema(Schema):
41 name = fields.Str()
42
43 class Meta:
44 dump_only = ("name",)
45
46 res = openapi.schema2parameters(schema=UserSchema(), location="query")
8147 assert len(res) == 0
8248
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
9249
9350 class TestMarshmallowSchemaToModelDefinition:
94 def test_invalid_schema(self, openapi):
95 with pytest.raises(ValueError):
96 openapi.schema2jsonschema(None)
97
9851 def test_schema2jsonschema_with_explicit_fields(self, openapi):
9952 class UserSchema(Schema):
10053 _id = fields.Int()
11366 assert props["email"]["format"] == "email"
11467 assert props["email"]["description"] == "email address of the user"
11568
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):
69 def test_schema2jsonschema_override_name(self, openapi):
14670 class ExampleSchema(Schema):
14771 _id = fields.Int(data_key="id")
14872 _global = fields.Int(data_key="global")
206130
207131 res = openapi.schema2jsonschema(WhiteStripesSchema)
208132 assert set(res["properties"].keys()) == {"guitarist", "drummer"}
133
134 def test_unknown_values_disallow(self, openapi):
135 class UnknownRaiseSchema(Schema):
136 class Meta:
137 unknown = RAISE
138
139 first = fields.Str()
140
141 res = openapi.schema2jsonschema(UnknownRaiseSchema)
142 assert res["additionalProperties"] is False
143
144 def test_unknown_values_allow(self, openapi):
145 class UnknownIncludeSchema(Schema):
146 class Meta:
147 unknown = INCLUDE
148
149 first = fields.Str()
150
151 res = openapi.schema2jsonschema(UnknownIncludeSchema)
152 assert res["additionalProperties"] is True
153
154 def test_unknown_values_ignore(self, openapi):
155 class UnknownExcludeSchema(Schema):
156 class Meta:
157 unknown = EXCLUDE
158
159 first = fields.Str()
160
161 res = openapi.schema2jsonschema(UnknownExcludeSchema)
162 assert "additionalProperties" not in res
209163
210164 def test_only_explicitly_declared_fields_are_translated(self, openapi):
211165 class UserSchema(Schema):
226180 assert "email" not in props
227181
228182 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
183 fields_dict = {"user_id": fields.Int(data_key="id", required=True)}
236184 res = openapi.fields2jsonschema(fields_dict)
237185 assert res["required"] == ["id"]
238186
250198 class NotASchema:
251199 pass
252200
253 expected_error = "{!r} doesn't have either `fields` or `_declared_fields`.".format(
254 NotASchema
201 expected_error = (
202 "{!r} doesn't have either `fields` or `_declared_fields`.".format(
203 NotASchema
204 )
255205 )
256206 with pytest.raises(ValueError, match=expected_error):
257207 openapi.schema2jsonschema(NotASchema)
260210 class TestMarshmallowSchemaToParameters:
261211 @pytest.mark.parametrize("ListClass", [fields.List, CustomList])
262212 def test_field_multiple(self, ListClass, openapi):
263 field = ListClass(fields.Str, location="querystring")
264 res = openapi.field2parameter(field, name="field", default_in=None)
213 field = ListClass(fields.Str)
214 res = openapi._field2parameter(field, name="field", location="query")
265215 assert res["in"] == "query"
266216 if openapi.openapi_version.major < 3:
267217 assert res["type"] == "array"
274224 assert res["explode"] is True
275225
276226 def test_field_required(self, openapi):
277 field = fields.Str(required=True, location="query")
278 res = openapi.field2parameter(field, name="field", default_in=None)
227 field = fields.Str(required=True)
228 res = openapi._field2parameter(field, name="field", location="query")
279229 assert res["required"] is True
280230
281 def test_invalid_schema(self, openapi):
282 with pytest.raises(ValueError):
283 openapi.schema2parameters(None)
231 def test_schema_partial(self, openapi):
232 class UserSchema(Schema):
233 field = fields.Str(required=True)
234
235 res_nodump = openapi.schema2parameters(
236 UserSchema(partial=True), location="query"
237 )
238
239 param = res_nodump[0]
240 assert param["required"] is False
241
242 def test_schema_partial_list(self, openapi):
243 class UserSchema(Schema):
244 field = fields.Str(required=True)
245 partial_field = fields.Str(required=True)
246
247 res_nodump = openapi.schema2parameters(
248 UserSchema(partial=("partial_field",)), location="query"
249 )
250
251 param = next(p for p in res_nodump if p["name"] == "field")
252 assert param["required"] is True
253 param = next(p for p in res_nodump if p["name"] == "partial_field")
254 assert param["required"] is False
284255
285256 # json/body is invalid for OpenAPI 3
286257 @pytest.mark.parametrize("openapi", ("2.0",), indirect=True)
289260 name = fields.Str()
290261 email = fields.Email()
291262
292 res = openapi.schema2parameters(UserSchema, default_in="body")
263 res = openapi.schema2parameters(UserSchema, location="body")
293264 assert len(res) == 1
294265 param = res[0]
295266 assert param["in"] == "body"
302273 name = fields.Str()
303274 email = fields.Email(dump_only=True)
304275
305 res_nodump = openapi.schema2parameters(UserSchema, default_in="body")
276 res_nodump = openapi.schema2parameters(UserSchema, location="body")
306277 assert len(res_nodump) == 1
307278 param = res_nodump[0]
308279 assert param["in"] == "body"
315286 name = fields.Str()
316287 email = fields.Email()
317288
318 res = openapi.schema2parameters(UserSchema(many=True), default_in="body")
289 res = openapi.schema2parameters(UserSchema(many=True), location="body")
319290 assert len(res) == 1
320291 param = res[0]
321292 assert param["in"] == "body"
327298 name = fields.Str()
328299 email = fields.Email()
329300
330 res = openapi.schema2parameters(UserSchema, default_in="query")
301 res = openapi.schema2parameters(UserSchema, location="query")
331302 assert len(res) == 2
332303 res.sort(key=lambda param: param["name"])
333304 assert res[0]["name"] == "email"
340311 name = fields.Str()
341312 email = fields.Email()
342313
343 res = openapi.schema2parameters(UserSchema(), default_in="query")
314 res = openapi.schema2parameters(UserSchema(), location="query")
344315 assert len(res) == 2
345316 res.sort(key=lambda param: param["name"])
346317 assert res[0]["name"] == "email"
354325 email = fields.Email()
355326
356327 with pytest.raises(AssertionError):
357 openapi.schema2parameters(UserSchema(many=True), default_in="query")
328 openapi.schema2parameters(UserSchema(many=True), location="query")
358329
359330 def test_fields_query(self, openapi):
360 field_dict = {"name": fields.Str(), "email": fields.Email()}
361 res = openapi.fields2parameters(field_dict, default_in="query")
331 class MySchema(Schema):
332 name = fields.Str()
333 email = fields.Email()
334
335 res = openapi.schema2parameters(MySchema, location="query")
362336 assert len(res) == 2
363337 res.sort(key=lambda param: param["name"])
364338 assert res[0]["name"] == "email"
370344 class NotASchema:
371345 pass
372346
373 expected_error = "{!r} doesn't have either `fields` or `_declared_fields`".format(
374 NotASchema
347 expected_error = (
348 f"{NotASchema!r} doesn't have either `fields` or `_declared_fields`"
375349 )
376350 with pytest.raises(ValueError, match=expected_error):
377351 openapi.schema2jsonschema(NotASchema)
476450 "required": True,
477451 "type": "string",
478452 },
479 openapi.field2parameter(
453 openapi._field2parameter(
480454 field=fields.List(
481455 fields.Str(),
482456 validate=validate.OneOf(["freddie", "roger"]),
483 location="querystring",
484457 ),
485 default_in=None,
458 location="query",
486459 name="body",
487460 ),
488461 ]
489 + openapi.schema2parameters(PageSchema, default_in="query"),
462 + openapi.schema2parameters(PageSchema, location="query"),
490463 "responses": {200: {"schema": PetSchema, "description": "A pet"}},
491464 },
492465 "post": {
499472 "type": "string",
500473 }
501474 ]
502 + openapi.schema2parameters(CategorySchema, default_in="body")
475 + openapi.schema2parameters(CategorySchema, location="body")
503476 ),
504477 "responses": {201: {"schema": PetSchema, "description": "A pet"}},
505478 },
534507 "required": True,
535508 "schema": {"type": "string"},
536509 },
537 openapi.field2parameter(
510 openapi._field2parameter(
538511 field=fields.List(
539512 fields.Str(),
540513 validate=validate.OneOf(["freddie", "roger"]),
541 location="querystring",
542514 ),
543 default_in=None,
515 location="query",
544516 name="body",
545517 ),
546518 ]
547 + openapi.schema2parameters(PageSchema, default_in="query"),
519 + openapi.schema2parameters(PageSchema, location="query"),
548520 "responses": {
549521 200: {
550522 "description": "success",
585557 class ValidationSchema(Schema):
586558 id = fields.Int(dump_only=True)
587559 range = fields.Int(validate=validate.Range(min=1, max=10))
560 range_no_upper = fields.Float(validate=validate.Range(min=1))
588561 multiple_ranges = fields.Int(
589562 validate=[
590563 validate.Range(min=1),
610583 equal_length = fields.Str(
611584 validate=[validate.Length(equal=5), validate.Length(min=1, max=10)]
612585 )
586 date_range = fields.DateTime(
587 validate=validate.Range(
588 min=datetime(1900, 1, 1),
589 )
590 )
613591
614592 @pytest.mark.parametrize(
615593 ("field", "properties"),
616594 [
617595 ("range", {"minimum": 1, "maximum": 10}),
596 ("range_no_upper", {"minimum": 1}),
618597 ("multiple_ranges", {"minimum": 3, "maximum": 7}),
619598 ("list_length", {"minItems": 1, "maxItems": 10}),
620599 ("custom_list_length", {"minItems": 1, "maxItems": 10}),
622601 ("custom_field_length", {"minLength": 1, "maxLength": 10}),
623602 ("multiple_lengths", {"minLength": 3, "maxLength": 7}),
624603 ("equal_length", {"minLength": 5, "maxLength": 5}),
604 ("date_range", {"x-minimum": datetime(1900, 1, 1)}),
625605 ],
626606 )
627607 def test_properties(self, field, properties, spec):
2525 @pytest.mark.parametrize("docstring", (None, "", "---"))
2626 def test_load_operations_from_docstring_empty_docstring(docstring):
2727 assert yaml_utils.load_operations_from_docstring(docstring) == {}
28
29
30 def test_dict_to_yaml_unicode():
31 assert yaml_utils.dict_to_yaml({"가": "나"}) == '"\\uAC00": "\\uB098"\n'
32 assert yaml_utils.dict_to_yaml({"가": "나"}, {"allow_unicode": True}) == "가: 나\n"
2020 return spec.to_dict()["components"]["parameters"]
2121
2222
23 def get_headers(spec):
24 if spec.openapi_version.major < 3:
25 return spec.to_dict()["headers"]
26 return spec.to_dict()["components"]["headers"]
27
28
2329 def get_examples(spec):
2430 return spec.to_dict()["components"]["examples"]
2531
00 [tox]
11 envlist=
22 lint
3 py{35,36,37,38}-marshmallow2
4 py{35,36,37,38}-marshmallow3
3 py{36,37,38,39}-marshmallow3
54 py38-marshmallowdev
65 docs
76
87 [testenv]
98 extras = tests
109 deps =
11 marshmallow2: marshmallow>=2.0.0,<3.0.0
1210 marshmallow3: marshmallow>=3.0.0,<4.0.0
1311 marshmallowdev: https://github.com/marshmallow-code/marshmallow/archive/dev.tar.gz
1412 commands = pytest {posargs}
2725 [testenv:watch-docs]
2826 deps = sphinx-autobuild
2927 extras = docs
30 commands = sphinx-autobuild --open-browser docs/ docs/_build {posargs} -z src/apispec -s 2
28 commands = sphinx-autobuild --open-browser docs/ docs/_build {posargs} --watch src/apispec --delay 2
3129
3230 [testenv:watch-readme]
3331 deps = restview