Skip to content

Commit 9b94105

Browse files
ddorianluolingchun
and
luolingchun
authored
Fix missing Field.default when it's value is None in openapi spec (#189)
* Fix missing `Field.default` when it's value is `None` in openapi spec * Faster generation of spec_json * Assignment follows the `unset` rule * Add test case with "null" parameters. * Add `test_deprecated_none` --------- Co-authored-by: luolingchun <[email protected]>
1 parent f6f9627 commit 9b94105

File tree

5 files changed

+178
-79
lines changed

5 files changed

+178
-79
lines changed

flask_openapi3/blueprint.py

+9-4
Original file line numberDiff line numberDiff line change
@@ -158,21 +158,26 @@ def _collect_openapi_info(
158158
)
159159

160160
# Set external docs
161-
operation.externalDocs = external_docs
161+
if external_docs:
162+
operation.externalDocs = external_docs
162163

163164
# Unique string used to identify the operation.
164165
operation.operationId = operation_id or self.operation_id_callback(
165166
name=self.name, path=rule, method=method
166167
)
167168

168169
# Only set `deprecated` if True, otherwise leave it as None
169-
operation.deprecated = deprecated
170+
if deprecated is not None:
171+
operation.deprecated = deprecated
170172

171173
# Add security
172-
operation.security = (security or []) + self.abp_security or None
174+
_security = (security or []) + self.abp_security or None
175+
if _security:
176+
operation.security = _security
173177

174178
# Add servers
175-
operation.servers = servers
179+
if servers:
180+
operation.servers = servers
176181

177182
# Store tags
178183
tags = (tags or []) + self.abp_tags

flask_openapi3/openapi.py

+19-10
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
# -*- coding: utf-8 -*-
22
# @Author : llc
33
# @Time : 2021/4/30 14:25
4-
import json
54
import os
65
import re
76
import sys
@@ -219,12 +218,18 @@ def api_doc(self) -> Dict:
219218
spec = APISpec(
220219
openapi=self.openapi_version,
221220
info=self.info,
222-
servers=self.severs,
223-
paths=self.paths,
224-
externalDocs=self.external_docs
221+
paths=self.paths
225222
)
223+
224+
if self.severs:
225+
spec.servers = self.severs
226+
227+
if self.external_docs:
228+
spec.externalDocs = self.external_docs
229+
226230
# Set tags
227-
spec.tags = self.tags or None
231+
if self.tags:
232+
spec.tags = self.tags
228233

229234
# Add ValidationErrorModel to components schemas
230235
schema = get_model_schema(self.validation_error_model)
@@ -241,7 +246,7 @@ def api_doc(self) -> Dict:
241246
spec.components = self.components
242247

243248
# Convert spec to JSON
244-
self.spec_json = json.loads(spec.model_dump_json(by_alias=True, exclude_none=True, warnings=False))
249+
self.spec_json = spec.model_dump(mode="json", by_alias=True, exclude_unset=True, warnings=False)
245250

246251
# Update with OpenAPI extensions
247252
self.spec_json.update(**self.openapi_extensions)
@@ -385,21 +390,25 @@ def _collect_openapi_info(
385390
openapi_extensions=openapi_extensions
386391
)
387392
# Set external docs
388-
operation.externalDocs = external_docs
393+
if external_docs:
394+
operation.externalDocs = external_docs
389395

390396
# Unique string used to identify the operation.
391397
operation.operationId = operation_id or self.operation_id_callback(
392398
name=func.__name__, path=rule, method=method
393399
)
394400

395401
# Only set `deprecated` if True, otherwise leave it as None
396-
operation.deprecated = deprecated
402+
if deprecated is not None:
403+
operation.deprecated = deprecated
397404

398405
# Add security
399-
operation.security = security
406+
if security:
407+
operation.security = security
400408

401409
# Add servers
402-
operation.servers = servers
410+
if servers:
411+
operation.servers = servers
403412

404413
# Store tags
405414
parse_and_store_tags(tags or [], self.tags, self.tag_names, operation)

flask_openapi3/utils.py

