flask-ast-openapi 0.2.0__tar.gz → 0.2.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (19) hide show
  1. {flask_ast_openapi-0.2.0 → flask_ast_openapi-0.2.2}/PKG-INFO +24 -5
  2. flask_ast_openapi-0.2.0/src/flask_ast_openapi.egg-info/PKG-INFO → flask_ast_openapi-0.2.2/README.md +37 -30
  3. {flask_ast_openapi-0.2.0 → flask_ast_openapi-0.2.2}/pyproject.toml +1 -1
  4. {flask_ast_openapi-0.2.0 → flask_ast_openapi-0.2.2}/src/flask_ast_openapi/generator.py +145 -46
  5. flask_ast_openapi-0.2.0/README.md → flask_ast_openapi-0.2.2/src/flask_ast_openapi.egg-info/PKG-INFO +46 -15
  6. {flask_ast_openapi-0.2.0 → flask_ast_openapi-0.2.2}/tests/test_generator.py +129 -6
  7. {flask_ast_openapi-0.2.0 → flask_ast_openapi-0.2.2}/LICENSE +0 -0
  8. {flask_ast_openapi-0.2.0 → flask_ast_openapi-0.2.2}/setup.cfg +0 -0
  9. {flask_ast_openapi-0.2.0 → flask_ast_openapi-0.2.2}/src/flask_ast_openapi/__init__.py +0 -0
  10. {flask_ast_openapi-0.2.0 → flask_ast_openapi-0.2.2}/src/flask_ast_openapi/cli.py +0 -0
  11. {flask_ast_openapi-0.2.0 → flask_ast_openapi-0.2.2}/src/flask_ast_openapi/swagger.py +0 -0
  12. {flask_ast_openapi-0.2.0 → flask_ast_openapi-0.2.2}/src/flask_ast_openapi.egg-info/SOURCES.txt +0 -0
  13. {flask_ast_openapi-0.2.0 → flask_ast_openapi-0.2.2}/src/flask_ast_openapi.egg-info/dependency_links.txt +0 -0
  14. {flask_ast_openapi-0.2.0 → flask_ast_openapi-0.2.2}/src/flask_ast_openapi.egg-info/entry_points.txt +0 -0
  15. {flask_ast_openapi-0.2.0 → flask_ast_openapi-0.2.2}/src/flask_ast_openapi.egg-info/requires.txt +0 -0
  16. {flask_ast_openapi-0.2.0 → flask_ast_openapi-0.2.2}/src/flask_ast_openapi.egg-info/top_level.txt +0 -0
  17. {flask_ast_openapi-0.2.0 → flask_ast_openapi-0.2.2}/tests/test_cli.py +0 -0
  18. {flask_ast_openapi-0.2.0 → flask_ast_openapi-0.2.2}/tests/test_package.py +0 -0
  19. {flask_ast_openapi-0.2.0 → flask_ast_openapi-0.2.2}/tests/test_swagger.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: flask-ast-openapi
3
- Version: 0.2.0
3
+ Version: 0.2.2
4
4
  Summary: Generate OpenAPI specifications from Flask source code using AST
5
5
  Requires-Python: >=3.10
6
6
  Description-Content-Type: text/markdown
@@ -55,6 +55,8 @@ It also provides optional Swagger UI integration for Flask applications.
55
55
  * Marshmallow request schema support
56
56
  * Marshmallow response schema support
57
57
  * Cross-file Marshmallow schema resolution
58
+ * Automatic request content-type detection for JSON and multipart forms
59
+ * Request and response content-type docstring overrides
58
60
  * Nested Marshmallow schema support
59
61
  * OpenAPI component schema generation
60
62
  * Configurable authentication decorators
@@ -431,6 +433,23 @@ def create_user():
431
433
  ...
