Skip to content

Commit b357744

Browse files
wing328diorcety
andauthored
Fix python-fastapi signature of parameters for method (#19830)
* Fix python-fastapi signature of parameters for method * update --------- Co-authored-by: Diorcet Yann <diorcet.yann@gmail.com>
1 parent 45fa438 commit b357744

13 files changed

Lines changed: 183 additions & 55 deletions

File tree

modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractPythonCodegen.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
import org.openapitools.codegen.CodegenOperation;
5151
import org.openapitools.codegen.CodegenParameter;
5252
import org.openapitools.codegen.CodegenProperty;
53+
import org.openapitools.codegen.CodegenResponse;
5354
import org.openapitools.codegen.DefaultCodegen;
5455
import org.openapitools.codegen.GeneratorLanguage;
5556
import org.openapitools.codegen.IJsonSchemaValidationProperties;
@@ -1264,6 +1265,23 @@ public OperationsMap postProcessOperationsWithModels(OperationsMap objs, List<Mo
12641265
);
12651266
}
12661267

1268+
// update typing import for operation responses type
1269+
// only python-fastapi needs this at the moment
1270+
if (this instanceof PythonFastAPIServerCodegen) {
1271+
for (CodegenResponse response : operation.responses) {
1272+
// Not interested in the result, only in the update of the imports
1273+
getPydanticType(
1274+
response.returnProperty,
1275+
modelImports,
1276+
exampleImports,
1277+
postponedModelImports,
1278+
postponedExampleImports,
1279+
moduleImports,
1280+
null
1281+
);
1282+
}
1283+
}
1284+
12671285
// add import for code samples
12681286
// import models one by one
12691287
if (!exampleImports.isEmpty()) {

modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/PythonFastAPIServerCodegen.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,8 @@ public String getTypeDeclaration(Schema p) {
219219

220220
@Override
221221
public OperationsMap postProcessOperationsWithModels(OperationsMap objs, List<ModelMap> allModels) {
222+
super.postProcessOperationsWithModels(objs, allModels);
223+
222224
OperationMap operations = objs.getOperations();
223225
// Set will make sure that no duplicated items are used.
224226
Set<String> securityImports = new HashSet<>();
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
{{#isString}}str{{/isString}}{{#isInteger}}int{{/isInteger}}{{#isLong}}int{{/isLong}}{{#isFloat}}float{{/isFloat}}{{#isDouble}}float{{/isDouble}}{{#isByteArray}}str{{/isByteArray}}{{#isBinary}}str{{/isBinary}}{{#isBoolean}}bool{{/isBoolean}}{{#isDate}}str{{/isDate}}{{#isDateTime}}str{{/isDateTime}}{{#isModel}}{{dataType}}{{/isModel}}{{#isContainer}}{{dataType}}{{/isContainer}}
1+
{{{vendorExtensions.x-py-typing}}}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
package org.openapitools.codegen.python;
2+
3+
import com.google.common.collect.Sets;
4+
import io.swagger.parser.OpenAPIParser;
5+
import io.swagger.v3.oas.models.OpenAPI;
6+
import io.swagger.v3.oas.models.Operation;
7+
import io.swagger.v3.oas.models.media.ArraySchema;
8+
import io.swagger.v3.oas.models.media.Schema;
9+
import io.swagger.v3.parser.core.models.ParseOptions;
10+
import org.openapitools.codegen.*;
11+
import org.openapitools.codegen.languages.PythonClientCodegen;
12+
import org.openapitools.codegen.languages.PythonFastAPIServerCodegen;
13+
import org.openapitools.codegen.languages.features.CXFServerFeatures;
14+
import org.testng.Assert;
15+
import org.testng.annotations.Test;
16+
17+
import java.io.File;
18+
import java.io.IOException;
19+
import java.nio.file.Files;
20+
import java.nio.file.Path;
21+
import java.nio.file.Paths;
22+
import java.util.List;
23+
24+
import static org.openapitools.codegen.TestUtils.assertFileContains;
25+
import static org.openapitools.codegen.TestUtils.assertFileExists;
26+
27+
public class PythonFastAPIServerCodegenTest {
28+
29+
// Helper function, intended to reduce boilerplate
30+
static private String generateFiles(DefaultCodegen codegen, String filePath) throws IOException {
31+
final File output = Files.createTempDirectory("test").toFile().getCanonicalFile();
32+
output.deleteOnExit();
33+
final String outputPath = output.getAbsolutePath().replace('\\', '/');
34+
35+
codegen.setOutputDir(output.getAbsolutePath());
36+
codegen.additionalProperties().put(CXFServerFeatures.LOAD_TEST_DATA_FROM_FILE, "true");
37+
38+
final ClientOptInput input = new ClientOptInput();
39+
final OpenAPI openAPI = new OpenAPIParser().readLocation(filePath, null, new ParseOptions()).getOpenAPI();
40+
input.openAPI(openAPI);
41+
input.config(codegen);
42+
43+
final DefaultGenerator generator = new DefaultGenerator();
44+
final List<File> files = generator.opts(input).generate();
45+
46+
Assert.assertTrue(files.size() > 0);
47+
return outputPath + "/";
48+
}
49+
50+
51+
@Test(description = "test containerType in parameters")
52+
public void testContainerType() throws IOException {
53+
final DefaultCodegen codegen = new PythonFastAPIServerCodegen();
54+
final String outputPath = generateFiles(codegen, "src/test/resources/bugs/pr_18691.json");
55+
final Path p = Paths.get(outputPath + "src/openapi_server/apis/default_api.py");
56+
57+
assertFileExists(p);
58+
assertFileContains(p, "body: Optional[Dict[str, Any]] = Body(None, description=\"\"),");
59+
}
60+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{
2+
"openapi": "3.0.1",
3+
"info": {
4+
"title": "OpenAPI definition",
5+
"version": "v0"
6+
},
7+
"paths": {
8+
"/licensing/token/renew": {
9+
"post": {
10+
"description": "Manually ask license issuer for a new token. Available in Grafana Enterprise v7.4+.\n\nYou need to have a permission with action `licensing:update`.",
11+
"operationId": "postRenewLicenseToken",
12+
"requestBody": {
13+
"content": {
14+
"application/json": {
15+
"schema": {
16+
"type": "object"
17+
}
18+
}
19+
}
20+
}
21+
}
22+
}
23+
}
24+
}

samples/server/petstore/python-fastapi/src/openapi_server/apis/fake_api.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@
2323
)
2424

2525
from openapi_server.models.extra_models import TokenModel # noqa: F401
26+
from pydantic import Field, StrictStr
27+
from typing import Any, Optional
28+
from typing_extensions import Annotated
2629

2730

2831
router = APIRouter()
@@ -43,8 +46,8 @@
4346
response_model_by_alias=True,
4447
)
4548
async def fake_query_param_default(
46-
has_default: str = Query('Hello World', description="has default value", alias="hasDefault"),
47-
no_default: str = Query(None, description="no default value", alias="noDefault"),
49+
has_default: Annotated[Optional[StrictStr], Field(description="has default value")] = Query('Hello World', description="has default value", alias="hasDefault"),
50+
no_default: Annotated[Optional[StrictStr], Field(description="no default value")] = Query(None, description="no default value", alias="noDefault"),
4851
) -> None:
4952
""""""
5053
if not BaseFakeApi.subclasses:

samples/server/petstore/python-fastapi/src/openapi_server/apis/fake_api_base.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22

33
from typing import ClassVar, Dict, List, Tuple # noqa: F401
44

5+
from pydantic import Field, StrictStr
6+
from typing import Any, Optional
7+
from typing_extensions import Annotated
58

69

710
class BaseFakeApi:
@@ -12,8 +15,8 @@ def __init_subclass__(cls, **kwargs):
1215
BaseFakeApi.subclasses = BaseFakeApi.subclasses + (cls,)
1316
async def fake_query_param_default(
1417
self,
15-
has_default: str,
16-
no_default: str,
18+
has_default: Annotated[Optional[StrictStr], Field(description="has default value")],
19+
no_default: Annotated[Optional[StrictStr], Field(description="no default value")],
1720
) -> None:
1821
""""""
1922
...

samples/server/petstore/python-fastapi/src/openapi_server/apis/pet_api.py

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@
2323
)
2424

2525
from openapi_server.models.extra_models import TokenModel # noqa: F401
26+
from pydantic import Field, StrictBytes, StrictInt, StrictStr, field_validator
27+
from typing import Any, List, Optional, Tuple, Union
28+
from typing_extensions import Annotated
2629
from openapi_server.models.api_response import ApiResponse
2730
from openapi_server.models.pet import Pet
2831
from openapi_server.security_api import get_token_petstore_auth, get_token_api_key
@@ -45,7 +48,7 @@
4548
response_model_by_alias=True,
4649
)
4750
async def add_pet(
48-
pet: Pet = Body(None, description="Pet object that needs to be added to the store"),
51+
pet: Annotated[Pet, Field(description="Pet object that needs to be added to the store")] = Body(None, description="Pet object that needs to be added to the store"),
4952
token_petstore_auth: TokenModel = Security(
5053
get_token_petstore_auth, scopes=["write:pets", "read:pets"]
5154
),
@@ -66,8 +69,8 @@ async def add_pet(
6669
response_model_by_alias=True,
6770
)
6871
async def delete_pet(
69-
petId: int = Path(..., description="Pet id to delete"),
70-
api_key: str = Header(None, description=""),
72+
petId: Annotated[StrictInt, Field(description="Pet id to delete")] = Path(..., description="Pet id to delete"),
73+
api_key: Optional[StrictStr] = Header(None, description=""),
7174
token_petstore_auth: TokenModel = Security(
7275
get_token_petstore_auth, scopes=["write:pets", "read:pets"]
7376
),
@@ -89,7 +92,7 @@ async def delete_pet(
8992
response_model_by_alias=True,
9093
)
9194
async def find_pets_by_status(
92-
status: List[str] = Query(None, description="Status values that need to be considered for filter", alias="status"),
95+
status: Annotated[List[StrictStr], Field(description="Status values that need to be considered for filter")] = Query(None, description="Status values that need to be considered for filter", alias="status"),
9396
token_petstore_auth: TokenModel = Security(
9497
get_token_petstore_auth, scopes=["read:pets"]
9598
),
@@ -111,7 +114,7 @@ async def find_pets_by_status(
111114
response_model_by_alias=True,
112115
)
113116
async def find_pets_by_tags(
114-
tags: List[str] = Query(None, description="Tags to filter by", alias="tags"),
117+
tags: Annotated[List[StrictStr], Field(description="Tags to filter by")] = Query(None, description="Tags to filter by", alias="tags"),
115118
token_petstore_auth: TokenModel = Security(
116119
get_token_petstore_auth, scopes=["read:pets"]
117120
),
@@ -134,7 +137,7 @@ async def find_pets_by_tags(
134137
response_model_by_alias=True,
135138
)
136139
async def get_pet_by_id(
137-
petId: int = Path(..., description="ID of pet to return"),
140+
petId: Annotated[StrictInt, Field(description="ID of pet to return")] = Path(..., description="ID of pet to return"),
138141
token_api_key: TokenModel = Security(
139142
get_token_api_key
140143
),
@@ -158,7 +161,7 @@ async def get_pet_by_id(
158161
response_model_by_alias=True,
159162
)
160163
async def update_pet(
161-
pet: Pet = Body(None, description="Pet object that needs to be added to the store"),
164+
pet: Annotated[Pet, Field(description="Pet object that needs to be added to the store")] = Body(None, description="Pet object that needs to be added to the store"),
162165
token_petstore_auth: TokenModel = Security(
163166
get_token_petstore_auth, scopes=["write:pets", "read:pets"]
164167
),
@@ -179,9 +182,9 @@ async def update_pet(
179182
response_model_by_alias=True,
180183
)
181184
async def update_pet_with_form(
182-
petId: int = Path(..., description="ID of pet that needs to be updated"),
183-
name: str = Form(None, description="Updated name of the pet"),
184-
status: str = Form(None, description="Updated status of the pet"),
185+
petId: Annotated[StrictInt, Field(description="ID of pet that needs to be updated")] = Path(..., description="ID of pet that needs to be updated"),
186+
name: Annotated[Optional[StrictStr], Field(description="Updated name of the pet")] = Form(None, description="Updated name of the pet"),
187+
status: Annotated[Optional[StrictStr], Field(description="Updated status of the pet")] = Form(None, description="Updated status of the pet"),
185188
token_petstore_auth: TokenModel = Security(
186189
get_token_petstore_auth, scopes=["write:pets", "read:pets"]
187190
),
@@ -202,9 +205,9 @@ async def update_pet_with_form(
202205
response_model_by_alias=True,
203206
)
204207
async def upload_file(
205-
petId: int = Path(..., description="ID of pet to update"),
206-
additional_metadata: str = Form(None, description="Additional data to pass to server"),
207-
file: str = Form(None, description="file to upload"),
208+
petId: Annotated[StrictInt, Field(description="ID of pet to update")] = Path(..., description="ID of pet to update"),
209+
additional_metadata: Annotated[Optional[StrictStr], Field(description="Additional data to pass to server")] = Form(None, description="Additional data to pass to server"),
210+
file: Annotated[Optional[Union[StrictBytes, StrictStr, Tuple[StrictStr, StrictBytes]]], Field(description="file to upload")] = Form(None, description="file to upload"),
208211
token_petstore_auth: TokenModel = Security(
209212
get_token_petstore_auth, scopes=["write:pets", "read:pets"]
210213
),

samples/server/petstore/python-fastapi/src/openapi_server/apis/pet_api_base.py

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22

33
from typing import ClassVar, Dict, List, Tuple # noqa: F401
44

5+
from pydantic import Field, StrictBytes, StrictInt, StrictStr, field_validator
6+
from typing import Any, List, Optional, Tuple, Union
7+
from typing_extensions import Annotated
58
from openapi_server.models.api_response import ApiResponse
69
from openapi_server.models.pet import Pet
710
from openapi_server.security_api import get_token_petstore_auth, get_token_api_key
@@ -14,68 +17,68 @@ def __init_subclass__(cls, **kwargs):
1417
BasePetApi.subclasses = BasePetApi.subclasses + (cls,)
1518
async def add_pet(
1619
self,
17-
pet: Pet,
20+
pet: Annotated[Pet, Field(description="Pet object that needs to be added to the store")],
1821
) -> Pet:
1922
""""""
2023
...
2124

2225

2326
async def delete_pet(
2427
self,
25-
petId: int,
26-
api_key: str,
28+
petId: Annotated[StrictInt, Field(description="Pet id to delete")],
29+
api_key: Optional[StrictStr],
2730
) -> None:
2831
""""""
2932
...
3033

3134

3235
async def find_pets_by_status(
3336
self,
34-
status: List[str],
37+
status: Annotated[List[StrictStr], Field(description="Status values that need to be considered for filter")],
3538
) -> List[Pet]:
3639
"""Multiple status values can be provided with comma separated strings"""
3740
...
3841

3942

4043
async def find_pets_by_tags(
4144
self,
42-
tags: List[str],
45+
tags: Annotated[List[StrictStr], Field(description="Tags to filter by")],
4346
) -> List[Pet]:
4447
"""Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing."""
4548
...
4649

4750

4851
async def get_pet_by_id(
4952
self,
50-
petId: int,
53+
petId: Annotated[StrictInt, Field(description="ID of pet to return")],
5154
) -> Pet:
5255
"""Returns a single pet"""
5356
...
5457

5558

5659
async def update_pet(
5760
self,
58-
pet: Pet,
61+
pet: Annotated[Pet, Field(description="Pet object that needs to be added to the store")],
5962
) -> Pet:
6063
""""""
6164
...
6265

6366

6467
async def update_pet_with_form(
6568
self,
66-
petId: int,
67-
name: str,
68-
status: str,
69+
petId: Annotated[StrictInt, Field(description="ID of pet that needs to be updated")],
70+
name: Annotated[Optional[StrictStr], Field(description="Updated name of the pet")],
71+
status: Annotated[Optional[StrictStr], Field(description="Updated status of the pet")],
6972
) -> None:
7073
""""""
7174
...
7275

7376

7477
async def upload_file(
7578
self,
76-
petId: int,
77-
additional_metadata: str,
78-
file: str,
79+
petId: Annotated[StrictInt, Field(description="ID of pet to update")],
80+
additional_metadata: Annotated[Optional[StrictStr], Field(description="Additional data to pass to server")],
81+
file: Annotated[Optional[Union[StrictBytes, StrictStr, Tuple[StrictStr, StrictBytes]]], Field(description="file to upload")],
7982
) -> ApiResponse:
8083
""""""
8184
...

samples/server/petstore/python-fastapi/src/openapi_server/apis/store_api.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@
2323
)
2424

2525
from openapi_server.models.extra_models import TokenModel # noqa: F401
26+
from pydantic import Field, StrictInt, StrictStr
27+
from typing import Any, Dict
28+
from typing_extensions import Annotated
2629
from openapi_server.models.order import Order
2730
from openapi_server.security_api import get_token_api_key
2831

@@ -44,7 +47,7 @@
4447
response_model_by_alias=True,
4548
)
4649
async def delete_order(
47-
orderId: str = Path(..., description="ID of the order that needs to be deleted"),
50+
orderId: Annotated[StrictStr, Field(description="ID of the order that needs to be deleted")] = Path(..., description="ID of the order that needs to be deleted"),
4851
) -> None:
4952
"""For valid response try integer IDs with value &lt; 1000. Anything above 1000 or nonintegers will generate API errors"""
5053
if not BaseStoreApi.subclasses:
@@ -84,7 +87,7 @@ async def get_inventory(
8487
response_model_by_alias=True,
8588
)
8689
async def get_order_by_id(
87-
orderId: int = Path(..., description="ID of pet that needs to be fetched", ge=1, le=5),
90+
orderId: Annotated[int, Field(le=5, strict=True, ge=1, description="ID of pet that needs to be fetched")] = Path(..., description="ID of pet that needs to be fetched", ge=1, le=5),
8891
) -> Order:
8992
"""For valid response try integer IDs with value &lt;&#x3D; 5 or &gt; 10. Other values will generate exceptions"""
9093
if not BaseStoreApi.subclasses:
@@ -103,7 +106,7 @@ async def get_order_by_id(
103106
response_model_by_alias=True,
104107
)
105108
async def place_order(
106-
order: Order = Body(None, description="order placed for purchasing the pet"),
109+
order: Annotated[Order, Field(description="order placed for purchasing the pet")] = Body(None, description="order placed for purchasing the pet"),
107110
) -> Order:
108111
""""""
109112
if not BaseStoreApi.subclasses:

0 commit comments

Comments
 (0)