+88-57
Original file line numberDiff line numberDiff line change
@@ -73,19 +73,25 @@ def get_operation(
7373
doc = inspect.getdoc(func) or ""
7474
doc = doc.strip()
7575
lines = doc.split("\n")
76-
doc_summary = lines[0] or None
76+
doc_summary = lines[0]
7777

7878
# Determine the summary and description based on provided arguments or docstring
7979
if summary is None:
80-
doc_description = lines[0] if len(lines) == 0 else "</br>".join(lines[1:]) or None
80+
doc_description = lines[0] if len(lines) == 0 else "</br>".join(lines[1:])
8181
else:
82-
doc_description = "</br>".join(lines) or None
82+
doc_description = "</br>".join(lines)
83+
84+
summary = summary or doc_summary
85+
description = description or doc_description
8386

8487
# Create the operation dictionary with summary and description
85-
operation_dict = dict(
86-
summary=summary or doc_summary,
87-
description=description or doc_description
88-
)
88+
operation_dict = {}
89+
90+
if summary:
91+
operation_dict["summary"] = summary # type: ignore
92+
93+
if description:
94+
operation_dict["description"] = description # type: ignore
8995

9096
# Add any additional openapi_extensions to the operation dictionary
9197
operation_dict.update(openapi_extensions or {})
@@ -136,16 +142,18 @@ def parse_header(header: Type[BaseModel]) -> Tuple[List[Parameter], dict]:
136142
data = {
137143
"name": name,
138144
"in": ParameterInType.HEADER,
139-
"description": value.get("description"),
140145
"required": name in schema.get("required", []),
141146
"schema": Schema(**value)
142147
}
143148
# Parse extra values
144-
data.update({
145-
"deprecated": value.get("deprecated"),
146-
"example": value.get("example"),
147-
"examples": value.get("examples"),
148-
})
149+
if "description" in value.keys():
150+
data["description"] = value.get("description")
151+
if "deprecated" in value.keys():
152+
data["deprecated"] = value.get("deprecated")
153+
if "example" in value.keys():
154+
data["example"] = value.get("example")
155+
if "examples" in value.keys():
156+
data["examples"] = value.get("examples")
149157
parameters.append(Parameter(**data))
150158

151159
# Parse definitions
@@ -167,16 +175,18 @@ def parse_cookie(cookie: Type[BaseModel]) -> Tuple[List[Parameter], dict]:
167175
data = {
168176
"name": name,
169177
"in": ParameterInType.COOKIE,
170-
"description": value.get("description"),
171178
"required": name in schema.get("required", []),
172179
"schema": Schema(**value)
173180
}
174181
# Parse extra values
175-
data.update({
176-
"deprecated": value.get("deprecated"),
177-
"example": value.get("example"),
178-
"examples": value.get("examples"),
179-
})
182+
if "description" in value.keys():
183+
data["description"] = value.get("description")
184+
if "deprecated" in value.keys():
185+
data["deprecated"] = value.get("deprecated")
186+
if "example" in value.keys():
187+
data["example"] = value.get("example")
188+
if "examples" in value.keys():
189+
data["examples"] = value.get("examples")
180190
parameters.append(Parameter(**data))
181191

182192
# Parse definitions
@@ -198,16 +208,18 @@ def parse_path(path: Type[BaseModel]) -> Tuple[List[Parameter], dict]:
198208
data = {
199209
"name": name,
200210
"in": ParameterInType.PATH,
201-
"description": value.get("description"),
202211
"required": True,
203212
"schema": Schema(**value)
204213
}
205214
# Parse extra values
206-
data.update({
207-
"deprecated": value.get("deprecated"),
208-
"example": value.get("example"),
209-
"examples": value.get("examples"),
210-
})
215+
if "description" in value.keys():
216+
data["description"] = value.get("description")
217+
if "deprecated" in value.keys():
218+
data["deprecated"] = value.get("deprecated")
219+
if "example" in value.keys():
220+
data["example"] = value.get("example")
221+
if "examples" in value.keys():
222+
data["examples"] = value.get("examples")
211223
parameters.append(Parameter(**data))
212224

213225
# Parse definitions
@@ -229,16 +241,18 @@ def parse_query(query: Type[BaseModel]) -> Tuple[List[Parameter], dict]:
229241
data = {
230242
"name": name,
231243
"in": ParameterInType.QUERY,
232-
"description": value.get("description"),
233244
"required": name in schema.get("required", []),
234245
"schema": Schema(**value)
235246
}
236247
# Parse extra values
237-
data.update({
238-
"deprecated": value.get("deprecated"),
239-
"example": value.get("example"),
240-
"examples": value.get("examples"),
241-
})
248+
if "description" in value.keys():
249+
data["description"] = value.get("description")
250+
if "deprecated" in value.keys():
251+
data["deprecated"] = value.get("deprecated")
252+
if "example" in value.keys():
253+
data["example"] = value.get("example")
254+
if "examples" in value.keys():
255+
data["examples"] = value.get("examples")
242256
parameters.append(Parameter(**data))
243257

244258
# Parse definitions
@@ -269,9 +283,10 @@ def parse_form(
269283
content = {
270284
"multipart/form-data": MediaType(
271285
schema=Schema(**{"$ref": f"{OPENAPI3_REF_PREFIX}/{title}"}),
272-
encoding=encoding or None
273286
)
274287
}
288+
if encoding:
289+
content["multipart/form-data"].encoding = encoding
275290

276291
# Parse definitions
277292
definitions = schema.get("$defs", {})
@@ -333,18 +348,24 @@ def get_responses(
333348
)})
334349