432
434
  ```
433
435
 
436
+ JSON bodies are detected from `request.get_json()` and `request.json`.
437
+ Multipart bodies are detected from `request.form` and `request.files`.
438
+ For custom media types, use docstring directives:
439
+
440
+ ```python
441
+ @app.post("/documents")
442
+ def upload_document():
443
+ """
444
+ :request: DocumentUploadSchema
445
+ :response: DocumentResponseSchema
446
+ :request-content-type: multipart/form-data
447
+ :response-content-type: application/json
448
+ """
449
+
450
+ ...
451
+ ```
452
+
434
453
  The generator detects `UserRequestSchema` and creates an OpenAPI request-body schema.
435
454
 
436
455
  ## Response Schemas
@@ -785,12 +804,12 @@ Future development may include:
785
804
  Current version:
786
805
 
787
806
  ```text
788
- 0.2.0
807
+ 0.2.1
789
808
  ```
790
809
 
791
- Version `0.2.0` adds nested cross-file Blueprint prefix resolution,
792
- automatic Blueprint tags, cross-file Marshmallow schema resolution, and
793
- default exclusion of test and virtual-environment directories.
810
+ Version `0.2.1` adds automatic JSON and multipart request content-type
811
+ detection, explicit request/response media-type overrides, and avoids creating
812
+ request bodies for header-only and path-only inputs.
794
813
 
795
814
  ## License
796
815
 
@@ -1,15 +1,3 @@
1
- Metadata-Version: 2.4
2
- Name: flask-ast-openapi
3
- Version: 0.2.0
4
- Summary: Generate OpenAPI specifications from Flask source code using AST
5
- Requires-Python: >=3.10
6
- Description-Content-Type: text/markdown
7
- License-File: LICENSE
8
- Requires-Dist: Flask>=3.0
9
- Provides-Extra: dev
10
- Requires-Dist: pytest>=8; extra == "dev"
11
- Dynamic: license-file
12
-
13
1
  # flask-ast-openapi
14
2
 
15
3
  Generate OpenAPI 3 specifications from Flask source code using Python's Abstract Syntax Tree (AST).
@@ -31,10 +19,10 @@ It also provides optional Swagger UI integration for Flask applications.
31
19
  * `@app.put(...)`
32
20
  * `@app.patch(...)`
33
21
  * `@app.delete(...)`
34
- * Blueprint route support
35
- * Blueprint `url_prefix` support
36
- * Nested and cross-file Blueprint registration support
37
- * Automatic Blueprint tags for Swagger UI grouping
22
+ * Blueprint route support
23
+ * Blueprint `url_prefix` support
24
+ * Nested and cross-file Blueprint registration support
25
+ * Automatic Blueprint tags for Swagger UI grouping
38
26
  * Flask path parameter conversion to OpenAPI format
39
27
  * Flask converter support:
40
28
 
@@ -53,8 +41,10 @@ It also provides optional Swagger UI integration for Flask applications.
53
41
  * Function docstrings as OpenAPI descriptions
54
42
  * Synchronous and asynchronous Flask route support
55
43
  * Marshmallow request schema support
56
- * Marshmallow response schema support
57
- * Cross-file Marshmallow schema resolution
44
+ * Marshmallow response schema support
45
+ * Cross-file Marshmallow schema resolution
46
+ * Automatic request content-type detection for JSON and multipart forms
47
+ * Request and response content-type docstring overrides
58
48
  * Nested Marshmallow schema support
59
49
  * OpenAPI component schema generation
60
50
  * Configurable authentication decorators
@@ -417,7 +407,7 @@ class UserRequestSchema(Schema):
417
407
  active = fields.Boolean()
418
408
  ```
419
409
 
420
- A route can reference the schema from its docstring:
410
+ A route can reference the schema from its docstring:
421
411
 
