Import upstream version 4.0.0
Kali Janitor
3 years ago
2 | 2 | rev: v2.4.1 |
3 | 3 | hooks: |
4 | 4 | - id: pyupgrade |
5 | args: [--py3-plus] | |
5 | args: [--py36-plus] | |
6 | 6 | - repo: https://github.com/python/black |
7 | 7 | rev: 19.10b0 |
8 | 8 | hooks: |
62 | 62 | - Ashutosh Chaudhary `@codeasashu <https://github.com/codeasashu>`_ |
63 | 63 | - Fedor Fominykh `@fedorfo <https://github.com/fedorfo>`_ |
64 | 64 | - 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>`_ |
0 | 0 | Changelog |
1 | 1 | --------- |
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. | |
2 | 47 | |
3 | 48 | 3.3.1 (2020-06-06) |
4 | 49 | ****************** |
8 | 53 | - Fix ``MarshmallowPlugin`` crash when ``resolve_schema_dict`` is passed a |
9 | 54 | schema as string and ``schema_name_resolver`` returns ``None`` |
10 | 55 | (:issue:`566`). Thanks :user:`black3r` for reporting and thanks |
11 | :user:`Bangterm` for the PR. | |
56 | :user:`Bangertm` for the PR. | |
12 | 57 | |
13 | 58 | 3.3.0 (2020-02-14) |
14 | 59 | ****************** |
13 | 13 | :target: https://apispec.readthedocs.io/ |
14 | 14 | :alt: Documentation |
15 | 15 | |
16 | .. image:: https://badgen.net/badge/marshmallow/2,3?list=1 | |
16 | .. image:: https://badgen.net/badge/marshmallow/3?list=1 | |
17 | 17 | :target: https://marshmallow.readthedocs.io/en/latest/upgrading.html |
18 | :alt: marshmallow 2/3 compatible | |
18 | :alt: marshmallow 3 only | |
19 | 19 | |
20 | 20 | .. image:: https://badgen.net/badge/OAS/2,3?list=1&color=cyan |
21 | 21 | :target: https://github.com/OAI/OpenAPI-Specification |
66 | 66 | name = fields.Str() |
67 | 67 | |
68 | 68 | |
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 | ||
69 | 74 | # Optional Flask support |
70 | 75 | app = Flask(__name__) |
71 | 76 | |
76 | 81 | --- |
77 | 82 | get: |
78 | 83 | description: Get a random pet |
84 | security: | |
85 | - ApiKeyAuth: [] | |
79 | 86 | responses: |
80 | 87 | 200: |
81 | 88 | content: |
104 | 111 | # "/random": { |
105 | 112 | # "get": { |
106 | 113 | # "description": "Get a random pet", |
114 | # "security": [ | |
115 | # { | |
116 | # "ApiKeyAuth": [] | |
117 | # } | |
118 | # ], | |
107 | 119 | # "responses": { |
108 | 120 | # "200": { |
109 | 121 | # "content": { |
157 | 169 | # } |
158 | 170 | # } |
159 | 171 | # } |
172 | # "securitySchemes": { | |
173 | # "ApiKeyAuth": { | |
174 | # "type": "apiKey", | |
175 | # "in": "header", | |
176 | # "name": "X-API-Key" | |
177 | # } | |
178 | # } | |
160 | 179 | # } |
161 | 180 | # } |
162 | 181 | # } |
179 | 198 | # type: array |
180 | 199 | # name: {type: string} |
181 | 200 | # type: object |
201 | # securitySchemes: | |
202 | # ApiKeyAuth: | |
203 | # in: header | |
204 | # name: X-API-KEY | |
205 | # type: apiKey | |
182 | 206 | # info: {title: Swagger Petstore, version: 1.0.0} |
183 | 207 | # openapi: 3.0.2 |
184 | 208 | # paths: |
190 | 214 | # content: |
191 | 215 | # application/json: |
192 | 216 | # schema: {$ref: '#/components/schemas/Pet'} |
217 | # security: | |
218 | # - ApiKeyAuth: [] | |
193 | 219 | # tags: [] |
194 | 220 | |
195 | 221 |
26 | 26 | toxenvs: |
27 | 27 | - lint |
28 | 28 | |
29 | - py35-marshmallow2 | |
30 | - py35-marshmallow3 | |
31 | ||
32 | 29 | - py36-marshmallow3 |
33 | ||
34 | 30 | - py37-marshmallow3 |
35 | ||
36 | - py38-marshmallow2 | |
37 | 31 | - py38-marshmallow3 |
38 | 32 | |
39 | 33 | - py38-marshmallowdev |
25 | 25 | source_suffix = ".rst" |
26 | 26 | master_doc = "index" |
27 | 27 | project = "apispec" |
28 | copyright = "Steven Loria {:%Y}".format(dt.datetime.utcnow()) | |
28 | copyright = f"Steven Loria {dt.datetime.utcnow():%Y}" | |
29 | 29 | |
30 | 30 | version = release = apispec.__version__ |
31 | 31 |
46 | 46 | name = fields.Str() |
47 | 47 | |
48 | 48 | |
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 | ||
49 | 54 | # Optional Flask support |
50 | 55 | app = Flask(__name__) |
51 | 56 | |
56 | 61 | --- |
57 | 62 | get: |
58 | 63 | description: Get a random pet |
64 | security: | |
65 | - ApiKeyAuth: [] | |
59 | 66 | responses: |
60 | 67 | 200: |
61 | 68 | description: Return a pet |
120 | 127 | # "type": "string" |
121 | 128 | # } |
122 | 129 | # } |
123 | # } | |
130 | # }, | |
131 | # } | |
132 | # }, | |
133 | # "securitySchemes": { | |
134 | # "ApiKeyAuth": { | |
135 | # "type": "apiKey", | |
136 | # "in": "header", | |
137 | # "name": "X-API-Key" | |
124 | 138 | # } |
125 | 139 | # }, |
126 | 140 | # "paths": { |
127 | 141 | # "/random": { |
128 | 142 | # "get": { |
129 | 143 | # "description": "Get a random pet", |
144 | # "security": [ | |
145 | # { | |
146 | # "ApiKeyAuth": [] | |
147 | # } | |
148 | # ], | |
130 | 149 | # "responses": { |
131 | 150 | # "200": { |
132 | 151 | # "description": "Return a pet", |
170 | 189 | # name: |
171 | 190 | # type: string |
172 | 191 | # type: object |
192 | # securitySchemes: | |
193 | # ApiKeyAuth: | |
194 | # in: header | |
195 | # name: X-API-KEY | |
196 | # type: apiKey | |
173 | 197 | # paths: |
174 | 198 | # /random: |
175 | 199 | # get: |
181 | 205 | # schema: |
182 | 206 | # $ref: '#/components/schemas/Pet' |
183 | 207 | # description: Return a pet |
208 | # security: | |
209 | # - ApiKeyAuth: [] | |
184 | 210 | |
185 | 211 | User Guide |
186 | 212 | ========== |
0 | 0 | Install |
1 | 1 | ======= |
2 | 2 | |
3 | **apispec** requires Python >= 3.5. | |
3 | **apispec** requires Python >= 3.6. | |
4 | 4 | |
5 | 5 | From the PyPI |
6 | 6 | ------------- |
24 | 24 | .. code-block:: python |
25 | 25 | |
26 | 26 | from apispec import Path, BasePlugin |
27 | from apispec.utils import load_operations_from_docstring | |
27 | from apispec.yaml_utils import load_operations_from_docstring | |
28 | 28 | |
29 | 29 | |
30 | 30 | class MyPlugin(BasePlugin): |
31 | def path_helper(self, path, func, **kwargs): | |
31 | def path_helper(self, path, operations, func, **kwargs): | |
32 | 32 | """Path helper that parses docstrings for operations. Adds a |
33 | 33 | ``func`` parameter to `apispec.APISpec.path`. |
34 | 34 | """ |
35 | operations = load_operations_from_docstring(func.__doc__) | |
36 | return Path(path=path, operations=operations) | |
35 | operations.update(load_operations_from_docstring(func.__doc__)) | |
37 | 36 | |
38 | 37 | |
39 | 38 | All plugin helpers must accept extra `**kwargs`, allowing custom plugins to define new arguments if required. |
3 | 3 | EXTRAS_REQUIRE = { |
4 | 4 | "yaml": ["PyYAML>=3.10"], |
5 | 5 | "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"], | |
7 | 7 | "docs": [ |
8 | "marshmallow>=2.19.2", | |
8 | "marshmallow>=3.0.0", | |
9 | 9 | "pyyaml==5.3.1", |
10 | "sphinx==3.0.4", | |
10 | "sphinx==3.2.1", | |
11 | 11 | "sphinx-issues==1.2.0", |
12 | "sphinx-rtd-theme==0.4.3", | |
12 | "sphinx-rtd-theme==0.5.0", | |
13 | 13 | ], |
14 | 14 | } |
15 | 15 | EXTRAS_REQUIRE["tests"] = ( |
16 | 16 | EXTRAS_REQUIRE["yaml"] |
17 | 17 | + EXTRAS_REQUIRE["validation"] |
18 | + ["marshmallow>=2.19.2", "pytest", "mock"] | |
18 | + ["marshmallow>=3.0.0", "pytest", "mock"] | |
19 | 19 | ) |
20 | 20 | EXTRAS_REQUIRE["dev"] = EXTRAS_REQUIRE["tests"] + EXTRAS_REQUIRE["lint"] + ["tox"] |
21 | 21 | |
59 | 59 | license="MIT", |
60 | 60 | zip_safe=False, |
61 | 61 | keywords="apispec swagger openapi specification oas documentation spec rest api", |
62 | python_requires=">=3.5", | |
62 | python_requires=">=3.6", | |
63 | 63 | classifiers=[ |
64 | 64 | "License :: OSI Approved :: MIT License", |
65 | 65 | "Programming Language :: Python :: 3", |
66 | "Programming Language :: Python :: 3.5", | |
67 | 66 | "Programming Language :: Python :: 3.6", |
68 | 67 | "Programming Language :: Python :: 3.7", |
69 | 68 | "Programming Language :: Python :: 3.8", |
2 | 2 | from .core import APISpec |
3 | 3 | from .plugin import BasePlugin |
4 | 4 | |
5 | __version__ = "3.3.1" | |
5 | __version__ = "4.0.0" | |
6 | 6 | __all__ = ["APISpec", "BasePlugin"] |
71 | 71 | """ |
72 | 72 | if name in self._schemas: |
73 | 73 | raise DuplicateComponentNameError( |
74 | 'Another schema with name "{}" is already registered.'.format(name) | |
74 | f'Another schema with name "{name}" is already registered.' | |
75 | 75 | ) |
76 | 76 | component = component or {} |
77 | 77 | ret = component.copy() |
150 | 150 | """ |
151 | 151 | if name in self._examples: |
152 | 152 | raise DuplicateComponentNameError( |
153 | 'Another example with name "{}" is already registered.'.format(name) | |
153 | f'Another example with name "{name}" is already registered.' | |
154 | 154 | ) |
155 | 155 | self._examples[name] = component |
156 | 156 | return self |
242 | 242 | summary=None, |
243 | 243 | description=None, |
244 | 244 | parameters=None, |
245 | **kwargs | |
245 | **kwargs, | |
246 | 246 | ): |
247 | 247 | """Add a new path object to the spec. |
248 | 248 | |
299 | 299 | Otherwise, it is assumed to be a reference name as string and the corresponding $ref |
300 | 300 | string is returned. |
301 | 301 | |
302 | :param str obj_type: "parameter" or "response" | |
303 | :param dict|str obj: parameter or response in dict form or as ref_id string | |
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 | |
304 | 304 | """ |
305 | 305 | if isinstance(obj, dict): |
306 | 306 | return obj |
307 | 307 | 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"]) | |
308 | 321 | |
309 | 322 | def clean_parameters(self, parameters): |
310 | 323 | """Ensure that all parameters with "in" equal to "path" are also required |
322 | 335 | missing_attrs = [attr for attr in ("name", "in") if attr not in parameter] |
323 | 336 | if missing_attrs: |
324 | 337 | raise InvalidParameterError( |
325 | "Missing keys {} for parameter".format(missing_attrs) | |
338 | f"Missing keys {missing_attrs} for parameter" | |
326 | 339 | ) |
327 | 340 | |
328 | 341 | # OpenAPI Spec 3 and 2 don't allow for duplicated parameters |
364 | 377 | for operation in (operations or {}).values(): |
365 | 378 | if "parameters" in operation: |
366 | 379 | operation["parameters"] = self.clean_parameters(operation["parameters"]) |
380 | # OAS 3 | |
381 | if "requestBody" in operation: | |
382 | self._resolve_schema(operation["requestBody"]) | |
367 | 383 | if "responses" in operation: |
368 | 384 | responses = OrderedDict() |
369 | 385 | for code, response in operation["responses"].items(): |
372 | 388 | except (TypeError, ValueError): |
373 | 389 | if self.openapi_version.major < 3 and code != "default": |
374 | 390 | warnings.warn("Non-integer code not allowed in OpenAPI < 3") |
375 | ||
391 | self._resolve_schema(response) | |
376 | 392 | responses[str(code)] = self.get_ref("response", response) |
377 | 393 | operation["responses"] = responses |
59 | 59 | # 'format': 'date-time', |
60 | 60 | # 'readOnly': True, |
61 | 61 | # 'type': 'string'}, |
62 | # 'id': {'format': 'int32', | |
63 | # 'readOnly': True, | |
62 | # 'id': {'readOnly': True, | |
64 | 63 | # 'type': 'integer'}, |
65 | 64 | # 'name': {'description': "The user's name", |
66 | 65 | # 'type': 'string'}}, |
139 | 138 | class MyCustomField(Integer): |
140 | 139 | # ... |
141 | 140 | |
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) | |
143 | 142 | class MyCustomFieldThatsKindaLikeAnInteger(Integer): |
144 | 143 | # ... |
145 | 144 | """ |
19 | 19 | return schema() |
20 | 20 | if isinstance(schema, marshmallow.Schema): |
21 | 21 | return schema |
22 | try: | |
23 | return marshmallow.class_registry.get_class(schema)() | |
24 | except marshmallow.exceptions.RegistryError: | |
25 | raise ValueError( | |
26 | "{!r} is not a marshmallow.Schema subclass or instance and has not" | |
27 | " been registered in the marshmallow class registry.".format(schema) | |
28 | ) | |
22 | return marshmallow.class_registry.get_class(schema)() | |
29 | 23 | |
30 | 24 | |
31 | 25 | def resolve_schema_cls(schema): |
38 | 32 | return schema |
39 | 33 | if isinstance(schema, marshmallow.Schema): |
40 | 34 | 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) | |
48 | 36 | |
49 | 37 | |
50 | 38 | def get_fields(schema, *, exclude_dump_only=False): |
60 | 48 | fields = copy.deepcopy(schema._declared_fields) |
61 | 49 | else: |
62 | 50 | 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`." | |
64 | 52 | ) |
65 | 53 | Meta = getattr(schema, "Meta", None) |
66 | 54 | warn_if_fields_defined_in_meta(fields, Meta) |
90 | 78 | |
91 | 79 | :param dict fields: A dictionary of fields name field object pairs |
92 | 80 | :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 | |
94 | 82 | """ |
95 | 83 | exclude = list(getattr(Meta, "exclude", [])) |
96 | 84 | if exclude_dump_only: |
97 | 85 | exclude.extend(getattr(Meta, "dump_only", [])) |
98 | 86 | |
99 | 87 | 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) | |
101 | 91 | ) |
102 | 92 | |
103 | 93 | return filtered_fields |
16 | 16 | |
17 | 17 | RegexType = type(re.compile("")) |
18 | 18 | |
19 | MARSHMALLOW_VERSION_INFO = tuple( | |
20 | [int(part) for part in marshmallow.__version__.split(".") if part.isdigit()] | |
21 | ) | |
22 | ||
23 | ||
24 | 19 | # marshmallow field => (JSON Schema type, format) |
25 | 20 | DEFAULT_FIELD_MAPPING = { |
26 | marshmallow.fields.Integer: ("integer", "int32"), | |
21 | marshmallow.fields.Integer: ("integer", None), | |
27 | 22 | marshmallow.fields.Number: ("number", None), |
28 | marshmallow.fields.Float: ("number", "float"), | |
23 | marshmallow.fields.Float: ("number", None), | |
29 | 24 | marshmallow.fields.Decimal: ("number", None), |
30 | 25 | marshmallow.fields.String: ("string", None), |
31 | 26 | marshmallow.fields.Boolean: ("boolean", None), |
211 | 206 | else: |
212 | 207 | default = field.missing |
213 | 208 | 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) | |
216 | 210 | ret["default"] = default |
217 | 211 | return ret |
218 | 212 | |
396 | 390 | metadata = { |
397 | 391 | key.replace("_", "-") if key.startswith("x_") else key: value |
398 | 392 | for key, value in field.metadata.items() |
393 | if isinstance(key, str) | |
399 | 394 | } |
400 | 395 | |
401 | 396 | # Avoid validation error with "Additional properties not allowed" |
435 | 430 | """ |
436 | 431 | ret = {} |
437 | 432 | 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) | |
442 | 434 | return ret |
443 | 435 | |
444 | 436 | def dict2properties(self, field, **kwargs): |
452 | 444 | """ |
453 | 445 | ret = {} |
454 | 446 | 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 |
18 | 18 | make_schema_key, |
19 | 19 | resolve_schema_instance, |
20 | 20 | get_unique_schema_name, |
21 | ) | |
22 | ||
23 | ||
24 | MARSHMALLOW_VERSION_INFO = tuple( | |
25 | [int(part) for part in marshmallow.__version__.split(".") if part.isdigit()] | |
26 | 21 | ) |
27 | 22 | |
28 | 23 | |
59 | 54 | # Schema references |
60 | 55 | self.refs = {} |
61 | 56 | |
62 | @staticmethod | |
63 | def _observed_name(field, name): | |
64 | """Adjust field name to reflect `dump_to` and `load_from` attributes. | |
65 | ||
66 | :param Field field: A marshmallow field. | |
67 | :param str name: Field name | |
68 | :rtype: str | |
69 | """ | |
70 | if MARSHMALLOW_VERSION_INFO[0] < 3: | |
71 | # use getattr in case we're running against older versions of marshmallow. | |
72 | dump_to = getattr(field, "dump_to", None) | |
73 | load_from = getattr(field, "load_from", None) | |
74 | return dump_to or load_from or name | |
75 | return field.data_key or name | |
76 | ||
77 | 57 | def resolve_nested_schema(self, schema): |
78 | 58 | """Return the OpenAPI representation of a marshmallow Schema. |
79 | 59 | |
86 | 66 | |
87 | 67 | :param schema: schema to add to the spec |
88 | 68 | """ |
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) | |
90 | 75 | schema_key = make_schema_key(schema_instance) |
91 | 76 | if schema_key not in self.refs: |
92 | 77 | name = self.schema_name_resolver(schema) |
109 | 94 | return self.get_ref_dict(schema_instance) |
110 | 95 | |
111 | 96 | 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 | |
119 | 98 | ): |
120 | 99 | """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 | |
122 | 101 | of a single parameter; else return an array of a parameter for each included field in |
123 | 102 | the :class:`Schema <marshmallow.Schema>`. |
124 | 103 | |
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 | ||
125 | 107 | https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#parameterObject |
126 | 108 | """ |
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": | |
131 | 112 | param = { |
132 | "in": openapi_default_in, | |
113 | "in": location, | |
133 | 114 | "required": required, |
134 | 115 | "name": name, |
135 | "schema": prop, | |
116 | "schema": self.resolve_nested_schema(schema), | |
136 | 117 | } |
137 | ||
138 | 118 | if description: |
139 | 119 | param["description"] = description |
140 | ||
141 | 120 | return [param] |
142 | 121 | |
143 | 122 | assert not getattr( |
146 | 125 | |
147 | 126 | fields = get_fields(schema, exclude_dump_only=True) |
148 | 127 | |
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, | |
174 | 131 | ) |
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): | |
193 | 136 | """Return an OpenAPI parameter as a `dict`, given a marshmallow |
194 | 137 | :class:`Field <marshmallow.Field>`. |
195 | 138 | |
196 | 139 | https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#parameterObject |
197 | 140 | """ |
198 | location = field.metadata.get("location", None) | |
141 | ret = {"in": location, "name": name, "required": field.required} | |
142 | ||
199 | 143 | 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) | |
235 | 150 | 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 | |
248 | 157 | return ret |
249 | 158 | |
250 | 159 | def schema2jsonschema(self, schema): |
285 | 194 | jsonschema = {"type": "object", "properties": OrderedDict() if ordered else {}} |
286 | 195 | |
287 | 196 | 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 | |
291 | 200 | |
292 | 201 | if field_obj.required: |
293 | 202 | if not partial or ( |
37 | 37 | |
38 | 38 | #Output |
39 | 39 | [ |
40 | {"in": "query", "name": "id", "required": False, "schema": {"type": "integer", "format": "int32"}}, | |
40 | {"in": "query", "name": "id", "required": False, "schema": {"type": "integer"}}, | |
41 | 41 | {"in": "query", "name": "name", "required": False, "schema": {"type": "string"}} |
42 | 42 | ] |
43 | 43 | |
75 | 75 | ): |
76 | 76 | schema_instance = resolve_schema_instance(parameter.pop("schema")) |
77 | 77 | resolved += self.converter.schema2parameters( |
78 | schema_instance, default_in=parameter.pop("in"), **parameter | |
78 | schema_instance, location=parameter.pop("in"), **parameter | |
79 | 79 | ) |
80 | 80 | else: |
81 | 81 | self.resolve_schema(parameter) |
161 | 161 | |
162 | 162 | def resolve_schema_dict(self, schema): |
163 | 163 | """Resolve a marshmallow Schema class, object, or a string that resolves |
164 | to a Schema class or an OpenAPI Schema Object containing one of the above | |
165 | to an OpenAPI Schema Object or Reference Object. | |
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. | |
166 | 166 | |
167 | 167 | If the input is a marshmallow Schema class, object or a string that resolves |
168 | 168 | to a Schema class the Schema will be translated to an OpenAPI Schema Object |
102 | 102 | < self.MAX_EXCLUSIVE_VERSION |
103 | 103 | ): |
104 | 104 | raise exceptions.APISpecError( |
105 | "Not a valid OpenAPI version number: {}".format(openapi_version) | |
105 | f"Not a valid OpenAPI version number: {openapi_version}" | |
106 | 106 | ) |
107 | 107 | super().__init__(openapi_version) |
108 | 108 |
0 | 0 | from marshmallow import Schema, fields |
1 | ||
2 | from apispec.ext.marshmallow.openapi import MARSHMALLOW_VERSION_INFO | |
3 | 1 | |
4 | 2 | |
5 | 3 | class PetSchema(Schema): |
39 | 37 | |
40 | 38 | class SelfReferencingSchema(Schema): |
41 | 39 | 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)) | |
48 | 42 | |
49 | 43 | |
50 | 44 | class OrderedSchema(Schema): |
59 | 59 | version="1.0.0", |
60 | 60 | openapi_version=openapi_version, |
61 | 61 | info={"description": description}, |
62 | **security_kwargs | |
62 | **security_kwargs, | |
63 | 63 | ) |
64 | 64 | |
65 | 65 | |
306 | 306 | } |
307 | 307 | ], |
308 | 308 | "responses": { |
309 | "200": {"schema": "Pet", "description": "successful operation"}, | |
309 | "200": {"description": "successful operation"}, | |
310 | 310 | "400": {"description": "Invalid ID supplied"}, |
311 | 311 | "404": {"description": "Pet not found"}, |
312 | 312 | }, |
617 | 617 | with pytest.raises(APISpecError, match=message): |
618 | 618 | spec.path("/pet/{petId}", operations={"dummy": {}}) |
619 | 619 | |
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 | ||
620 | 649 | |
621 | 650 | class TestPlugins: |
622 | 651 | @staticmethod |
741 | 770 | self.output = output |
742 | 771 | |
743 | 772 | 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") | |
745 | 774 | |
746 | 775 | 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") | |
748 | 777 | |
749 | 778 | def test_plugins_order(self): |
750 | 779 | """Test plugins execution order in APISpec.path |
6 | 6 | |
7 | 7 | from apispec import APISpec |
8 | 8 | from apispec.ext.marshmallow import MarshmallowPlugin |
9 | from apispec.ext.marshmallow.openapi import MARSHMALLOW_VERSION_INFO | |
10 | 9 | from apispec.ext.marshmallow import common |
11 | 10 | from apispec.exceptions import APISpecError |
12 | 11 | from .schemas import ( |
439 | 438 | p = get_paths(spec_fixture.spec)["/pet"] |
440 | 439 | get = p["get"] |
441 | 440 | assert get["parameters"] == spec_fixture.openapi.schema2parameters( |
442 | PetSchema(), default_in="query" | |
441 | PetSchema(), location="query" | |
443 | 442 | ) |
444 | 443 | post = p["post"] |
445 | 444 | assert post["parameters"] == spec_fixture.openapi.schema2parameters( |
446 | 445 | PetSchema, |
447 | default_in="body", | |
446 | location="body", | |
448 | 447 | required=True, |
449 | 448 | name="pet", |
450 | 449 | description="a pet schema", |
468 | 467 | p = get_paths(spec_fixture.spec)["/pet"] |
469 | 468 | get = p["get"] |
470 | 469 | assert get["parameters"] == spec_fixture.openapi.schema2parameters( |
471 | PetSchema(), default_in="query" | |
470 | PetSchema(), location="query" | |
472 | 471 | ) |
473 | 472 | for parameter in get["parameters"]: |
474 | 473 | description = parameter.get("description", False) |
818 | 817 | |
819 | 818 | def test_schema_global_state_untouched_2parameters(self, spec_fixture): |
820 | 819 | assert get_nested_schema(RunSchema, "sample") is None |
821 | data = spec_fixture.openapi.schema2parameters(RunSchema) | |
820 | data = spec_fixture.openapi.schema2parameters(RunSchema, location="json") | |
822 | 821 | json.dumps(data) |
823 | 822 | 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") | |
824 | 835 | |
825 | 836 | |
826 | 837 | class TestCircularReference: |
882 | 893 | assert "default" not in props["numbers"] |
883 | 894 | |
884 | 895 | |
885 | @pytest.mark.skipif( | |
886 | MARSHMALLOW_VERSION_INFO[0] < 3, reason="Values ignored in marshmallow 2" | |
887 | ) | |
888 | 896 | class TestDictValues: |
889 | 897 | def test_dict_values_resolve_to_additional_properties(self, spec): |
890 | 898 | class SchemaWithDict(Schema): |
2 | 2 | |
3 | 3 | import pytest |
4 | 4 | from marshmallow import fields, validate |
5 | ||
6 | from apispec.ext.marshmallow.openapi import MARSHMALLOW_VERSION_INFO | |
7 | 5 | |
8 | 6 | from .schemas import CategorySchema, CustomList, CustomStringField, CustomIntegerField |
9 | 7 | from .utils import build_ref |
60 | 58 | @pytest.mark.parametrize( |
61 | 59 | ("FieldClass", "expected_format"), |
62 | 60 | [ |
63 | (fields.Integer, "int32"), | |
64 | (fields.Float, "float"), | |
65 | 61 | (fields.UUID, "uuid"), |
66 | 62 | (fields.DateTime, "date-time"), |
67 | 63 | (fields.Date, "date"), |
94 | 90 | |
95 | 91 | |
96 | 92 | 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)) | |
101 | 94 | res = spec_fixture.openapi.field2property(field) |
102 | 95 | assert res["default"] == dt.date(2014, 7, 18).isoformat() |
103 | 96 | |
292 | 285 | assert properties["x-customString"] == ( |
293 | 286 | spec_fixture.openapi.openapi_version == "2.0" |
294 | 287 | ) |
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"} |
2 | 2 | from marshmallow import fields, Schema, validate |
3 | 3 | |
4 | 4 | from apispec.ext.marshmallow import MarshmallowPlugin |
5 | from apispec.ext.marshmallow.openapi import MARSHMALLOW_VERSION_INFO | |
6 | 5 | from apispec import exceptions, utils, APISpec |
7 | 6 | |
8 | 7 | from .schemas import CustomList, CustomStringField |
11 | 10 | |
12 | 11 | class TestMarshmallowFieldToOpenAPI: |
13 | 12 | 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") | |
16 | 17 | if openapi.openapi_version.major < 3: |
17 | 18 | assert res[0]["default"] == "bar" |
18 | 19 | else: |
19 | 20 | assert res[0]["schema"]["default"] == "bar" |
20 | ||
21 | def test_fields_with_location(self, openapi): | |
22 | field_dict = {"field": fields.Str(location="querystring")} | |
23 | res = openapi.fields2parameters(field_dict, default_in="headers") | |
24 | assert res[0]["in"] == "query" | |
25 | ||
26 | # json/body is invalid for OpenAPI 3 | |
27 | @pytest.mark.parametrize("openapi", ("2.0",), indirect=True) | |
28 | def test_fields_with_multiple_json_locations(self, openapi): | |
29 | field_dict = { | |
30 | "field1": fields.Str(location="json", required=True), | |
31 | "field2": fields.Str(location="json", required=True), | |
32 | "field3": fields.Str(location="json"), | |
33 | } | |
34 | res = openapi.fields2parameters(field_dict, default_in=None) | |
35 | assert len(res) == 1 | |
36 | assert res[0]["in"] == "body" | |
37 | assert res[0]["required"] is False | |
38 | assert "field1" in res[0]["schema"]["properties"] | |
39 | assert "field2" in res[0]["schema"]["properties"] | |
40 | assert "field3" in res[0]["schema"]["properties"] | |
41 | assert "required" in res[0]["schema"] | |
42 | assert len(res[0]["schema"]["required"]) == 2 | |
43 | assert "field1" in res[0]["schema"]["required"] | |
44 | assert "field2" in res[0]["schema"]["required"] | |
45 | ||
46 | def test_fields2parameters_does_not_modify_metadata(self, openapi): | |
47 | field_dict = {"field": fields.Str(location="querystring")} | |
48 | res = openapi.fields2parameters(field_dict, default_in="headers") | |
49 | assert res[0]["in"] == "query" | |
50 | ||
51 | res = openapi.fields2parameters(field_dict, default_in="headers") | |
52 | assert res[0]["in"] == "query" | |
53 | ||
54 | def test_fields_location_mapping(self, openapi): | |
55 | field_dict = {"field": fields.Str(location="cookies")} | |
56 | res = openapi.fields2parameters(field_dict, default_in="headers") | |
57 | assert res[0]["in"] == "cookie" | |
58 | ||
59 | def test_fields_default_location_mapping(self, openapi): | |
60 | field_dict = {"field": fields.Str()} | |
61 | res = openapi.fields2parameters(field_dict, default_in="headers") | |
62 | assert res[0]["in"] == "header" | |
63 | 21 | |
64 | 22 | # json/body is invalid for OpenAPI 3 |
65 | 23 | @pytest.mark.parametrize("openapi", ("2.0",), indirect=True) |
68 | 26 | id = fields.Int() |
69 | 27 | |
70 | 28 | schema = ExampleSchema(many=True) |
71 | res = openapi.schema2parameters(schema=schema, default_in="json") | |
29 | res = openapi.schema2parameters(schema=schema, location="json") | |
72 | 30 | assert res[0]["in"] == "body" |
73 | 31 | |
74 | 32 | def test_fields_with_dump_only(self, openapi): |
75 | 33 | class UserSchema(Schema): |
76 | 34 | name = fields.Str(dump_only=True) |
77 | 35 | |
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") | |
81 | 37 | assert len(res) == 0 |
82 | 38 | |
83 | 39 | class UserSchema(Schema): |
86 | 42 | class Meta: |
87 | 43 | dump_only = ("name",) |
88 | 44 | |
89 | res = openapi.schema2parameters(schema=UserSchema, default_in="query") | |
45 | res = openapi.schema2parameters(schema=UserSchema(), location="query") | |
90 | 46 | assert len(res) == 0 |
91 | 47 | |
92 | 48 | |
93 | 49 | class TestMarshmallowSchemaToModelDefinition: |
94 | def test_invalid_schema(self, openapi): | |
95 | with pytest.raises(ValueError): | |
96 | openapi.schema2jsonschema(None) | |
97 | ||
98 | 50 | def test_schema2jsonschema_with_explicit_fields(self, openapi): |
99 | 51 | class UserSchema(Schema): |
100 | 52 | _id = fields.Int() |
113 | 65 | assert props["email"]["format"] == "email" |
114 | 66 | assert props["email"]["description"] == "email address of the user" |
115 | 67 | |
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): | |
146 | 69 | class ExampleSchema(Schema): |
147 | 70 | _id = fields.Int(data_key="id") |
148 | 71 | _global = fields.Int(data_key="global") |
226 | 149 | assert "email" not in props |
227 | 150 | |
228 | 151 | 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)} | |
236 | 153 | res = openapi.fields2jsonschema(fields_dict) |
237 | 154 | assert res["required"] == ["id"] |
238 | 155 | |
260 | 177 | class TestMarshmallowSchemaToParameters: |
261 | 178 | @pytest.mark.parametrize("ListClass", [fields.List, CustomList]) |
262 | 179 | 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") | |
265 | 182 | assert res["in"] == "query" |
266 | 183 | if openapi.openapi_version.major < 3: |
267 | 184 | assert res["type"] == "array" |
274 | 191 | assert res["explode"] is True |
275 | 192 | |
276 | 193 | 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") | |
279 | 196 | assert res["required"] is True |
280 | ||
281 | def test_invalid_schema(self, openapi): | |
282 | with pytest.raises(ValueError): | |
283 | openapi.schema2parameters(None) | |
284 | 197 | |
285 | 198 | # json/body is invalid for OpenAPI 3 |
286 | 199 | @pytest.mark.parametrize("openapi", ("2.0",), indirect=True) |
289 | 202 | name = fields.Str() |
290 | 203 | email = fields.Email() |
291 | 204 | |
292 | res = openapi.schema2parameters(UserSchema, default_in="body") | |
205 | res = openapi.schema2parameters(UserSchema, location="body") | |
293 | 206 | assert len(res) == 1 |
294 | 207 | param = res[0] |
295 | 208 | assert param["in"] == "body" |
302 | 215 | name = fields.Str() |
303 | 216 | email = fields.Email(dump_only=True) |
304 | 217 | |
305 | res_nodump = openapi.schema2parameters(UserSchema, default_in="body") | |
218 | res_nodump = openapi.schema2parameters(UserSchema, location="body") | |
306 | 219 | assert len(res_nodump) == 1 |
307 | 220 | param = res_nodump[0] |
308 | 221 | assert param["in"] == "body" |
315 | 228 | name = fields.Str() |
316 | 229 | email = fields.Email() |
317 | 230 | |
318 | res = openapi.schema2parameters(UserSchema(many=True), default_in="body") | |
231 | res = openapi.schema2parameters(UserSchema(many=True), location="body") | |
319 | 232 | assert len(res) == 1 |
320 | 233 | param = res[0] |
321 | 234 | assert param["in"] == "body" |
327 | 240 | name = fields.Str() |
328 | 241 | email = fields.Email() |
329 | 242 | |
330 | res = openapi.schema2parameters(UserSchema, default_in="query") | |
243 | res = openapi.schema2parameters(UserSchema, location="query") | |
331 | 244 | assert len(res) == 2 |
332 | 245 | res.sort(key=lambda param: param["name"]) |
333 | 246 | assert res[0]["name"] == "email" |
340 | 253 | name = fields.Str() |
341 | 254 | email = fields.Email() |
342 | 255 | |
343 | res = openapi.schema2parameters(UserSchema(), default_in="query") | |
256 | res = openapi.schema2parameters(UserSchema(), location="query") | |
344 | 257 | assert len(res) == 2 |
345 | 258 | res.sort(key=lambda param: param["name"]) |
346 | 259 | assert res[0]["name"] == "email" |
354 | 267 | email = fields.Email() |
355 | 268 | |
356 | 269 | with pytest.raises(AssertionError): |
357 | openapi.schema2parameters(UserSchema(many=True), default_in="query") | |
270 | openapi.schema2parameters(UserSchema(many=True), location="query") | |
358 | 271 | |
359 | 272 | 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") | |
362 | 278 | assert len(res) == 2 |
363 | 279 | res.sort(key=lambda param: param["name"]) |
364 | 280 | assert res[0]["name"] == "email" |
476 | 392 | "required": True, |
477 | 393 | "type": "string", |
478 | 394 | }, |
479 | openapi.field2parameter( | |
395 | openapi._field2parameter( | |
480 | 396 | field=fields.List( |
481 | fields.Str(), | |
482 | validate=validate.OneOf(["freddie", "roger"]), | |
483 | location="querystring", | |
397 | fields.Str(), validate=validate.OneOf(["freddie", "roger"]), | |
484 | 398 | ), |
485 | default_in=None, | |
399 | location="query", | |
486 | 400 | name="body", |
487 | 401 | ), |
488 | 402 | ] |
489 | + openapi.schema2parameters(PageSchema, default_in="query"), | |
403 | + openapi.schema2parameters(PageSchema, location="query"), | |
490 | 404 | "responses": {200: {"schema": PetSchema, "description": "A pet"}}, |
491 | 405 | }, |
492 | 406 | "post": { |
499 | 413 | "type": "string", |
500 | 414 | } |
501 | 415 | ] |
502 | + openapi.schema2parameters(CategorySchema, default_in="body") | |
416 | + openapi.schema2parameters(CategorySchema, location="body") | |
503 | 417 | ), |
504 | 418 | "responses": {201: {"schema": PetSchema, "description": "A pet"}}, |
505 | 419 | }, |
534 | 448 | "required": True, |
535 | 449 | "schema": {"type": "string"}, |
536 | 450 | }, |
537 | openapi.field2parameter( | |
451 | openapi._field2parameter( | |
538 | 452 | field=fields.List( |
539 | fields.Str(), | |
540 | validate=validate.OneOf(["freddie", "roger"]), | |
541 | location="querystring", | |
453 | fields.Str(), validate=validate.OneOf(["freddie", "roger"]), | |
542 | 454 | ), |
543 | default_in=None, | |
455 | location="query", | |
544 | 456 | name="body", |
545 | 457 | ), |
546 | 458 | ] |
547 | + openapi.schema2parameters(PageSchema, default_in="query"), | |
459 | + openapi.schema2parameters(PageSchema, location="query"), | |
548 | 460 | "responses": { |
549 | 461 | 200: { |
550 | 462 | "description": "success", |
0 | 0 | [tox] |
1 | 1 | envlist= |
2 | 2 | lint |
3 | py{35,36,37,38}-marshmallow2 | |
4 | py{35,36,37,38}-marshmallow3 | |
3 | py{36,37,38}-marshmallow3 | |
5 | 4 | py38-marshmallowdev |
6 | 5 | docs |
7 | 6 | |
8 | 7 | [testenv] |
9 | 8 | extras = tests |
10 | 9 | deps = |
11 | marshmallow2: marshmallow>=2.0.0,<3.0.0 | |
12 | 10 | marshmallow3: marshmallow>=3.0.0,<4.0.0 |
13 | 11 | marshmallowdev: https://github.com/marshmallow-code/marshmallow/archive/dev.tar.gz |
14 | 12 | commands = pytest {posargs} |
27 | 25 | [testenv:watch-docs] |
28 | 26 | deps = sphinx-autobuild |
29 | 27 | 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 | |
31 | 29 | |
32 | 30 | [testenv:watch-readme] |
33 | 31 | deps = restview |