335350
model_config: DefaultDict[str, Any] = response.model_config # type: ignore
336-
openapi_extra = model_config.get("openapi_extra")
351+
openapi_extra = model_config.get("openapi_extra", {})
337352
if openapi_extra:
353+
openapi_extra_keys = openapi_extra.keys()
338354
# Add additional information from model_config to the response
339-
_responses[key].description = openapi_extra.get("description")
340-
_responses[key].headers = openapi_extra.get("headers")
341-
_responses[key].links = openapi_extra.get("links")
355+
if "description" in openapi_extra_keys:
356+
_responses[key].description = openapi_extra.get("description")
357+
if "headers" in openapi_extra_keys:
358+
_responses[key].headers = openapi_extra.get("headers")
359+
if "links" in openapi_extra_keys:
360+
_responses[key].links = openapi_extra.get("links")
342361
_content = _responses[key].content
343-
if _content is not None:
344-
_content["application/json"].example = openapi_extra.get("example")
345-
_content["application/json"].examples = openapi_extra.get("examples")
346-
_content["application/json"].encoding = openapi_extra.get("encoding")
347-
_content.update(openapi_extra.get("content", {}))
362+
if "example" in openapi_extra_keys:
363+
_content["application/json"].example = openapi_extra.get("example") # type: ignore
364+
if "examples" in openapi_extra_keys:
365+
_content["application/json"].examples = openapi_extra.get("examples") # type: ignore
366+
if "encoding" in openapi_extra_keys:
367+
_content["application/json"].encoding = openapi_extra.get("encoding") # type: ignore
368+
_content.update(openapi_extra.get("content", {})) # type: ignore
348369