422
412
  ```python
423
413
  @app.post("/users")
@@ -428,8 +418,25 @@ def create_user():
428
418
  :request: UserRequestSchema
429
419
  """
430
420
 
431
- ...
432
- ```
421
+ ...
422
+ ```
423
+
424
+ JSON bodies are detected from `request.get_json()` and `request.json`.
425
+ Multipart bodies are detected from `request.form` and `request.files`.
426
+ For custom media types, use docstring directives:
427
+
428
+ ```python
429
+ @app.post("/documents")
430
+ def upload_document():
431
+ """
432
+ :request: DocumentUploadSchema
433
+ :response: DocumentResponseSchema
434
+ :request-content-type: multipart/form-data
435
+ :response-content-type: application/json
436
+ """
437
+
438
+ ...
439
+ ```
433
440
 
434
441
  The generator detects `UserRequestSchema` and creates an OpenAPI request-body schema.
435
442
 
@@ -782,15 +789,15 @@ Future development may include:
782
789
 
783
790
  ## Version
784
791
 
785
- Current version:
786
-
787
- ```text
788
- 0.2.0
789
- ```
790
-
791
- Version `0.2.0` adds nested cross-file Blueprint prefix resolution,
792
- automatic Blueprint tags, cross-file Marshmallow schema resolution, and
793
- default exclusion of test and virtual-environment directories.
792
+ Current version:
793
+
794
+ ```text
795
+ 0.2.1
796
+ ```
797
+
798
+ Version `0.2.1` adds automatic JSON and multipart request content-type
799
+ detection, explicit request/response media-type overrides, and avoids creating
800
+ request bodies for header-only and path-only inputs.
794
801
 
795
802
  ## License
796
803
 
@@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
5
5
 
6
6
  [project]
7
7
  name = "flask-ast-openapi"
8
- version = "0.2.0"
8
+ version = "0.2.2"
9
9
  description = "Generate OpenAPI specifications from Flask source code using AST"
10
10
  readme = "README.md"
11
11
  requires-python = ">=3.10"
@@ -32,8 +32,10 @@ class RouteDefinition:
32
32
  auth_schemes: list[str] = field(default_factory=list)
33
33
  auth_scheme_mode: str = "and"
34
34
  description: str | None = None
35
- request_schema: dict[str, Any] | None = None
35
+ request_schema: dict[str, Any] | None = None
36
36
  response_schema: dict[str, Any] | None = None
37
+ request_content_type: str | None = "application/json"
38
+ response_content_type: str = "application/json"
37
39
  tag: str | None = None
38
40
  @dataclass
39
41
  class PathParameter:
@@ -283,10 +285,16 @@ class FlaskASTOpenAPI:
283
285
  function
284
286
  )
285
287
  ),
286
- request_schema=self.build_request_schema_from_docstring(
287
- tree,
288
- function,
289
- ),
288
+ request_schema=self.build_request_schema_from_docstring(
289
+ tree,
290
+ function,
291
+ ),
292
+ request_content_type=(
293
+ self.extract_request_content_type(function)
294
+ ),
295
+ response_content_type=(
296
+ self.extract_response_content_type(function)
297
+ ),
290
298
  uses_json_body=(
291
299
  self.function_uses_json_body(
292
300
  function
@@ -422,11 +430,12 @@ class FlaskASTOpenAPI:
422
430
  route.path
423
431
  )
424
432
 
425
- parameters.extend(
426
- self.build_query_parameters_from_names(
427
- route.query_parameter_names
428
- )
429
- )
433
+ parameters.extend(
434
+ self.build_query_parameters_from_names(
435
+ route.query_parameter_names,
436
+ route.request_schema,
437
+ )
438
+ )
430
439
  response_status_codes = list(
431
440
  route.response_status_codes
432
441
  )
@@ -451,16 +460,20 @@ class FlaskASTOpenAPI:
451
460
  operation: dict[str, Any] = {
452
461
  "operationId": route.function_name,
453
462
  "parameters": parameters,
454
- "responses": self.build_openapi_responses(
455
- response_status_codes,
456
- response_schemas,
457
- ),
463
+ "responses": self.build_openapi_responses(
464
+ response_status_codes,
465
+ response_schemas,
466
+ route.response_content_type,
467
+ ),
458
468
  }
459
469
 
460
470
  if route.tag:
461
471
  operation["tags"] = [route.tag]
462
472
 
463
- if route.uses_json_body or route.request_schema is not None:
473
+ if (
474
+ route.request_content_type is not None
475
+ and (route.uses_json_body or route.request_schema is not None)
476
+ ):
464
477
  request_schema = route.request_schema
465
478
 
466
479
  if request_schema is None:
@@ -471,12 +484,12 @@ class FlaskASTOpenAPI:
471
484
  )
472
485
 
473
486
  operation["requestBody"] = {
474
- "required": True,
475
- "content": {
476
- "application/json": {
477
- "schema": request_schema,
478
- }
479
- },
487
+ "required": True,
488
+ "content": {
489
+ route.request_content_type: {
490
+ "schema": request_schema,
491
+ }
492
+ },
480
493
  }
481
494
 
482
495
  if route.description:
@@ -781,21 +794,34 @@ class FlaskASTOpenAPI:
781
794
  )
782
795
 
783
796
  return parameters
784
- def build_query_parameters_from_names(self,names: list[str],) -> list[dict[str, Any]]:
785
- """Build OpenAPI query parameters from their names."""
786
-
787
- return [
788
- {
789
- "name": name,
790
- "in": "query",
791
- "required": False,
792
- "schema": {
793
- "type": "string",
794
- },
795
- }
796
- for name in names
797
- ]
798
- def function_uses_json_body(self,function: ast.FunctionDef | ast.AsyncFunctionDef,) -> bool:
797
+ def build_query_parameters_from_names(
798
+ self,
799
+ names: list[str],
800
+ request_schema: dict[str, Any] | None = None,
801
+ ) -> list[dict[str, Any]]:
802
+ """Build OpenAPI query parameters from their names."""
803
+
804
+ schema_properties = (
805
+ request_schema.get("properties", {})
806
+ if request_schema
807
+ else {}
808
+ )
809
+ required_names = set(
810
+ request_schema.get("required", [])
811
+ if request_schema
812
+ else []
813
+ )
814
+
815
+ return [
816
+ {
817
+ "name": name,
818
+ "in": "query",
819
+ "required": name in required_names,
820
+ "schema": schema_properties.get(name, {"type": "string"}),
821
+ }
822
+ for name in names
823
+ ]
824
+ def function_uses_json_body(self,function: ast.FunctionDef | ast.AsyncFunctionDef,) -> bool:
799
825
  """Check whether a route function reads a JSON request body."""
800
826
 
801
827
  for node in ast.walk(function):
@@ -816,7 +842,79 @@ class FlaskASTOpenAPI:
816
842
  ):
817
843
  return True
818
844
 
819
- return False
845
+ return False
846
+
847
+ def function_uses_multipart_body(
848
+ self,
849
+ function: ast.FunctionDef | ast.AsyncFunctionDef,
850
+ ) -> bool:
851
+ """Check whether a route reads form fields or uploaded files."""
852
+
853
+ for node in ast.walk(function):
854
+ if not isinstance(node, ast.Attribute):
855
+ continue
856
+
857
+ if (
858
+ isinstance(node.value, ast.Name)
859
+ and node.value.id == "request"
860
+ and node.attr in {"form", "files"}
861
+ ):
862
+ return True
863
+
864
+ return False
865
+
866
+ def extract_docstring_directive(
867
+ self,
868
+ function: ast.FunctionDef | ast.AsyncFunctionDef,
869
+ directive: str,
870
+ ) -> str | None:
871
+ """Extract a named ``:directive: value`` from a docstring."""
872
+
873
+ docstring = ast.get_docstring(function)
874
+ if not docstring:
875
+ return None
876
+
877
+ match = re.search(
878
+ rf"^\s*:{re.escape(directive)}:\s*([^\s]+)",
879
+ docstring,
880
+ re.MULTILINE,
881
+ )
882
+ return match.group(1) if match else None
883
+
884
+ def extract_request_content_type(
885
+ self,
886
+ function: ast.FunctionDef | ast.AsyncFunctionDef,
887
+ ) -> str | None:
888
+ """Infer request body media type, allowing a docstring override."""
889
+
890
+ explicit = self.extract_docstring_directive(
891
+ function,
892
+ "request-content-type",
893
+ )
894
+ if explicit:
895
+ return explicit
896
+
897
+ if self.function_uses_multipart_body(function):
898
+ return "multipart/form-data"
899
+
900
+ if self.function_uses_json_body(function):
901
+ return "application/json"
902
+
903
+ return None
904
+
905
+ def extract_response_content_type(
906
+ self,
907
+ function: ast.FunctionDef | ast.AsyncFunctionDef,
908
+ ) -> str:
909
+ """Return the declared response media type or JSON by default."""
910
+
911
+ return (
912
+ self.extract_docstring_directive(
913
+ function,
914
+ "response-content-type",
915
+ )
916
+ or "application/json"
917
+ )
820
918
  def extract_json_body_field_names(self,function: ast.FunctionDef | ast.AsyncFunctionDef,) -> list[str]:
821
919
  """Extract JSON body field names used inside a route function."""
822
920
 
@@ -1151,10 +1249,11 @@ class FlaskASTOpenAPI:
1151
1249
  status_codes.append(status_code)
1152
1250
 
1153
1251
  return status_codes
1154
- def build_openapi_responses(
1155
- self,
1156
- status_codes: list[int],
1157
- response_schemas: dict[int, dict[str, Any]] | None = None,
1252
+ def build_openapi_responses(
1253
+ self,
1254
+ status_codes: list[int],
1255
+ response_schemas: dict[int, dict[str, Any]] | None = None,
1256
+ content_type: str = "application/json",
1158
1257
  ) -> dict[str, Any]:
1159
1258
  """Build OpenAPI responses from status codes and schemas."""
1160
1259
 
@@ -1176,11 +1275,11 @@ class FlaskASTOpenAPI:
1176
1275
  schema = response_schemas.get(status_code)
1177
1276
 
1178
1277
  if schema:
1179
- response["content"] = {
1180
- "application/json": {
1181
- "schema": schema,
1182
- }
1183
- }
1278
+ response["content"] = {
1279
+ content_type: {
1280
+ "schema": schema,
1281
+ }
1282
+ }
1184
1283
 
1185
1284
  responses[str(status_code)] = response
1186
1285
 
@@ -1,3 +1,15 @@
1
+ Metadata-Version: 2.4
2
+ Name: flask-ast-openapi
3
+ Version: 0.2.2
4
+ Summary: Generate OpenAPI specifications from Flask source code using AST
5
+ Requires-Python: >=3.10
6
+ Description-Content-Type: text/markdown
7
+ License-File: LICENSE
8
+ Requires-Dist: Flask>=3.0
9
+ Provides-Extra: dev
10
+ Requires-Dist: pytest>=8; extra == "dev"
11
+ Dynamic: license-file
12
+
1
13
  # flask-ast-openapi
2
14
 
3
15
  Generate OpenAPI 3 specifications from Flask source code using Python's Abstract Syntax Tree (AST).
@@ -19,10 +31,10 @@ It also provides optional Swagger UI integration for Flask applications.
19
31
  * `@app.put(...)`
20
32
  * `@app.patch(...)`
21
33
  * `@app.delete(...)`
22
- * Blueprint route support
23
- * Blueprint `url_prefix` support
24
- * Nested and cross-file Blueprint registration support
25
- * Automatic Blueprint tags for Swagger UI grouping
34
+ * Blueprint route support
35
+ * Blueprint `url_prefix` support
36
+ * Nested and cross-file Blueprint registration support
37
+ * Automatic Blueprint tags for Swagger UI grouping
26
38
  * Flask path parameter conversion to OpenAPI format
27
39
  * Flask converter support:
28
40
 
@@ -41,8 +53,10 @@ It also provides optional Swagger UI integration for Flask applications.
41
53
  * Function docstrings as OpenAPI descriptions
42
54
  * Synchronous and asynchronous Flask route support
43
55
  * Marshmallow request schema support
44
- * Marshmallow response schema support
45
- * Cross-file Marshmallow schema resolution
56
+ * Marshmallow response schema support
57
+ * Cross-file Marshmallow schema resolution
58
+ * Automatic request content-type detection for JSON and multipart forms
59
+ * Request and response content-type docstring overrides
46
60
  * Nested Marshmallow schema support
47
61
  * OpenAPI component schema generation
48
62
  * Configurable authentication decorators
@@ -419,6 +433,23 @@ def create_user():
419
433
  ...
420
434
  ```
