Codebase list apispec / 3e5b5814-1152-45e1-96dc-6d1b4d48f9e8/upstream
Import upstream version 4.0.0 Kali Janitor 3 years ago
25 changed file(s) with 302 addition(s) and 359 deletion(s). Raw diff Collapse all Expand all
22 rev: v2.4.1
33 hooks:
44 - id: pyupgrade
5 args: [--py3-plus]
5 args: [--py36-plus]
66 - repo: https://github.com/python/black
77 rev: 19.10b0
88 hooks:
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 - David Bishop `@teancom <https://github.com/teancom>`_
66 - Andrea Ghensi `@sanzoghenzo <https://github.com/sanzoghenzo>`_
00 Changelog
11 ---------
2
3 4.0.0 (2020-09-30)
4 ******************
5
6 Features:
7
8 - *Backwards-incompatible*: Automatically generate references for schemas
9 passed as strings in responses and request bodies. When using
10 ``MarshmallowPlugin``, if a schema is passed as string, the marshmallow
11 registry is looked up for this schema name and if none is found, the name is
12 assumed to be a reference to a manually created schema and a reference is
13 generated. No exception is raised anymore if the schema name can't be found
14 in the registry. (:pr:554)
15
16 4.0.0b1 (2020-09-06)
17 ********************
18
19 Features:
20
21 - *Backwards-incompatible*: Ignore ``location`` field metadata. This attribute
22 was used in webargs but it has now been dropped. A ``Schema`` can now only
23 have a single location. This simplifies the logic in ``OpenAPIConverter``
24 methods, where ``default_in`` argument now becomes ``location``. (:pr:`526`)
25 - *Backwards-incompatible*: Don't document ``int`` format as ``"int32"`` and
26 ``float`` format as ``"float"``, as those are platform-dependent (:pr:`595`).
27
28 Refactoring:
29
30 - ``OpenAPIConverter.field2parameters`` and
31 ``OpenAPIConverter.property2parameter`` are removed.
32 ``OpenAPIConverter.field2parameter`` becomes private. (:pr:`581`)
33
34 Other changes:
35
36 - Drop support for marshmallow 2. Marshmallow 3.x is required. (:pr:`583`)
37 - Drop support for Python 3.5. Python 3.6+ is required. (:pr:`582`)
38
39
40 3.3.2 (2020-08-29)
41 ******************
42
43 Bug fixes:
44
45 - Fix crash when field metadata contains non-string keys (:pr:`596`).
46 Thanks :user:`sanzoghenzo` for the fix.
247
348 3.3.1 (2020-06-06)
449 ******************
853 - Fix ``MarshmallowPlugin`` crash when ``resolve_schema_dict`` is passed a
954 schema as string and ``schema_name_resolver`` returns ``None``
1055 (:issue:`566`). Thanks :user:`black3r` for reporting and thanks
11 :user:`Bangterm` for the PR.
56 :user:`Bangertm` for the PR.
1257
1358 3.3.0 (2020-02-14)
1459 ******************
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
31
3229 - py36-marshmallow3
33
3430 - py37-marshmallow3
35
36 - py38-marshmallow2
3731 - py38-marshmallow3
3832
3933 - py38-marshmallowdev
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 -------------
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.
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.8.3", "flake8-bugbear==20.1.4", "pre-commit~=2.4"],
77 "docs": [
8 "marshmallow>=2.19.2",
8 "marshmallow>=3.0.0",
99 "pyyaml==5.3.1",
10 "sphinx==3.0.4",
10 "sphinx==3.2.1",
1111 "sphinx-issues==1.2.0",
12 "sphinx-rtd-theme==0.4.3",
12 "sphinx-rtd-theme==0.5.0",
1313 ],
1414 }
1515 EXTRAS_REQUIRE["tests"] = (
1616 EXTRAS_REQUIRE["yaml"]
1717 + EXTRAS_REQUIRE["validation"]
18 + ["marshmallow>=2.19.2", "pytest", "mock"]
18 + ["marshmallow>=3.0.0", "pytest", "mock"]
1919 )
2020 EXTRAS_REQUIRE["dev"] = EXTRAS_REQUIRE["tests"] + EXTRAS_REQUIRE["lint"] + ["tox"]
2121
5959 license="MIT",
6060 zip_safe=False,
6161 keywords="apispec swagger openapi specification oas documentation spec rest api",
62 python_requires=">=3.5",
62 python_requires=">=3.6",
6363 classifiers=[
6464 "License :: OSI Approved :: MIT License",
6565 "Programming Language :: Python :: 3",
66 "Programming Language :: Python :: 3.5",
6766 "Programming Language :: Python :: 3.6",
6867 "Programming Language :: Python :: 3.7",
6968 "Programming Language :: Python :: 3.8",
22 from .core import APISpec
33 from .plugin import BasePlugin
44
5 __version__ = "3.3.1"
5 __version__ = "4.0.0"
66 __all__ = ["APISpec", "BasePlugin"]
7171 """
7272 if name in self._schemas:
7373 raise DuplicateComponentNameError(
74 'Another schema with name "{}" is already registered.'.format(name)
74 f'Another schema with name "{name}" is already registered.'
7575 )
7676 component = component or {}
7777 ret = component.copy()
150150 """
151151 if name in self._examples:
152152 raise DuplicateComponentNameError(
153 'Another example with name "{}" is already registered.'.format(name)
153 f'Another example with name "{name}" is already registered.'
154154 )
155155 self._examples[name] = component
156156 return self
242242 summary=None,
243243 description=None,
244244 parameters=None,
245 **kwargs
245 **kwargs,
246246 ):
247247 """Add a new path object to the spec.
248248
299299 Otherwise, it is assumed to be a reference name as string and the corresponding $ref
300300 string is returned.
301301
302 :param str obj_type: "parameter" or "response"
303 :param dict|str obj: parameter or response in dict form or as ref_id string
302 :param str obj_type: "schema", "parameter", "response" or "security_scheme"
303 :param dict|str obj: object in dict form or as ref_id string
304304 """
305305 if isinstance(obj, dict):
306306 return obj
307307 return build_reference(obj_type, self.openapi_version.major, obj)
308
309 def _resolve_schema(self, obj):
310 """Replace schema reference as string with a $ref if needed."""
311 if not isinstance(obj, dict):
312 return
313 if self.openapi_version.major < 3:
314 if "schema" in obj:
315 obj["schema"] = self.get_ref("schema", obj["schema"])
316 else:
317 if "content" in obj:
318 for content in obj["content"].values():
319 if "schema" in content:
320 content["schema"] = self.get_ref("schema", content["schema"])
308321
309322 def clean_parameters(self, parameters):
310323 """Ensure that all parameters with "in" equal to "path" are also required
322335 missing_attrs = [attr for attr in ("name", "in") if attr not in parameter]
323336 if missing_attrs:
324337 raise InvalidParameterError(
325 "Missing keys {} for parameter".format(missing_attrs)
338 f"Missing keys {missing_attrs} for parameter"
326339 )
327340
328341 # OpenAPI Spec 3 and 2 don't allow for duplicated parameters
364377 for operation in (operations or {}).values():
365378 if "parameters" in operation:
366379 operation["parameters"] = self.clean_parameters(operation["parameters"])
380 # OAS 3
381 if "requestBody" in operation:
382 self._resolve_schema(operation["requestBody"])
367383 if "responses" in operation:
368384 responses = OrderedDict()
369385 for code, response in operation["responses"].items():
372388 except (TypeError, ValueError):
373389 if self.openapi_version.major < 3 and code != "default":
374390 warnings.warn("Non-integer code not allowed in OpenAPI < 3")
375
391 self._resolve_schema(response)
376392 responses[str(code)] = self.get_ref("response", response)
377393 operation["responses"] = responses
5959 # 'format': 'date-time',
6060 # 'readOnly': True,
6161 # 'type': 'string'},
62 # 'id': {'format': 'int32',
63 # 'readOnly': True,
62 # 'id': {'readOnly': True,
6463 # 'type': 'integer'},
6564 # 'name': {'description': "The user's name",
6665 # 'type': 'string'}},
139138 class MyCustomField(Integer):
140139 # ...
141140
142 @ma_plugin.map_to_openapi_type(Integer) # will map to ('integer', 'int32')
141 @ma_plugin.map_to_openapi_type(Integer) # will map to ('integer', None)
143142 class MyCustomFieldThatsKindaLikeAnInteger(Integer):
144143 # ...
145144 """
1919 return schema()
2020 if isinstance(schema, marshmallow.Schema):
2121 return schema
22 try:
23 return marshmallow.class_registry.get_class(schema)()
24 except marshmallow.exceptions.RegistryError:
25 raise ValueError(
26 "{!r} is not a marshmallow.Schema subclass or instance and has not"
27 " been registered in the marshmallow class registry.".format(schema)
28 )
22 return marshmallow.class_registry.get_class(schema)()
2923
3024
3125 def resolve_schema_cls(schema):
3832 return schema
3933 if isinstance(schema, marshmallow.Schema):
4034 return type(schema)
41 try:
42 return marshmallow.class_registry.get_class(schema)
43 except marshmallow.exceptions.RegistryError:
44 raise ValueError(
45 "{!r} is not a marshmallow.Schema subclass or instance and has not"
46 " been registered in the marshmallow class registry.".format(schema)
47 )
35 return marshmallow.class_registry.get_class(schema)
4836
4937
5038 def get_fields(schema, *, exclude_dump_only=False):
6048 fields = copy.deepcopy(schema._declared_fields)
6149 else:
6250 raise ValueError(
63 "{!r} doesn't have either `fields` or `_declared_fields`.".format(schema)
51 f"{schema!r} doesn't have either `fields` or `_declared_fields`."
6452 )
6553 Meta = getattr(schema, "Meta", None)
6654 warn_if_fields_defined_in_meta(fields, Meta)
9078
9179 :param dict fields: A dictionary of fields name field object pairs
9280 :param Meta: the schema's Meta class
93 :param bool exclude_dump_only: whether to filter fields in Meta.dump_only
81 :param bool exclude_dump_only: whether to filter dump_only fields
9482 """
9583 exclude = list(getattr(Meta, "exclude", []))
9684 if exclude_dump_only:
9785 exclude.extend(getattr(Meta, "dump_only", []))
9886
9987 filtered_fields = OrderedDict(
100 (key, value) for key, value in fields.items() if key not in exclude
88 (key, value)
89 for key, value in fields.items()
90 if key not in exclude and not (exclude_dump_only and value.dump_only)
10191 )
10292
10393 return filtered_fields
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),
211206 else:
212207 default = field.missing
213208 if default is not marshmallow.missing and not callable(default):
214 if MARSHMALLOW_VERSION_INFO[0] >= 3:
215 default = field._serialize(default, None, None)
209 default = field._serialize(default, None, None)
216210 ret["default"] = default
217211 return ret
218212
396390 metadata = {
397391 key.replace("_", "-") if key.startswith("x_") else key: value
398392 for key, value in field.metadata.items()
393 if isinstance(key, str)
399394 }
400395
401396 # Avoid validation error with "Additional properties not allowed"
435430 """
436431 ret = {}
437432 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)
433 ret["items"] = self.field2property(field.inner)
442434 return ret
443435
444436 def dict2properties(self, field, **kwargs):
452444 """
453445 ret = {}
454446 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
447 value_field = field.value_field
448 if value_field:
449 ret["additionalProperties"] = self.field2property(value_field)
450 return ret
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(
171 field_obj,
172 name=self._observed_name(field_obj, field_name),
173 default_in=default_in,
128 return [
129 self._field2parameter(
130 field_obj, name=field_obj.data_key or field_name, location=location,
174131 )
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):
132 for field_name, field_obj in fields.items()
133 ]
134
135 def _field2parameter(self, field, *, name, location):
193136 """Return an OpenAPI parameter as a `dict`, given a marshmallow
194137 :class:`Field <marshmallow.Field>`.
195138
196139 https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#parameterObject
197140 """
198 location = field.metadata.get("location", None)
141 ret = {"in": location, "name": name, "required": field.required}
142
199143 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]
144 multiple = isinstance(field, marshmallow.fields.List)
145
146 if self.openapi_version.major < 3:
147 if multiple:
148 ret["collectionFormat"] = "multi"
149 ret.update(prop)
235150 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
151 if multiple:
152 ret["explode"] = True
153 ret["style"] = "form"
154 if prop.get("description", None):
155 ret["description"] = prop.pop("description")
156 ret["schema"] = prop
248157 return ret
249158
250159 def schema2jsonschema(self, schema):
285194 jsonschema = {"type": "object", "properties": OrderedDict() if ordered else {}}
286195
287196 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
197 observed_field_name = field_obj.data_key or field_name
198 prop = self.field2property(field_obj)
199 jsonschema["properties"][observed_field_name] = prop
291200
292201 if field_obj.required:
293202 if not partial or (
3737
3838 #Output
3939 [
40 {"in": "query", "name": "id", "required": False, "schema": {"type": "integer", "format": "int32"}},
40 {"in": "query", "name": "id", "required": False, "schema": {"type": "integer"}},
4141 {"in": "query", "name": "name", "required": False, "schema": {"type": "string"}}
4242 ]
4343
7575 ):
7676 schema_instance = resolve_schema_instance(parameter.pop("schema"))
7777 resolved += self.converter.schema2parameters(
78 schema_instance, default_in=parameter.pop("in"), **parameter
78 schema_instance, location=parameter.pop("in"), **parameter
7979 )
8080 else:
8181 self.resolve_schema(parameter)
161161
162162 def resolve_schema_dict(self, schema):
163163 """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.
164 to a Schema class or a schema reference or an OpenAPI Schema Object
165 containing one of the above to an OpenAPI Schema Object or Reference Object.
166166
167167 If the input is a marshmallow Schema class, object or a string that resolves
168168 to a Schema class the Schema will be translated to an OpenAPI Schema Object
102102 < self.MAX_EXCLUSIVE_VERSION
103103 ):
104104 raise exceptions.APISpecError(
105 "Not a valid OpenAPI version number: {}".format(openapi_version)
105 f"Not a valid OpenAPI version number: {openapi_version}"
106106 )
107107 super().__init__(openapi_version)
108108
00 from marshmallow import Schema, fields
1
2 from apispec.ext.marshmallow.openapi import MARSHMALLOW_VERSION_INFO
31
42
53 class PetSchema(Schema):
3937
4038 class SelfReferencingSchema(Schema):
4139 id = fields.Int()
42 if MARSHMALLOW_VERSION_INFO[0] < 3:
43 single = fields.Nested("self")
44 many = fields.Nested("self", many=True)
45 else:
46 single = fields.Nested(lambda: SelfReferencingSchema())
47 many = fields.Nested(lambda: SelfReferencingSchema(many=True))
40 single = fields.Nested(lambda: SelfReferencingSchema())
41 many = fields.Nested(lambda: SelfReferencingSchema(many=True))
4842
4943
5044 class OrderedSchema(Schema):
5959 version="1.0.0",
6060 openapi_version=openapi_version,
6161 info={"description": description},
62 **security_kwargs
62 **security_kwargs,
6363 )
6464
6565
306306 }
307307 ],
308308 "responses": {
309 "200": {"schema": "Pet", "description": "successful operation"},
309 "200": {"description": "successful operation"},
310310 "400": {"description": "Invalid ID supplied"},
311311 "404": {"description": "Pet not found"},
312312 },
617617 with pytest.raises(APISpecError, match=message):
618618 spec.path("/pet/{petId}", operations={"dummy": {}})
619619
620 def test_path_resolve_response_schema(self, spec):
621 schema = {"schema": "PetSchema"}
622 if spec.openapi_version.major >= 3:
623 schema = {"content": {"application/json": schema}}
624 spec.path("/pet/{petId}", operations={"get": {"responses": {"200": schema}}})
625 resp = get_paths(spec)["/pet/{petId}"]["get"]["responses"]["200"]
626 if spec.openapi_version.major < 3:
627 schema = resp["schema"]
628 else:
629 schema = resp["content"]["application/json"]["schema"]
630 assert schema == build_ref(spec, "schema", "PetSchema")
631
632 # requestBody only exists in OAS 3
633 @pytest.mark.parametrize("spec", ("3.0.0",), indirect=True)
634 def test_path_resolve_request_body(self, spec):
635 spec.path(
636 "/pet/{petId}",
637 operations={
638 "get": {
639 "requestBody": {
640 "content": {"application/json": {"schema": "PetSchema"}}
641 }
642 }
643 },
644 )
645 assert get_paths(spec)["/pet/{petId}"]["get"]["requestBody"]["content"][
646 "application/json"
647 ]["schema"] == build_ref(spec, "schema", "PetSchema")
648
620649
621650 class TestPlugins:
622651 @staticmethod
741770 self.output = output
742771
743772 def path_helper(self, path, operations, **kwargs):
744 self.output.append("plugin_{}_path".format(self.index))
773 self.output.append(f"plugin_{self.index}_path")
745774
746775 def operation_helper(self, path, operations, **kwargs):
747 self.output.append("plugin_{}_operations".format(self.index))
776 self.output.append(f"plugin_{self.index}_operations")
748777
749778 def test_plugins_order(self):
750779 """Test plugins execution order in APISpec.path
66
77 from apispec import APISpec
88 from apispec.ext.marshmallow import MarshmallowPlugin
9 from apispec.ext.marshmallow.openapi import MARSHMALLOW_VERSION_INFO
109 from apispec.ext.marshmallow import common
1110 from apispec.exceptions import APISpecError
1211 from .schemas import (
439438 p = get_paths(spec_fixture.spec)["/pet"]
440439 get = p["get"]
441440 assert get["parameters"] == spec_fixture.openapi.schema2parameters(
442 PetSchema(), default_in="query"
441 PetSchema(), location="query"
443442 )
444443 post = p["post"]
445444 assert post["parameters"] == spec_fixture.openapi.schema2parameters(
446445 PetSchema,
447 default_in="body",
446 location="body",
448447 required=True,
449448 name="pet",
450449 description="a pet schema",
468467 p = get_paths(spec_fixture.spec)["/pet"]
469468 get = p["get"]
470469 assert get["parameters"] == spec_fixture.openapi.schema2parameters(
471 PetSchema(), default_in="query"
470 PetSchema(), location="query"
472471 )
473472 for parameter in get["parameters"]:
474473 description = parameter.get("description", False)
818817
819818 def test_schema_global_state_untouched_2parameters(self, spec_fixture):
820819 assert get_nested_schema(RunSchema, "sample") is None
821 data = spec_fixture.openapi.schema2parameters(RunSchema)
820 data = spec_fixture.openapi.schema2parameters(RunSchema, location="json")
822821 json.dumps(data)
823822 assert get_nested_schema(RunSchema, "sample") is None
823
824 def test_resolve_schema_dict_ref_as_string(self, spec):
825 schema = {"schema": "PetSchema"}
826 if spec.openapi_version.major >= 3:
827 schema = {"content": {"application/json": schema}}
828 spec.path("/pet/{petId}", operations={"get": {"responses": {"200": schema}}})
829 resp = get_paths(spec)["/pet/{petId}"]["get"]["responses"]["200"]
830 if spec.openapi_version.major < 3:
831 schema = resp["schema"]
832 else:
833 schema = resp["content"]["application/json"]["schema"]
834 assert schema == build_ref(spec, "schema", "PetSchema")
824835
825836
826837 class TestCircularReference:
882893 assert "default" not in props["numbers"]
883894
884895
885 @pytest.mark.skipif(
886 MARSHMALLOW_VERSION_INFO[0] < 3, reason="Values ignored in marshmallow 2"
887 )
888896 class TestDictValues:
889897 def test_dict_values_resolve_to_additional_properties(self, spec):
890898 class SchemaWithDict(Schema):
22
33 import pytest
44 from marshmallow import fields, validate
5
6 from apispec.ext.marshmallow.openapi import MARSHMALLOW_VERSION_INFO
75
86 from .schemas import CategorySchema, CustomList, CustomStringField, CustomIntegerField
97 from .utils import build_ref
6058 @pytest.mark.parametrize(
6159 ("FieldClass", "expected_format"),
6260 [
63 (fields.Integer, "int32"),
64 (fields.Float, "float"),
6561 (fields.UUID, "uuid"),
6662 (fields.DateTime, "date-time"),
6763 (fields.Date, "date"),
9490
9591
9692 def test_datetime_field_with_missing(spec_fixture):
97 if MARSHMALLOW_VERSION_INFO[0] < 3:
98 field = fields.Date(missing=dt.date(2014, 7, 18).isoformat())
99 else:
100 field = fields.Date(missing=dt.date(2014, 7, 18))
93 field = fields.Date(missing=dt.date(2014, 7, 18))
10194 res = spec_fixture.openapi.field2property(field)
10295 assert res["default"] == dt.date(2014, 7, 18).isoformat()
10396
292285 assert properties["x-customString"] == (
293286 spec_fixture.openapi.openapi_version == "2.0"
294287 )
288
289
290 def test_field2property_with_non_string_metadata_keys(spec_fixture):
291 class _DesertSentinel:
292 pass
293
294 field = fields.Boolean(description="A description")
295 field.metadata[_DesertSentinel()] = "to be ignored"
296 result = spec_fixture.openapi.field2property(field)
297 assert result == {"description": "A description", "type": "boolean"}
22 from marshmallow import fields, Schema, validate
33
44 from apispec.ext.marshmallow import MarshmallowPlugin
5 from apispec.ext.marshmallow.openapi import MARSHMALLOW_VERSION_INFO
65 from apispec import exceptions, utils, APISpec
76
87 from .schemas import CustomList, CustomStringField
1110
1211 class TestMarshmallowFieldToOpenAPI:
1312 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")
13 class MySchema(Schema):
14 field = fields.Str(default="foo", missing="bar")
15
16 res = openapi.schema2parameters(MySchema, location="query")
1617 if openapi.openapi_version.major < 3:
1718 assert res[0]["default"] == "bar"
1819 else:
1920 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"
6321
6422 # json/body is invalid for OpenAPI 3
6523 @pytest.mark.parametrize("openapi", ("2.0",), indirect=True)
6826 id = fields.Int()
6927
7028 schema = ExampleSchema(many=True)
71 res = openapi.schema2parameters(schema=schema, default_in="json")
29 res = openapi.schema2parameters(schema=schema, location="json")
7230 assert res[0]["in"] == "body"
7331
7432 def test_fields_with_dump_only(self, openapi):
7533 class UserSchema(Schema):
7634 name = fields.Str(dump_only=True)
7735
78 res = openapi.fields2parameters(UserSchema._declared_fields, default_in="query")
79 assert len(res) == 0
80 res = openapi.fields2parameters(UserSchema().fields, default_in="query")
36 res = openapi.schema2parameters(schema=UserSchema(), location="query")
8137 assert len(res) == 0
8238
8339 class UserSchema(Schema):
8642 class Meta:
8743 dump_only = ("name",)
8844
89 res = openapi.schema2parameters(schema=UserSchema, default_in="query")
45 res = openapi.schema2parameters(schema=UserSchema(), location="query")
9046 assert len(res) == 0
9147
9248
9349 class TestMarshmallowSchemaToModelDefinition:
94 def test_invalid_schema(self, openapi):
95 with pytest.raises(ValueError):
96 openapi.schema2jsonschema(None)
97
9850 def test_schema2jsonschema_with_explicit_fields(self, openapi):
9951 class UserSchema(Schema):
10052 _id = fields.Int()
11365 assert props["email"]["format"] == "email"
11466 assert props["email"]["description"] == "email address of the user"
11567
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):
68 def test_schema2jsonschema_override_name(self, openapi):
14669 class ExampleSchema(Schema):
14770 _id = fields.Int(data_key="id")
14871 _global = fields.Int(data_key="global")
226149 assert "email" not in props
227150
228151 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
152 fields_dict = {"user_id": fields.Int(data_key="id", required=True)}
236153 res = openapi.fields2jsonschema(fields_dict)
237154 assert res["required"] == ["id"]
238155
260177 class TestMarshmallowSchemaToParameters:
261178 @pytest.mark.parametrize("ListClass", [fields.List, CustomList])
262179 def test_field_multiple(self, ListClass, openapi):
263 field = ListClass(fields.Str, location="querystring")
264 res = openapi.field2parameter(field, name="field", default_in=None)
180 field = ListClass(fields.Str)
181 res = openapi._field2parameter(field, name="field", location="query")
265182 assert res["in"] == "query"
266183 if openapi.openapi_version.major < 3:
267184 assert res["type"] == "array"
274191 assert res["explode"] is True
275192
276193 def test_field_required(self, openapi):
277 field = fields.Str(required=True, location="query")
278 res = openapi.field2parameter(field, name="field", default_in=None)
194 field = fields.Str(required=True)
195 res = openapi._field2parameter(field, name="field", location="query")
279196 assert res["required"] is True
280
281 def test_invalid_schema(self, openapi):
282 with pytest.raises(ValueError):
283 openapi.schema2parameters(None)
284197
285198 # json/body is invalid for OpenAPI 3
286199 @pytest.mark.parametrize("openapi", ("2.0",), indirect=True)
289202 name = fields.Str()
290203 email = fields.Email()
291204
292 res = openapi.schema2parameters(UserSchema, default_in="body")
205 res = openapi.schema2parameters(UserSchema, location="body")
293206 assert len(res) == 1
294207 param = res[0]
295208 assert param["in"] == "body"
302215 name = fields.Str()
303216 email = fields.Email(dump_only=True)
304217
305 res_nodump = openapi.schema2parameters(UserSchema, default_in="body")
218 res_nodump = openapi.schema2parameters(UserSchema, location="body")
306219 assert len(res_nodump) == 1
307220 param = res_nodump[0]
308221 assert param["in"] == "body"
315228 name = fields.Str()
316229 email = fields.Email()
317230
318 res = openapi.schema2parameters(UserSchema(many=True), default_in="body")
231 res = openapi.schema2parameters(UserSchema(many=True), location="body")
319232 assert len(res) == 1
320233 param = res[0]
321234 assert param["in"] == "body"
327240 name = fields.Str()
328241 email = fields.Email()
329242
330 res = openapi.schema2parameters(UserSchema, default_in="query")
243 res = openapi.schema2parameters(UserSchema, location="query")
331244 assert len(res) == 2
332245 res.sort(key=lambda param: param["name"])
333246 assert res[0]["name"] == "email"
340253 name = fields.Str()
341254 email = fields.Email()
342255
343 res = openapi.schema2parameters(UserSchema(), default_in="query")
256 res = openapi.schema2parameters(UserSchema(), location="query")
344257 assert len(res) == 2
345258 res.sort(key=lambda param: param["name"])
346259 assert res[0]["name"] == "email"
354267 email = fields.Email()
355268
356269 with pytest.raises(AssertionError):
357 openapi.schema2parameters(UserSchema(many=True), default_in="query")
270 openapi.schema2parameters(UserSchema(many=True), location="query")
358271
359272 def test_fields_query(self, openapi):
360 field_dict = {"name": fields.Str(), "email": fields.Email()}
361 res = openapi.fields2parameters(field_dict, default_in="query")
273 class MySchema(Schema):
274 name = fields.Str()
275 email = fields.Email()
276
277 res = openapi.schema2parameters(MySchema, location="query")
362278 assert len(res) == 2
363279 res.sort(key=lambda param: param["name"])
364280 assert res[0]["name"] == "email"
476392 "required": True,
477393 "type": "string",
478394 },
479 openapi.field2parameter(
395 openapi._field2parameter(
480396 field=fields.List(
481 fields.Str(),
482 validate=validate.OneOf(["freddie", "roger"]),
483 location="querystring",
397 fields.Str(), validate=validate.OneOf(["freddie", "roger"]),
484398 ),
485 default_in=None,
399 location="query",
486400 name="body",
487401 ),
488402 ]
489 + openapi.schema2parameters(PageSchema, default_in="query"),
403 + openapi.schema2parameters(PageSchema, location="query"),
490404 "responses": {200: {"schema": PetSchema, "description": "A pet"}},
491405 },
492406 "post": {
499413 "type": "string",
500414 }
501415 ]
502 + openapi.schema2parameters(CategorySchema, default_in="body")
416 + openapi.schema2parameters(CategorySchema, location="body")
503417 ),
504418 "responses": {201: {"schema": PetSchema, "description": "A pet"}},
505419 },
534448 "required": True,
535449 "schema": {"type": "string"},
536450 },
537 openapi.field2parameter(
451 openapi._field2parameter(
538452 field=fields.List(
539 fields.Str(),
540 validate=validate.OneOf(["freddie", "roger"]),
541 location="querystring",
453 fields.Str(), validate=validate.OneOf(["freddie", "roger"]),
542454 ),
543 default_in=None,
455 location="query",
544456 name="body",
545457 ),
546458 ]
547 + openapi.schema2parameters(PageSchema, default_in="query"),
459 + openapi.schema2parameters(PageSchema, location="query"),
548460 "responses": {
549461 200: {
550462 "description": "success",
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}-marshmallow3
54 py38-marshmallowdev
65 docs
76
87 [testenv]
98 extras = tests
109 deps =
11 marshmallow2: marshmallow>=2.0.0,<3.0.0
1210 marshmallow3: marshmallow>=3.0.0,<4.0.0
1311 marshmallowdev: https://github.com/marshmallow-code/marshmallow/archive/dev.tar.gz
1412 commands = pytest {posargs}
2725 [testenv:watch-docs]
2826 deps = sphinx-autobuild
2927 extras = docs
30 commands = sphinx-autobuild --open-browser docs/ docs/_build {posargs} -z src/apispec -s 2
28 commands = sphinx-autobuild --open-browser docs/ docs/_build {posargs} --watch src/apispec --delay 2
3129
3230 [testenv:watch-readme]
3331 deps = restview