349370
_schemas[name] = Schema(**schema)
350371
definitions = schema.get("$defs")
@@ -383,8 +404,8 @@ def parse_and_store_tags(
383404
old_tags.append(tag)
384405

385406
# Set the tags attribute of the operation object to a list of unique tag names from new_tags
386-
# If the resulting list is empty, set it to None
387-
operation.tags = list(set([tag.name for tag in new_tags])) or None
407+
# If the resulting list is empty, set it to ["default"]
408+
operation.tags = list(set([tag.name for tag in new_tags])) or ["default"]
388409

389410

390411
def parse_parameters(
@@ -459,29 +480,38 @@ def parse_parameters(
459480
if form:
460481
_content, _components_schemas = parse_form(form)
461482
components_schemas.update(**_components_schemas)
462-
request_body = RequestBody(content=_content)
483+
request_body = RequestBody(content=_content, required=True)
463484
model_config: DefaultDict[str, Any] = form.model_config # type: ignore
464-
openapi_extra = model_config.get("openapi_extra")
485+
openapi_extra = model_config.get("openapi_extra", {})
465486
if openapi_extra:
466-
request_body.description = openapi_extra.get("description")
467-
request_body.content["multipart/form-data"].example = openapi_extra.get("example")
468-
request_body.content["multipart/form-data"].examples = openapi_extra.get("examples")
469-
if openapi_extra.get("encoding"):
487+
openapi_extra_keys = openapi_extra.keys()
488+
if "description" in openapi_extra_keys:
489+
request_body.description = openapi_extra.get("description")
490+
if "example" in openapi_extra_keys:
491+
request_body.content["multipart/form-data"].example = openapi_extra.get("example")
492+
if "examples" in openapi_extra_keys:
493+
request_body.content["multipart/form-data"].examples = openapi_extra.get("examples")
494+
if "encoding" in openapi_extra_keys:
470495
request_body.content["multipart/form-data"].encoding = openapi_extra.get("encoding")
471496
operation.requestBody = request_body
472497

473498
if body:
474499
_content, _components_schemas = parse_body(body)
475500
components_schemas.update(**_components_schemas)
476-
request_body = RequestBody(content=_content)
501+
request_body = RequestBody(content=_content, required=True)
477502
model_config: DefaultDict[str, Any] = body.model_config # type: ignore
478-
openapi_extra = model_config.get("openapi_extra")
503+
openapi_extra = model_config.get("openapi_extra", {})
479504
if openapi_extra:
480-
request_body.description = openapi_extra.get("description")
505+
openapi_extra_keys = openapi_extra.keys()
506+
if "description" in openapi_extra_keys:
507+
request_body.description = openapi_extra.get("description")
481508
request_body.required = openapi_extra.get("required", True)
482-
request_body.content["application/json"].example = openapi_extra.get("example")
483-
request_body.content["application/json"].examples = openapi_extra.get("examples")
484-
request_body.content["application/json"].encoding = openapi_extra.get("encoding")
509+
if "example" in openapi_extra_keys:
510+
request_body.content["application/json"].example = openapi_extra.get("example")
511+
if "examples" in openapi_extra_keys:
512+
request_body.content["application/json"].examples = openapi_extra.get("examples")
513+
if "encoding" in openapi_extra_keys:
514+
request_body.content["application/json"].encoding = openapi_extra.get("encoding")
485515
operation.requestBody = request_body
486516

487517
if raw:
@@ -498,8 +528,9 @@ def parse_parameters(
498528
request_body = RequestBody(content=_content)
499529
operation.requestBody = request_body
500530

501-
# Set the parsed parameters in the operation object
502-
operation.parameters = parameters if parameters else None
531+
if parameters:
532+
# Set the parsed parameters in the operation object
533+
operation.parameters = parameters
503534

504535
return header, cookie, path, query, form, body, raw
505536

flask_openapi3/view.py

+11-5
Original file line numberDiff line numberDiff line change
@@ -152,19 +152,25 @@ def decorator(func):
152152
)
153153

154154
# Set external docs
155-
operation.externalDocs = external_docs
155+
if external_docs:
156+
operation.externalDocs = external_docs
156157

157158
# Unique string used to identify the operation.
158-
operation.operationId = operation_id
159+
if operation_id:
160+
operation.operationId = operation_id
159161

160162
# Only set `deprecated` if True, otherwise leave it as None
161-
operation.deprecated = deprecated
163+
if deprecated is not None:
164+
operation.deprecated = deprecated
162165

163166
# Add security
164-
operation.security = security + self.view_security or None
167+
_security = (security or []) + self.view_security or None
168+
if _security:
169+
operation.security = _security
165170

166171
# Add servers
167-
operation.servers = servers
172+
if servers:
173+
operation.servers = servers
168174

169175
# Store tags
170176
parse_and_store_tags(tags, self.tags, self.tag_names, operation)

0 commit comments

Comments
 (0)