421
435
 
436
+ JSON bodies are detected from `request.get_json()` and `request.json`.
437
+ Multipart bodies are detected from `request.form` and `request.files`.
438
+ For custom media types, use docstring directives:
439
+
440
+ ```python
441
+ @app.post("/documents")
442
+ def upload_document():
443
+ """
444
+ :request: DocumentUploadSchema
445
+ :response: DocumentResponseSchema
446
+ :request-content-type: multipart/form-data
447
+ :response-content-type: application/json
448
+ """
449
+
450
+ ...
451
+ ```
452
+
422
453
  The generator detects `UserRequestSchema` and creates an OpenAPI request-body schema.
423
454
 
424
455
  ## Response Schemas
@@ -770,15 +801,15 @@ Future development may include:
770
801
 
771
802
  ## Version
772
803
 
773
- Current version:
774
-
775
- ```text
776
- 0.2.0
777
- ```
778
-
779
- Version `0.2.0` adds nested cross-file Blueprint prefix resolution,
780
- automatic Blueprint tags, cross-file Marshmallow schema resolution, and
781
- default exclusion of test and virtual-environment directories.
804
+ Current version:
805
+
806
+ ```text
807
+ 0.2.1
808
+ ```
809
+
810
+ Version `0.2.1` adds automatic JSON and multipart request content-type
811
+ detection, explicit request/response media-type overrides, and avoids creating
812
+ request bodies for header-only and path-only inputs.
782
813
 
