Codebase list apispec / 15094bc3-43fa-437e-a299-1d90a33102b6/main
New upstream release. Kali Janitor 2 years ago
32 changed file(s) with 1335 addition(s) and 524 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>`_
73 - François Magimel `<https://github.com/Linkid>`_
00 Changelog
11 ---------
2
3 4.7.1 (2021-07-06)
4 ******************
5
6 Bug fixes:
7
8 - Correct spelling of 'null': remove extra quotes (:issue:`689`).
9 Thanks :user:`mjpieters` for the PR.
10
11 4.7.0 (2021-06-28)
12 ******************
13
14 Features:
15
16 - Document `deprecated` property from field metadata (:pr:`686`).
17 Thanks :user:`greyli` for the PR.
18 - Document `writeOnly` and `nullable` properties from field metadata
19 (:pr:`684`). Thanks :user:`greyli` for the PR.
20
21 4.6.0 (2021-06-14)
22 ******************
23
24 Features:
25
26 - Support `Pluck` field (:pr:`677`). Thanks :user:`mjpieters` for the PR.
27 - Support `TimeDelta` field (:pr:`678`).
28
29 4.5.0 (2021-06-04)
30 ******************
31
32 Features:
33
34 - Support OpenAPI 3.1.0 (:issue:`579`).
35
36 Bug fixes:
37
38 - Fix `get_fields` to avoid crashing when a field is named `fields`
39 (:issue:`673`). Thanks :user:`Reskov` for reporting.
40
41 Other changes:
42
43 - Don't pass field metadata as keyword arguments in the tests. This is
44 deprecated since marshmallow 3.10. apispec is still compatible with
45 marshmallow >=3,<3.10 but tests now require marshmallow >=3.10. (:pr:`675`)
46
47 4.4.2 (2021-05-24)
48 ******************
49
50 Bug fixes:
51
52 - Respect ``partial`` marshmallow schema parameter: don't document the field as
53 required. (:issue:`627`). Thanks :user:`Anti-Distinctlyminty` for the PR.
54
55 4.4.1 (2021-05-07)
56 ******************
57
58 Bug fixes:
59
60 - Don't set ``additionalProperties`` if ``Meta.unknown`` is ``EXCLUDE``
61 (:issue:`659`). Thanks :user:`kupuguy` for the PR.
62
63 4.4.0 (2021-03-31)
64 ******************
65
66 Features:
67
68 - Populate ``additionalProperties`` from ``Meta.unknown`` (:pr:`635`).
69 Thanks :user:`timsilvers` for the PR.
70 - Allow ``to_yaml`` to pass kwargs to ``yaml.dump`` (:pr:`648`).
71 - Resolve header references in responses (:pr:`650`).
72 - Resolve example references in parameters, request bodies and responses
73 (:pr:`651`).
74
75 4.3.0 (2021-02-10)
76 ******************
77
78 Features:
79
80 - Add `apispec.core.Components.header` to register header components
81 (:pr:`637`).
82
83 4.2.0 (2021-02-06)
84 ******************
85
86 Features:
87
88 - Make components public attributes of ``Components`` class (:pr:`634`).
89
90 4.1.0 (2021-01-26)
91 ******************
92
93 Features:
94
95 - Resolve schemas in callbacks (:pr:`544`). Thanks :user:`kortsi` for the PR.
96
97 Bug fixes:
98
99 - Fix docstrings documenting kwargs type as dict (:issue:`534`).
100 - Use ``x-minimum`` and ``x-maximum`` extensions to document ranges that are
101 not of number type (e.g. datetime) (:issue:`614`).
102
103 Other changes:
104
105 - Test against Python 3.9.
106
107 4.0.0 (2020-09-30)
108 ******************
109
110 Features:
111
112 - *Backwards-incompatible*: Automatically generate references for schemas
113 passed as strings in responses and request bodies. When using
114 ``MarshmallowPlugin``, if a schema is passed as string, the marshmallow
115 registry is looked up for this schema name and if none is found, the name is
116 assumed to be a reference to a manually created schema and a reference is
117 generated. No exception is raised anymore if the schema name can't be found
118 in the registry. (:pr:`554`)
119
120 4.0.0b1 (2020-09-06)
121 ********************
122
123 Features:
124
125 - *Backwards-incompatible*: Ignore ``location`` field metadata. This attribute
126 was used in webargs but it has now been dropped. A ``Schema`` can now only
127 have a single location. This simplifies the logic in ``OpenAPIConverter``
128 methods, where ``default_in`` argument now becomes ``location``. (:pr:`526`)
129 - *Backwards-incompatible*: Don't document ``int`` format as ``"int32"`` and
130 ``float`` format as ``"float"``, as those are platform-dependent (:pr:`595`).
131
132 Refactoring:
133
134 - ``OpenAPIConverter.field2parameters`` and
135 ``OpenAPIConverter.property2parameter`` are removed.
136 ``OpenAPIConverter.field2parameter`` becomes private. (:pr:`581`)
137
138 Other changes:
139
140 - Drop support for marshmallow 2. Marshmallow 3.x is required. (:pr:`583`)
141 - Drop support for Python 3.5. Python 3.6+ is required. (:pr:`582`)
142
143
144 3.3.2 (2020-08-29)
145 ******************
146
147 Bug fixes:
148
149 - Fix crash when field metadata contains non-string keys (:pr:`596`).
150 Thanks :user:`sanzoghenzo` for the fix.
2151
3152 3.3.1 (2020-06-06)
4153 ******************
8157 - Fix ``MarshmallowPlugin`` crash when ``resolve_schema_dict`` is passed a
9158 schema as string and ``schema_name_resolver`` returns ``None``
10159 (:issue:`566`). Thanks :user:`black3r` for reporting and thanks
11 :user:`Bangterm` for the PR.
160 :user:`Bangertm` for the PR.
12161
13162 3.3.0 (2020-02-14)
14163 ******************
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
0 apispec (4.7.1-0kali1) UNRELEASED; urgency=low
1
2 * New upstream release.
3
4 -- Kali Janitor <[email protected]> Tue, 27 Jul 2021 10:36:33 -0000
5
06 apispec (3.3.1-0kali2) kali-dev; urgency=medium
17
28 [ Sophie Brun ]
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.3",
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.10.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.7.1"
66 __all__ = ["APISpec", "BasePlugin"]
2828 def __init__(self, plugins, openapi_version):
2929 self._plugins = plugins
3030 self.openapi_version = openapi_version
31 self._schemas = {}
32 self._responses = {}
33 self._parameters = {}
34 self._examples = {}
35 self._security_schemes = {}
31 self.schemas = {}
32 self.responses = {}
33 self.parameters = {}
34 self.headers = {}
35 self.examples = {}
36 self.security_schemes = {}
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
6365
6466 status = fields.String(
6567 required=True,
66 enum=['open', 'closed'],
67 description='Status (open or closed)',
68 metadata={
69 "description": "Status (open or closed)",
70 "enum": ["open", "closed"],
71 },
6872 )
6973
7074 https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#schemaObject
7175 """
72 if name in self._schemas:
73 raise DuplicateComponentNameError(
74 'Another schema with name "{}" is already registered.'.format(name)
76 if name in self.schemas:
77 raise DuplicateComponentNameError(
78 f'Another schema with name "{name}" is already registered.'
7579 )
7680 component = component or {}
7781 ret = component.copy()
8185 ret.update(plugin.schema_helper(name, component, **kwargs) or {})
8286 except PluginMethodNotImplementedError:
8387 continue
84 self._schemas[name] = ret
88 self.schemas[name] = ret
8589 return self
8690
8791 def response(self, component_id, component=None, **kwargs):
8993
9094 :param str component_id: ref_id to use as reference
9195 :param dict component: response fields
92 :param dict kwargs: plugin-specific arguments
93 """
94 if component_id in self._responses:
96 :param kwargs: plugin-specific arguments
97 """
98 if component_id in self.responses:
9599 raise DuplicateComponentNameError(
96100 'Another response with name "{}" is already registered.'.format(
97101 component_id
105109 ret.update(plugin.response_helper(component, **kwargs) or {})
106110 except PluginMethodNotImplementedError:
107111 continue
108 self._responses[component_id] = ret
112 self.responses[component_id] = ret
109113 return self
110114
111115 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.
116 """Add a parameter which can be referenced.
117
118 :param str component_id: identifier by which parameter may be referenced.
115119 :param str location: location of the parameter.
116120 :param dict component: parameter fields.
117 :param dict kwargs: plugin-specific arguments
118 """
119 if component_id in self._parameters:
121 :param kwargs: plugin-specific arguments
122 """
123 if component_id in self.parameters:
120124 raise DuplicateComponentNameError(
121125 'Another parameter with name "{}" is already registered.'.format(
122126 component_id
137141 ret.update(plugin.parameter_helper(component, **kwargs) or {})
138142 except PluginMethodNotImplementedError:
139143 continue
140 self._parameters[component_id] = ret
144 self.parameters[component_id] = ret
145 return self
146
147 def header(self, component_id, component):
148 """Add a header which can be referenced.
149
150 :param str component_id: identifier by which header may be referenced.
151 :param dict component: header fields.
152
153 https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.1.md#headerObject
154 """
155 if component_id in self.headers:
156 raise DuplicateComponentNameError(
157 f'Another header with name "{component_id}" is already registered.'
158 )
159 self.headers[component_id] = component
141160 return self
142161
143162 def example(self, name, component, **kwargs):
148167
149168 https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.1.md#exampleObject
150169 """
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
170 if name in self.examples:
171 raise DuplicateComponentNameError(
172 f'Another example with name "{name}" is already registered.'
173 )
174 self.examples[name] = component
156175 return self
157176
158177 def security_scheme(self, component_id, component):
159178 """Add a security scheme which can be referenced.
160179
161180 :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:
181 :param dict component: security scheme fields
182 """
183 if component_id in self.security_schemes:
165184 raise DuplicateComponentNameError(
166185 'Another security scheme with name "{}" is already registered.'.format(
167186 component_id
168187 )
169188 )
170 self._security_schemes[component_id] = component
189 self.security_schemes[component_id] = component
171190 return self
172191
173192
180199 See https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#infoObject
181200 :param str|OpenAPIVersion openapi_version: OpenAPI Specification version.
182201 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
202 :param options: Optional top-level keys
184203 See https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#openapi-object
185204 """
186205
220239 ret = deepupdate(ret, self.options)
221240 return ret
222241
223 def to_yaml(self):
224 """Render the spec to YAML. Requires PyYAML to be installed."""
242 def to_yaml(self, yaml_dump_kwargs=None):
243 """Render the spec to YAML. Requires PyYAML to be installed.
244
245 :param dict yaml_dump_kwargs: Additional keyword arguments to pass to `yaml.dump`
246 """
225247 from .yaml_utils import dict_to_yaml
226248
227 return dict_to_yaml(self.to_dict())
249 return dict_to_yaml(self.to_dict(), yaml_dump_kwargs)
228250
229251 def tag(self, tag):
230 """ Store information about a tag.
252 """Store information about a tag.
231253
232254 :param dict tag: the dictionary storing information about the tag.
233255 """
242264 summary=None,
243265 description=None,
244266 parameters=None,
245 **kwargs
267 **kwargs,
246268 ):
247269 """Add a new path object to the spec.
248270
253275 :param str summary: short summary relevant to all operations in this path
254276 :param str description: long description relevant to all operations in this path
255277 :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`
278 :param kwargs: parameters used by any path helpers see :meth:`register_path_helper`
257279 """
258280 # operations and parameters must be deepcopied because they are mutated
259281 # in clean_operations and operation helpers and path may be called twice
299321 Otherwise, it is assumed to be a reference name as string and the corresponding $ref
300322 string is returned.
301323
302 :param str obj_type: "parameter" or "response"
303 :param dict|str obj: parameter or response in dict form or as ref_id string
324 :param str obj_type: "schema", "parameter", "response" or "security_scheme"
325 :param dict|str obj: object in dict form or as ref_id string
304326 """
305327 if isinstance(obj, dict):
306328 return obj
307329 return build_reference(obj_type, self.openapi_version.major, obj)
330
331 def _resolve_schema(self, obj):
332 """Replace schema reference as string with a $ref if needed."""
333 if not isinstance(obj, dict):
334 return
335 if self.openapi_version.major < 3:
336 if "schema" in obj:
337 obj["schema"] = self.get_ref("schema", obj["schema"])
338 else:
339 if "content" in obj:
340 for content in obj["content"].values():
341 if "schema" in content:
342 content["schema"] = self.get_ref("schema", content["schema"])
343
344 def _resolve_examples(self, obj):
345 """Replace example reference as string with a $ref"""
346 for name, example in obj.get("examples", {}).items():
347 obj["examples"][name] = self.get_ref("example", example)
308348
309349 def clean_parameters(self, parameters):
310350 """Ensure that all parameters with "in" equal to "path" are also required
322362 missing_attrs = [attr for attr in ("name", "in") if attr not in parameter]
323363 if missing_attrs:
324364 raise InvalidParameterError(
325 "Missing keys {} for parameter".format(missing_attrs)
365 f"Missing keys {missing_attrs} for parameter"
326366 )
327367
328368 # OpenAPI Spec 3 and 2 don't allow for duplicated parameters
340380 if parameter["in"] == "path":
341381 parameter["required"] = True
342382
383 self._resolve_examples(parameter)
384
343385 return [self.get_ref("parameter", p) for p in parameters]
344386
345387 def clean_operations(self, operations):
364406 for operation in (operations or {}).values():
365407 if "parameters" in operation:
366408 operation["parameters"] = self.clean_parameters(operation["parameters"])
409 # OAS 3
410 if "requestBody" in operation:
411 self._resolve_schema(operation["requestBody"])
412 for media_type in operation["requestBody"]["content"].values():
413 self._resolve_examples(media_type)
414
367415 if "responses" in operation:
368416 responses = OrderedDict()
369417 for code, response in operation["responses"].items():
372420 except (TypeError, ValueError):
373421 if self.openapi_version.major < 3 and code != "default":
374422 warnings.warn("Non-integer code not allowed in OpenAPI < 3")
375
376 responses[str(code)] = self.get_ref("response", response)
423 self._resolve_schema(response)
424 response = self.get_ref("response", response)
425 for name, header in response.get("headers", {}).items():
426 response["headers"][name] = self.get_ref("header", header)
427 for media_type in response.get("content", {}).values():
428 self._resolve_examples(media_type)
429 responses[str(code)] = response
377430 operation["responses"] = responses
4747
4848 class UserSchema(Schema):
4949 id = fields.Int(dump_only=True)
50 name = fields.Str(description="The user's name")
50 name = fields.Str(metadata={"description": "The user's name"})
5151 created = fields.DateTime(
52 dump_only=True, default=dt.datetime.utcnow, doc_default="The current datetime"
52 dump_only=True,
53 default=dt.datetime.utcnow,
54 metadata={"doc_default": "The current datetime"}
5355 )
5456
5557
5961 # 'format': 'date-time',
6062 # 'readOnly': True,
6163 # 'type': 'string'},
62 # 'id': {'format': 'int32',
63 # 'readOnly': True,
64 # 'id': {'readOnly': True,
6465 # 'type': 'integer'},
6566 # 'name': {'description': "The user's name",
6667 # 'type': 'string'}},
139140 class MyCustomField(Integer):
140141 # ...
141142
142 @ma_plugin.map_to_openapi_type(Integer) # will map to ('integer', 'int32')
143 @ma_plugin.map_to_openapi_type(Integer) # will map to ('integer', None)
143144 class MyCustomFieldThatsKindaLikeAnInteger(Integer):
144145 # ...
145146 """
187188 return response
188189
189190 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)
191 self.resolver.resolve_operations(operations)
202192
203193 def warn_if_schema_already_in_spec(self, schema_key):
204194 """Method to warn the user if the schema has already been added to the
1919 return schema()
2020 if isinstance(schema, marshmallow.Schema):
2121 return schema
22 try:
23 return marshmallow.class_registry.get_class(schema)()
24 except marshmallow.exceptions.RegistryError:
25 raise ValueError(
26 "{!r} is not a marshmallow.Schema subclass or instance and has not"
27 " been registered in the marshmallow class registry.".format(schema)
28 )
22 return marshmallow.class_registry.get_class(schema)()
2923
3024
3125 def resolve_schema_cls(schema):
3832 return schema
3933 if isinstance(schema, marshmallow.Schema):
4034 return type(schema)
41 try:
42 return marshmallow.class_registry.get_class(schema)
43 except marshmallow.exceptions.RegistryError:
44 raise ValueError(
45 "{!r} is not a marshmallow.Schema subclass or instance and has not"
46 " been registered in the marshmallow class registry.".format(schema)
47 )
35 return marshmallow.class_registry.get_class(schema)
4836
4937
5038 def get_fields(schema, *, exclude_dump_only=False):
5442 :param bool exclude_dump_only: whether to filter fields in Meta.dump_only
5543 :rtype: dict, of field name field object pairs
5644 """
57 if hasattr(schema, "fields"):
45 if isinstance(schema, marshmallow.Schema):
5846 fields = schema.fields
59 elif hasattr(schema, "_declared_fields"):
47 elif isinstance(schema, type) and issubclass(schema, marshmallow.Schema):
6048 fields = copy.deepcopy(schema._declared_fields)
6149 else:
62 raise ValueError(
63 "{!r} doesn't have either `fields` or `_declared_fields`.".format(schema)
64 )
50 raise ValueError(f"{schema!r} is neither a Schema class nor a Schema instance.")
6551 Meta = getattr(schema, "Meta", None)
6652 warn_if_fields_defined_in_meta(fields, Meta)
6753 return filter_excluded_fields(fields, Meta, exclude_dump_only=exclude_dump_only)
9076
9177 :param dict fields: A dictionary of fields name field object pairs
9278 :param Meta: the schema's Meta class
93 :param bool exclude_dump_only: whether to filter fields in Meta.dump_only
79 :param bool exclude_dump_only: whether to filter dump_only fields
9480 """
9581 exclude = list(getattr(Meta, "exclude", []))
9682 if exclude_dump_only:
9783 exclude.extend(getattr(Meta, "dump_only", []))
9884
9985 filtered_fields = OrderedDict(
100 (key, value) for key, value in fields.items() if key not in exclude
86 (key, value)
87 for key, value in fields.items()
88 if key not in exclude and not (exclude_dump_only and value.dump_only)
10189 )
10290
10391 return filtered_fields
132120 :param int counter: the counter of the number of recursions
133121 :return: the unique name
134122 """
135 if name not in components._schemas:
123 if name not in components.schemas:
136124 return name
137125 if not counter: # first time through recursion
138126 warnings.warn(
1616
1717 RegexType = type(re.compile(""))
1818
19 MARSHMALLOW_VERSION_INFO = tuple(
20 [int(part) for part in marshmallow.__version__.split(".") if part.isdigit()]
21 )
22
23
2419 # marshmallow field => (JSON Schema type, format)
2520 DEFAULT_FIELD_MAPPING = {
26 marshmallow.fields.Integer: ("integer", "int32"),
21 marshmallow.fields.Integer: ("integer", None),
2722 marshmallow.fields.Number: ("number", None),
28 marshmallow.fields.Float: ("number", "float"),
23 marshmallow.fields.Float: ("number", None),
2924 marshmallow.fields.Decimal: ("number", None),
3025 marshmallow.fields.String: ("string", None),
3126 marshmallow.fields.Boolean: ("boolean", None),
3328 marshmallow.fields.DateTime: ("string", "date-time"),
3429 marshmallow.fields.Date: ("string", "date"),
3530 marshmallow.fields.Time: ("string", None),
31 marshmallow.fields.TimeDelta: ("integer", None),
3632 marshmallow.fields.Email: ("string", "email"),
3733 marshmallow.fields.URL: ("string", "url"),
3834 marshmallow.fields.Dict: ("object", None),
7167 "properties",
7268 "additionalProperties",
7369 "readOnly",
70 "writeOnly",
7471 "xml",
7572 "externalDocs",
7673 "example",
74 "nullable",
75 "deprecated",
7776 }
7877
7978
8786
8887 def init_attribute_functions(self):
8988 self.attribute_functions = [
89 # self.field2type_and_format should run first
90 # as other functions may rely on its output
9091 self.field2type_and_format,
9192 self.field2default,
9293 self.field2choices,
9899 self.field2pattern,
99100 self.metadata2properties,
100101 self.nested2properties,
102 self.pluck2properties,
101103 self.list2properties,
102104 self.dict2properties,
105 self.timedelta2properties,
103106 ]
104107
105108 def map_to_openapi_type(self, *args):
211214 else:
212215 default = field.missing
213216 if default is not marshmallow.missing and not callable(default):
214 if MARSHMALLOW_VERSION_INFO[0] >= 3:
215 default = field._serialize(default, None, None)
217 default = field._serialize(default, None, None)
216218 ret["default"] = default
217219 return ret
218220
264266 attributes["writeOnly"] = True
265267 return attributes
266268
267 def field2nullable(self, field, **kwargs):
269 def field2nullable(self, field, ret):
268270 """Return the dictionary of OpenAPI field attributes for a nullable field.
269271
270272 :param Field field: A marshmallow field.
272274 """
273275 attributes = {}
274276 if field.allow_none:
275 attributes[
276 "x-nullable" if self.openapi_version.major < 3 else "nullable"
277 ] = True
277 if self.openapi_version.major < 3:
278 attributes["x-nullable"] = True
279 elif self.openapi_version.minor < 1:
280 attributes["nullable"] = True
281 else:
282 attributes["type"] = [*make_type_list(ret.get("type")), "null"]
278283 return attributes
279284
280 def field2range(self, field, **kwargs):
285 def field2range(self, field, ret):
281286 """Return the dictionary of OpenAPI field attributes for a set of
282287 :class:`Range <marshmallow.validators.Range>` validators.
283288
294299 )
295300 ]
296301
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
302 min_attr, max_attr = (
303 ("minimum", "maximum")
304 if set(make_type_list(ret.get("type"))) & {"number", "integer"}
305 else ("x-minimum", "x-maximum")
306 )
307 return make_min_max_attributes(validators, min_attr, max_attr)
310308
311309 def field2length(self, field, **kwargs):
312310 """Return the dictionary of OpenAPI field attributes for a set of
315313 :param Field field: A marshmallow field.
316314 :rtype: dict
317315 """
318 attributes = {}
319
320316 validators = [
321317 validator
322318 for validator in field.validators
333329 min_attr = "minItems" if is_array else "minLength"
334330 max_attr = "maxItems" if is_array else "maxLength"
335331
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
332 equal_list = [
333 validator.equal for validator in validators if validator.equal is not None
334 ]
335 if equal_list:
336 return {min_attr: equal_list[0], max_attr: equal_list[0]}
337
338 return make_min_max_attributes(validators, min_attr, max_attr)
353339
354340 def field2pattern(self, field, **kwargs):
355 """Return the dictionary of OpenAPI field attributes for a set of
356 :class:`Range <marshmallow.validators.Regexp>` validators.
341 """Return the dictionary of OpenAPI field attributes for a
342 :class:`Regexp <marshmallow.validators.Regexp>` validator.
343
344 If there is more than one such validator, only the first
345 is used in the output spec.
357346
358347 :param Field field: A marshmallow field.
359348 :rtype: dict
396385 metadata = {
397386 key.replace("_", "-") if key.startswith("x_") else key: value
398387 for key, value in field.metadata.items()
388 if isinstance(key, str)
399389 }
400390
401391 # Avoid validation error with "Additional properties not allowed"
417407 :param Field field: A marshmallow field.
418408 :rtype: dict
419409 """
420 if isinstance(field, marshmallow.fields.Nested):
410 # Pluck is a subclass of Nested but is in essence a single field; it
411 # is treated separately by pluck2properties.
412 if isinstance(field, marshmallow.fields.Nested) and not isinstance(
413 field, marshmallow.fields.Pluck
414 ):
421415 schema_dict = self.resolve_nested_schema(field.schema)
422416 if ret and "$ref" in schema_dict:
423417 ret.update({"allOf": [schema_dict]})
425419 ret.update(schema_dict)
426420 return ret
427421
422 def pluck2properties(self, field, **kwargs):
423 """Return a dictionary of properties from :class:`Pluck <marshmallow.fields.Pluck` fields.
424
425 Pluck effectively trans-includes a field from another schema into this,
426 possibly wrapped in an array (`many=True`).
427
428 :param Field field: A marshmallow field.
429 :rtype: dict
430 """
431 if isinstance(field, marshmallow.fields.Pluck):
432 plucked_field = field.schema.fields[field.field_name]
433 ret = self.field2property(plucked_field)
434 return {"type": "array", "items": ret} if field.many else ret
435 return {}
436
428437 def list2properties(self, field, **kwargs):
429438 """Return a dictionary of properties from :class:`List <marshmallow.fields.List>` fields.
430439
435444 """
436445 ret = {}
437446 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)
447 ret["items"] = self.field2property(field.inner)
442448 return ret
443449
444450 def dict2properties(self, field, **kwargs):
452458 """
453459 ret = {}
454460 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
461 value_field = field.value_field
462 if value_field:
463 ret["additionalProperties"] = self.field2property(value_field)
464 return ret
465
466 def timedelta2properties(self, field, **kwargs):
467 """Return a dictionary of properties from :class:`TimeDelta <marshmallow.fields.TimeDelta>` fields.
468
469 Adds a `x-unit` vendor property based on the field's `precision` attribute
470
471 :param Field field: A marshmallow field.
472 :rtype: dict
473 """
474 ret = {}
475 if isinstance(field, marshmallow.fields.TimeDelta):
476 ret["x-unit"] = field.precision
477 return ret
478
479
480 def make_type_list(types):
481 """Return a list of types from a type attribute
482
483 Since OpenAPI 3.1.0, "type" can be a single type as string or a list of
484 types, including 'null'. This function takes a "type" attribute as input
485 and returns it as a list, be it an empty or single-element list.
486 This is useful to factorize type-conditional code or code adding a type.
487 """
488 if types is None:
489 return []
490 if isinstance(types, str):
491 return [types]
492 return types
493
494
495 def make_min_max_attributes(validators, min_attr, max_attr):
496 """Return a dictionary of minimum and maximum attributes based on a list
497 of validators. If either minimum or maximum values are not present in any
498 of the validator objects that attribute will be omitted.
499
500 :param validators list: A list of `Marshmallow` validator objects. Each
501 objct is inspected for a minimum and maximum values
502 :param min_attr string: The OpenAPI attribute for the minimum value
503 :param max_attr string: The OpenAPI attribute for the maximum value
504 """
505 attributes = {}
506 min_list = [validator.min for validator in validators if validator.min is not None]
507 max_list = [validator.max for validator in validators if validator.max is not None]
508 if min_list:
509 attributes[min_attr] = max(min_list)
510 if max_list:
511 attributes[max_attr] = min(max_list)
512 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):
64 description = dict(id="Pet id", name="Pet name", password="Password")
7 id = fields.Int(dump_only=True, description=description["id"])
5 id = fields.Int(dump_only=True, metadata={"description": description["id"]})
86 name = fields.Str(
97 required=True,
10 deprecated=False,
11 allowEmptyValue=False,
12 description=description["name"],
8 metadata={
9 "description": description["name"],
10 "deprecated": False,
11 "allowEmptyValue": False,
12 },
1313 )
14 password = fields.Str(load_only=True, description=description["password"])
14 password = fields.Str(
15 load_only=True, metadata={"description": description["password"]}
16 )
1517
1618
1719 class SampleSchema(Schema):
3335
3436
3537 class PatternedObjectSchema(Schema):
36 count = fields.Int(dump_only=True, **{"x-count": 1})
37 count2 = fields.Int(dump_only=True, x_count2=2)
38 count = fields.Int(dump_only=True, metadata={"x-count": 1})
39 count2 = fields.Int(dump_only=True, metadata={"x_count2": 2})
3840
3941
4042 class SelfReferencingSchema(Schema):
4143 id = fields.Int()
42 if MARSHMALLOW_VERSION_INFO[0] < 3:
43 single = fields.Nested("self")
44 many = fields.Nested("self", many=True)
45 else:
46 single = fields.Nested(lambda: SelfReferencingSchema())
47 many = fields.Nested(lambda: SelfReferencingSchema(many=True))
44 single = fields.Nested(lambda: SelfReferencingSchema())
45 many = fields.Nested(lambda: SelfReferencingSchema(many=True))
4846
4947
5048 class OrderedSchema(Schema):
6058
6159 class DefaultValuesSchema(Schema):
6260 number_auto_default = fields.Int(missing=12)
63 number_manual_default = fields.Int(missing=12, doc_default=42)
61 number_manual_default = fields.Int(missing=12, metadata={"doc_default": 42})
6462 string_callable_default = fields.Str(missing=lambda: "Callable")
65 string_manual_default = fields.Str(missing=lambda: "Callable", doc_default="Manual")
63 string_manual_default = fields.Str(
64 missing=lambda: "Callable", metadata={"doc_default": "Manual"}
65 )
6666 numbers = fields.List(fields.Int, missing=list)
6767
6868
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
11
22 import pytest
33
4 from marshmallow.fields import Field, DateTime, Dict, String, Nested, List
4 from marshmallow.fields import Field, DateTime, Dict, String, Nested, List, TimeDelta
55 from marshmallow import Schema
66
77 from apispec import APISpec
88 from apispec.ext.marshmallow import MarshmallowPlugin
9 from apispec.ext.marshmallow.openapi import MARSHMALLOW_VERSION_INFO
109 from apispec.ext.marshmallow import common
1110 from apispec.exceptions import APISpecError
1211 from .schemas import (
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
708888 },
709889 )
710890 get = get_paths(spec_fixture.spec)["/pet"]["get"]
891 assert len(get["parameters"]) == 1
892 resolved_schema = {
893 "type": "array",
894 "items": build_ref(spec_fixture.spec, "schema", "Pet"),
895 }
896 request_schema = get["parameters"][0]["content"]["application/json"]["schema"]
897 assert request_schema == resolved_schema
898 response_schema = get["responses"]["200"]["content"]["application/json"][
899 "schema"
900 ]
901 assert response_schema == resolved_schema
902
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"]
711936 assert len(get["parameters"]) == 1
712937 resolved_schema = {
713938 "type": "array",
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):
9281196
9291197 result = get_schemas(spec)["SchemaWithList"]["properties"]["list_field"]
9301198 assert result == {"items": build_ref(spec, "schema", "Pet"), "type": "array"}
1199
1200
1201 class TestTimeDelta:
1202 def test_timedelta_x_unit(self, spec):
1203 class SchemaWithTimeDelta(Schema):
1204 sec = TimeDelta("seconds")
1205 day = TimeDelta("days")
1206
1207 spec.components.schema("SchemaWithTimeDelta", schema=SchemaWithTimeDelta)
1208
1209 assert (
1210 get_schemas(spec)["SchemaWithTimeDelta"]["properties"]["sec"]["x-unit"]
1211 == "seconds"
1212 )
1213 assert (
1214 get_schemas(spec)["SchemaWithTimeDelta"]["properties"]["day"]["x-unit"]
1215 == "days"
1216 )
7878 assert list(get_fields(ExcludeSchema, exclude_dump_only=True).keys()) == [
7979 "field5"
8080 ]
81
82 # regression test for https://github.com/marshmallow-code/apispec/issues/673
83 def test_schema_with_field_named_fields(self):
84 class TestSchema(Schema):
85 fields = fields.Int()
86
87 schema_fields = get_fields(TestSchema)
88 assert list(schema_fields.keys()) == ["fields"]
89 assert isinstance(schema_fields["fields"], fields.Int)
33 import pytest
44 from marshmallow import fields, validate
55
6 from apispec.ext.marshmallow.openapi import MARSHMALLOW_VERSION_INFO
7
86 from .schemas import CategorySchema, CustomList, CustomStringField, CustomIntegerField
9 from .utils import build_ref
7 from .utils import build_ref, get_schemas
108
119
1210 def test_field2choices_preserving_order(openapi):
2927 (fields.DateTime, "string"),
3028 (fields.Date, "string"),
3129 (fields.Time, "string"),
30 (fields.TimeDelta, "integer"),
3231 (fields.Email, "string"),
3332 (fields.URL, "string"),
3433 # Custom fields inherit types from their parents
6059 @pytest.mark.parametrize(
6160 ("FieldClass", "expected_format"),
6261 [
63 (fields.Integer, "int32"),
64 (fields.Float, "float"),
6562 (fields.UUID, "uuid"),
6663 (fields.DateTime, "date-time"),
6764 (fields.Date, "date"),
7673
7774
7875 def test_field_with_description(spec_fixture):
79 field = fields.Str(description="a username")
76 field = fields.Str(metadata={"description": "a username"})
8077 res = spec_fixture.openapi.field2property(field)
8178 assert res["description"] == "a username"
8279
9491
9592
9693 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))
94 field = fields.Date(missing=dt.date(2014, 7, 18))
10195 res = spec_fixture.openapi.field2property(field)
10296 assert res["default"] == dt.date(2014, 7, 18).isoformat()
10397
109103
110104
111105 def test_field_with_doc_default(spec_fixture):
112 field = fields.Str(doc_default="Manual default")
106 field = fields.Str(metadata={"doc_default": "Manual default"})
113107 res = spec_fixture.openapi.field2property(field)
114108 assert res["default"] == "Manual default"
115109
116110
117111 def test_field_with_doc_default_and_missing(spec_fixture):
118 field = fields.Int(doc_default=42, missing=12)
112 field = fields.Int(missing=12, metadata={"doc_default": 42})
119113 res = spec_fixture.openapi.field2property(field)
120114 assert res["default"] == 42
121115
135129 def test_only_allows_valid_properties_in_metadata(spec_fixture):
136130 field = fields.Str(
137131 missing="foo",
138 description="foo",
139 enum=["red", "blue"],
140 allOf=["bar"],
141 not_valid="lol",
132 metadata={
133 "description": "foo",
134 "not_valid": "lol",
135 "allOf": ["bar"],
136 "enum": ["red", "blue"],
137 },
142138 )
143139 res = spec_fixture.openapi.field2property(field)
144140 assert res["default"] == field.missing
160156
161157
162158 def test_field_with_additional_metadata(spec_fixture):
163 field = fields.Str(minLength=6, maxLength=100)
159 field = fields.Str(metadata={"minLength": 6, "maxLength": 100})
164160 res = spec_fixture.openapi.field2property(field)
165161 assert res["maxLength"] == 100
166162 assert res["minLength"] == 6
167163
168164
165 @pytest.mark.parametrize("spec_fixture", ("2.0", "3.0.0", "3.1.0"), indirect=True)
169166 def test_field_with_allow_none(spec_fixture):
170167 field = fields.Str(allow_none=True)
171168 res = spec_fixture.openapi.field2property(field)
172169 if spec_fixture.openapi.openapi_version.major < 3:
173170 assert res["x-nullable"] is True
171 elif spec_fixture.openapi.openapi_version.minor < 1:
172 assert res["nullable"] is True
174173 else:
175 assert res["nullable"] is True
174 assert "nullable" not in res
175 assert res["type"] == ["string", "null"]
176
177
178 def test_field_with_dump_only(spec_fixture):
179 field = fields.Str(dump_only=True)
180 res = spec_fixture.openapi.field2property(field)
181 assert res["readOnly"] is True
182
183
184 @pytest.mark.parametrize("spec_fixture", ("2.0", "3.0.0", "3.1.0"), indirect=True)
185 def test_field_with_load_only(spec_fixture):
186 field = fields.Str(load_only=True)
187 res = spec_fixture.openapi.field2property(field)
188 if spec_fixture.openapi.openapi_version.major < 3:
189 assert "writeOnly" not in res
190 else:
191 assert res["writeOnly"] is True
192
193
194 def test_field_with_range_no_type(spec_fixture):
195 field = fields.Field(validate=validate.Range(min=1, max=10))
196 res = spec_fixture.openapi.field2property(field)
197 assert res["x-minimum"] == 1
198 assert res["x-maximum"] == 10
199 assert "type" not in res
200
201
202 @pytest.mark.parametrize("field", (fields.Number, fields.Integer))
203 def test_field_with_range_string_type(spec_fixture, field):
204 field = field(validate=validate.Range(min=1, max=10))
205 res = spec_fixture.openapi.field2property(field)
206 assert res["minimum"] == 1
207 assert res["maximum"] == 10
208 assert isinstance(res["type"], str)
209
210
211 @pytest.mark.parametrize("spec_fixture", ("3.1.0",), indirect=True)
212 def test_field_with_range_type_list_with_number(spec_fixture):
213 @spec_fixture.openapi.map_to_openapi_type(["integer", "null"], None)
214 class NullableInteger(fields.Field):
215 """Nullable integer"""
216
217 field = NullableInteger(validate=validate.Range(min=1, max=10))
218 res = spec_fixture.openapi.field2property(field)
219 assert res["minimum"] == 1
220 assert res["maximum"] == 10
221 assert res["type"] == ["integer", "null"]
222
223
224 @pytest.mark.parametrize("spec_fixture", ("3.1.0",), indirect=True)
225 def test_field_with_range_type_list_without_number(spec_fixture):
226 @spec_fixture.openapi.map_to_openapi_type(["string", "null"], None)
227 class NullableInteger(fields.Field):
228 """Nullable integer"""
229
230 field = NullableInteger(validate=validate.Range(min=1, max=10))
231 res = spec_fixture.openapi.field2property(field)
232 assert res["x-minimum"] == 1
233 assert res["x-maximum"] == 10
234 assert res["type"] == ["string", "null"]
176235
177236
178237 def test_field_with_str_regex(spec_fixture):
207266 spec_fixture.spec.components.schema("Category", schema=CategorySchema)
208267 category = fields.Nested(
209268 CategorySchema,
210 description="A category",
211 invalid_property="not in the result",
212 x_extension="A great extension",
269 metadata={
270 "description": "A category",
271 "invalid_property": "not in the result",
272 "x_extension": "A great extension",
273 },
213274 )
214275 result = spec_fixture.openapi.field2property(category)
215276 assert result == {
273334 }
274335
275336
337 class TestField2PropertyPluck:
338 @pytest.fixture(autouse=True)
339 def _setup(self, spec_fixture):
340 self.field2property = spec_fixture.openapi.field2property
341
342 self.spec = spec_fixture.spec
343 self.spec.components.schema("Category", schema=CategorySchema)
344 self.unplucked = get_schemas(self.spec)["Category"]["properties"]["breed"]
345
346 def test_spec(self, spec_fixture):
347 breed = fields.Pluck(CategorySchema, "breed")
348 assert self.field2property(breed) == self.unplucked
349
350 def test_with_property(self):
351 breed = fields.Pluck(CategorySchema, "breed", dump_only=True)
352 assert self.field2property(breed) == {**self.unplucked, "readOnly": True}
353
354 def test_metadata(self):
355 breed = fields.Pluck(
356 CategorySchema,
357 "breed",
358 metadata={
359 "description": "Category breed",
360 "invalid_property": "not in the result",
361 "x_extension": "A great extension",
362 },
363 )
364 assert self.field2property(breed) == {
365 **self.unplucked,
366 "description": "Category breed",
367 "x-extension": "A great extension",
368 }
369
370 def test_many(self):
371 breed = fields.Pluck(CategorySchema, "breed", many=True)
372 assert self.field2property(breed) == {"type": "array", "items": self.unplucked}
373
374 def test_many_with_property(self):
375 breed = fields.Pluck(CategorySchema, "breed", many=True, dump_only=True)
376 assert self.field2property(breed) == {
377 "items": self.unplucked,
378 "type": "array",
379 "readOnly": True,
380 }
381
382
276383 def test_custom_properties_for_custom_fields(spec_fixture):
277384 def custom_string2properties(self, field, **kwargs):
278385 ret = {}
292399 assert properties["x-customString"] == (
293400 spec_fixture.openapi.openapi_version == "2.0"
294401 )
402
403
404 def test_field2property_with_non_string_metadata_keys(spec_fixture):
405 class _DesertSentinel:
406 pass
407
408 field = fields.Boolean(metadata={"description": "A description"})
409 field.metadata[_DesertSentinel()] = "to be ignored"
410 result = spec_fixture.openapi.field2property(field)
411 assert result == {"description": "A description", "type": "boolean"}
00 import pytest
1
2 from marshmallow import fields, Schema, validate
1 from 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()
101 email = fields.Email(description="email address of the user")
54 email = fields.Email(metadata={"description": "email address of the user"})
10255 name = fields.Str()
10356
10457 class Meta:
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 f"{NotASchema!r} is neither a Schema class nor a Schema instance."
255203 )
256204 with pytest.raises(ValueError, match=expected_error):
257205 openapi.schema2jsonschema(NotASchema)
260208 class TestMarshmallowSchemaToParameters:
261209 @pytest.mark.parametrize("ListClass", [fields.List, CustomList])
262210 def test_field_multiple(self, ListClass, openapi):
263 field = ListClass(fields.Str, location="querystring")
264 res = openapi.field2parameter(field, name="field", default_in=None)
211 field = ListClass(fields.Str)
212 res = openapi._field2parameter(field, name="field", location="query")
265213 assert res["in"] == "query"
266214 if openapi.openapi_version.major < 3:
267215 assert res["type"] == "array"
274222 assert res["explode"] is True
275223
276224 def test_field_required(self, openapi):
277 field = fields.Str(required=True, location="query")
278 res = openapi.field2parameter(field, name="field", default_in=None)
225 field = fields.Str(required=True)
226 res = openapi._field2parameter(field, name="field", location="query")
279227 assert res["required"] is True
280228
281 def test_invalid_schema(self, openapi):
282 with pytest.raises(ValueError):
283 openapi.schema2parameters(None)
229 def test_schema_partial(self, openapi):
230 class UserSchema(Schema):
231 field = fields.Str(required=True)
232
233 res_nodump = openapi.schema2parameters(
234 UserSchema(partial=True), location="query"
235 )
236
237 param = res_nodump[0]
238 assert param["required"] is False
239
240 def test_schema_partial_list(self, openapi):
241 class UserSchema(Schema):
242 field = fields.Str(required=True)
243 partial_field = fields.Str(required=True)
244
245 res_nodump = openapi.schema2parameters(
246 UserSchema(partial=("partial_field",)), location="query"
247 )
248
249 param = next(p for p in res_nodump if p["name"] == "field")
250 assert param["required"] is True
251 param = next(p for p in res_nodump if p["name"] == "partial_field")
252 assert param["required"] is False
284253
285254 # json/body is invalid for OpenAPI 3
286255 @pytest.mark.parametrize("openapi", ("2.0",), indirect=True)
289258 name = fields.Str()
290259 email = fields.Email()
291260
292 res = openapi.schema2parameters(UserSchema, default_in="body")
261 res = openapi.schema2parameters(UserSchema, location="body")
293262 assert len(res) == 1
294263 param = res[0]
295264 assert param["in"] == "body"
302271 name = fields.Str()
303272 email = fields.Email(dump_only=True)
304273
305 res_nodump = openapi.schema2parameters(UserSchema, default_in="body")
274 res_nodump = openapi.schema2parameters(UserSchema, location="body")
306275 assert len(res_nodump) == 1
307276 param = res_nodump[0]
308277 assert param["in"] == "body"
315284 name = fields.Str()
316285 email = fields.Email()
317286
318 res = openapi.schema2parameters(UserSchema(many=True), default_in="body")
287 res = openapi.schema2parameters(UserSchema(many=True), location="body")
319288 assert len(res) == 1
320289 param = res[0]
321290 assert param["in"] == "body"
327296 name = fields.Str()
328297 email = fields.Email()
329298
330 res = openapi.schema2parameters(UserSchema, default_in="query")
299 res = openapi.schema2parameters(UserSchema, location="query")
331300 assert len(res) == 2
332301 res.sort(key=lambda param: param["name"])
333302 assert res[0]["name"] == "email"
340309 name = fields.Str()
341310 email = fields.Email()
342311
343 res = openapi.schema2parameters(UserSchema(), default_in="query")
312 res = openapi.schema2parameters(UserSchema(), location="query")
344313 assert len(res) == 2
345314 res.sort(key=lambda param: param["name"])
346315 assert res[0]["name"] == "email"
354323 email = fields.Email()
355324
356325 with pytest.raises(AssertionError):
357 openapi.schema2parameters(UserSchema(many=True), default_in="query")
326 openapi.schema2parameters(UserSchema(many=True), location="query")
358327
359328 def test_fields_query(self, openapi):
360 field_dict = {"name": fields.Str(), "email": fields.Email()}
361 res = openapi.fields2parameters(field_dict, default_in="query")
329 class MySchema(Schema):
330 name = fields.Str()
331 email = fields.Email()
332
333 res = openapi.schema2parameters(MySchema, location="query")
362334 assert len(res) == 2
363335 res.sort(key=lambda param: param["name"])
364336 assert res[0]["name"] == "email"
370342 class NotASchema:
371343 pass
372344
373 expected_error = "{!r} doesn't have either `fields` or `_declared_fields`".format(
374 NotASchema
345 expected_error = (
346 f"{NotASchema!r} is neither a Schema class nor a Schema instance."
375347 )
376348 with pytest.raises(ValueError, match=expected_error):
377349 openapi.schema2jsonschema(NotASchema)
418390 assert ("i" in props) == (modifier == "only")
419391 assert ("j" not in props) == (modifier == "only")
420392
393 def test_schema2jsonschema_with_plucked_field(self, spec_fixture):
394 class PetSchema(Schema):
395 breed = fields.Pluck(CategorySchema, "breed")
396
397 category_schema = spec_fixture.openapi.schema2jsonschema(CategorySchema)
398 pet_schema = spec_fixture.openapi.schema2jsonschema(PetSchema)
399 assert (
400 pet_schema["properties"]["breed"] == category_schema["properties"]["breed"]
401 )
402
421403 def test_schema2jsonschema_with_nested_fields_with_adhoc_changes(
422404 self, spec_fixture
423405 ):
440422 assert props["Category"] == spec_fixture.openapi.schema2jsonschema(
441423 CategorySchema
442424 )
425
426 def test_schema2jsonschema_with_plucked_fields_with_adhoc_changes(
427 self, spec_fixture
428 ):
429 category_schema = CategorySchema()
430 category_schema.fields["breed"].dump_only = True
431
432 class PetSchema(Schema):
433 breed = fields.Pluck(category_schema, "breed", many=True)
434
435 spec_fixture.spec.components.schema("Pet", schema=PetSchema)
436 props = get_schemas(spec_fixture.spec)["Pet"]["properties"]
437
438 assert props["breed"]["items"]["readOnly"] is True
443439
444440 def test_schema2jsonschema_with_nested_excluded_fields(self, spec):
445441 category_schema = CategorySchema(exclude=("breed",))
476472 "required": True,
477473 "type": "string",
478474 },
479 openapi.field2parameter(
475 openapi._field2parameter(
480476 field=fields.List(
481477 fields.Str(),
482478 validate=validate.OneOf(["freddie", "roger"]),
483 location="querystring",
484479 ),
485 default_in=None,
480 location="query",
486481 name="body",
487482 ),
488483 ]
489 + openapi.schema2parameters(PageSchema, default_in="query"),
484 + openapi.schema2parameters(PageSchema, location="query"),
490485 "responses": {200: {"schema": PetSchema, "description": "A pet"}},
491486 },
492487 "post": {
499494 "type": "string",
500495 }
501496 ]
502 + openapi.schema2parameters(CategorySchema, default_in="body")
497 + openapi.schema2parameters(CategorySchema, location="body")
503498 ),
504499 "responses": {201: {"schema": PetSchema, "description": "A pet"}},
505500 },
534529 "required": True,
535530 "schema": {"type": "string"},
536531 },
537 openapi.field2parameter(
532 openapi._field2parameter(
538533 field=fields.List(
539534 fields.Str(),
540535 validate=validate.OneOf(["freddie", "roger"]),
541 location="querystring",
542536 ),
543 default_in=None,
537 location="query",
544538 name="body",
545539 ),
546540 ]
547 + openapi.schema2parameters(PageSchema, default_in="query"),
541 + openapi.schema2parameters(PageSchema, location="query"),
548542 "responses": {
549543 200: {
550544 "description": "success",
585579 class ValidationSchema(Schema):
586580 id = fields.Int(dump_only=True)
587581 range = fields.Int(validate=validate.Range(min=1, max=10))
582 range_no_upper = fields.Float(validate=validate.Range(min=1))
588583 multiple_ranges = fields.Int(
589584 validate=[
590585 validate.Range(min=1),
610605 equal_length = fields.Str(
611606 validate=[validate.Length(equal=5), validate.Length(min=1, max=10)]
612607 )
608 date_range = fields.DateTime(
609 validate=validate.Range(
610 min=datetime(1900, 1, 1),
611 )
612 )
613613
614614 @pytest.mark.parametrize(
615615 ("field", "properties"),
616616 [
617617 ("range", {"minimum": 1, "maximum": 10}),
618 ("range_no_upper", {"minimum": 1}),
618619 ("multiple_ranges", {"minimum": 3, "maximum": 7}),
619620 ("list_length", {"minItems": 1, "maxItems": 10}),
620621 ("custom_list_length", {"minItems": 1, "maxItems": 10}),
622623 ("custom_field_length", {"minLength": 1, "maxLength": 10}),
623624 ("multiple_lengths", {"minLength": 3, "maxLength": 7}),
624625 ("equal_length", {"minLength": 5, "maxLength": 5}),
626 ("date_range", {"x-minimum": datetime(1900, 1, 1)}),
625627 ],
626628 )
627629 def test_properties(self, field, properties, spec):
2525 @pytest.mark.parametrize("docstring", (None, "", "---"))
2626 def test_load_operations_from_docstring_empty_docstring(docstring):
2727 assert yaml_utils.load_operations_from_docstring(docstring) == {}
28
29
30 def test_dict_to_yaml_unicode():
31 assert yaml_utils.dict_to_yaml({"가": "나"}) == '"\\uAC00": "\\uB098"\n'
32 assert yaml_utils.dict_to_yaml({"가": "나"}, {"allow_unicode": True}) == "가: 나\n"
2020 return spec.to_dict()["components"]["parameters"]
2121
2222
23 def get_headers(spec):
24 if spec.openapi_version.major < 3:
25 return spec.to_dict()["headers"]
26 return spec.to_dict()["components"]["headers"]
27
28
2329 def get_examples(spec):
2430 return spec.to_dict()["components"]["examples"]
2531
00 [tox]
11 envlist=
22 lint
3 py{35,36,37,38}-marshmallow2
4 py{35,36,37,38}-marshmallow3
3 py{36,37,38,39}-marshmallow3
54 py38-marshmallowdev
65 docs
76
87 [testenv]
98 extras = tests
109 deps =
11 marshmallow2: marshmallow>=2.0.0,<3.0.0
12 marshmallow3: marshmallow>=3.0.0,<4.0.0
10 marshmallow3: marshmallow>=3.10.0,<4.0.0
1311 marshmallowdev: https://github.com/marshmallow-code/marshmallow/archive/dev.tar.gz
1412 commands = pytest {posargs}
1513
2725 [testenv:watch-docs]
2826 deps = sphinx-autobuild
2927 extras = docs
30 commands = sphinx-autobuild --open-browser docs/ docs/_build {posargs} -z src/apispec -s 2
28 commands = sphinx-autobuild --open-browser docs/ docs/_build {posargs} --watch src/apispec --delay 2
3129
3230 [testenv:watch-readme]
3331 deps = restview