783
814
  ## License
784
815
 
@@ -908,7 +908,7 @@ def get_users():
908
908
  ]
909
909
 
910
910
 
911
- def test_build_openapi_operation_includes_query_parameters(tmp_path):
911
+ def test_build_openapi_operation_includes_query_parameters(tmp_path):
912
912
  generator = FlaskASTOpenAPI(tmp_path)
913
913
 
914
914
  route = RouteDefinition(
@@ -920,7 +920,7 @@ def test_build_openapi_operation_includes_query_parameters(tmp_path):
920
920
 
921
921
  operation = generator.build_openapi_operation(route)
922
922
 
923
- assert operation["parameters"] == [
923
+ assert operation["parameters"] == [
924
924
  {
925
925
  "name": "page",
926
926
  "in": "query",
@@ -937,10 +937,47 @@ def test_build_openapi_operation_includes_query_parameters(tmp_path):
937
937
  "type": "string",
938
938
  },
939
939
  },
940
- ]
941
-
942
-
943
- def test_build_openapi_operation_combines_path_and_query_parameters(
940
+ ]
941
+
942
+
943
+ def test_build_openapi_operation_uses_request_schema_for_query_parameters(
944
+ tmp_path,
945
+ ):
946
+ generator = FlaskASTOpenAPI(tmp_path)
947
+
948
+ route = RouteDefinition(
949
+ function_name="get_users",
950
+ path="/users",
951
+ methods=["GET"],
952
+ query_parameter_names=["page", "page_size"],
953
+ request_schema={
954
+ "type": "object",
955
+ "properties": {
956
+ "page": {"type": "integer"},
957
+ "page_size": {"type": "integer"},
958
+ },
959
+ },
960
+ )
961
+
962
+ operation = generator.build_openapi_operation(route)
963
+
964
+ assert operation["parameters"] == [
965
+ {
966
+ "name": "page",
967
+ "in": "query",
968
+ "required": False,
969
+ "schema": {"type": "integer"},
970
+ },
971
+ {
972
+ "name": "page_size",
973
+ "in": "query",
974
+ "required": False,
975
+ "schema": {"type": "integer"},
976
+ },
977
+ ]
978
+
979
+
980
+ def test_build_openapi_operation_combines_path_and_query_parameters(
944
981
  tmp_path,
945
982
  ):
946
983
  generator = FlaskASTOpenAPI(tmp_path)
@@ -6405,3 +6442,89 @@ def create_user():
6405
6442
  response_schema = operation["responses"]["200"]["content"]["application/json"]["schema"]
6406
6443
  assert request_schema["properties"]["name"] == {"type": "string"}
6407
6444
  assert response_schema["properties"]["id"] == {"type": "integer"}
6445
+
6446
+
6447
+ def test_generate_infers_multipart_form_data(tmp_path):
6448
+ (tmp_path / "routes.py").write_text(
6449
+ """
6450
+ from flask import Blueprint, request
6451
+ from marshmallow import Schema, fields
6452
+
6453
+ class UploadSchema(Schema):
6454
+ title = fields.String(required=True)
6455
+ file = fields.Raw(required=True)
6456
+
6457
+ upload_bp = Blueprint("upload", __name__)
6458
+
6459
+ @upload_bp.route("/upload", methods=["POST"])
6460
+ def upload():
6461
+ \"\"\":request: UploadSchema\"\"\"
6462
+ title = request.form.get("title")
6463
+ file = request.files.get("file")
6464
+ return {}, 200
6465
+ """,
6466
+ encoding="utf-8",
6467
+ )
6468
+
6469
+ spec = FlaskASTOpenAPI(tmp_path).generate()
6470
+ content = spec["paths"]["/upload"]["post"]["requestBody"]["content"]
6471
+
6472
+ assert "multipart/form-data" in content
6473
+ assert "application/json" not in content
6474
+
6475
+
6476
+ def test_generate_does_not_create_body_for_header_only_schema(tmp_path):
6477
+ (tmp_path / "routes.py").write_text(
6478
+ """
6479
+ from flask import Blueprint, request
6480
+ from marshmallow import Schema, fields
6481
+
6482
+ class TokenSchema(Schema):
6483
+ token = fields.String(required=True)
6484
+
6485
+ auth_bp = Blueprint("auth", __name__)
6486
+
6487
+ @auth_bp.route("/validate", methods=["POST"])
6488
+ def validate():
6489
+ \"\"\":request: TokenSchema\"\"\"
6490
+ token = request.headers.get("Authorization")
6491
+ return {}, 200
6492
+ """,
6493
+ encoding="utf-8",
6494
+ )
6495
+
6496
+ spec = FlaskASTOpenAPI(tmp_path).generate()
6497
+
6498
+ assert "requestBody" not in spec["paths"]["/validate"]["post"]
6499
+
6500
+
6501
+ def test_docstring_can_override_request_and_response_content_types(tmp_path):
6502
+ (tmp_path / "routes.py").write_text(
6503
+ """
6504
+ from flask import Blueprint, request
6505
+ from marshmallow import Schema, fields
6506
+
6507
+ class PayloadSchema(Schema):
6508
+ value = fields.String(required=True)
6509
+
6510
+ bp = Blueprint("custom", __name__)
6511
+
6512
+ @bp.route("/custom", methods=["POST"])
6513
+ def custom():
6514
+ \"\"\"
6515
+ :request: PayloadSchema
6516
+ :response: PayloadSchema
6517
+ :request-content-type: application/x-www-form-urlencoded
6518
+ :response-content-type: application/problem+json
6519
+ \"\"\"
6520
+ value = request.form.get("value")
6521
+ return {"value": value}, 200
6522
+ """,
6523
+ encoding="utf-8",
6524
+ )
6525
+
6526
+ spec = FlaskASTOpenAPI(tmp_path).generate()
6527
+ operation = spec["paths"]["/custom"]["post"]
6528
+
6529
+ assert "application/x-www-form-urlencoded" in operation["requestBody"]["content"]
6530
+ assert "application/problem+json" in operation["responses"]["200"]["content"]