Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions bin/configs/python-aiohttp.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ inputSpec: modules/openapi-generator/src/test/resources/3_0/python/petstore-with
templateDir: modules/openapi-generator/src/main/resources/python
library: asyncio
additionalProperties:
useIndependentImplicitClients: true
packageName: petstore_api
mapNumberTo: float
poetry1: true
Expand Down
1 change: 1 addition & 0 deletions bin/configs/python-lazyImports.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ outputDir: samples/openapi3/client/petstore/python-lazyImports
inputSpec: modules/openapi-generator/src/test/resources/3_0/python/petstore-with-fake-endpoints-models-for-testing.yaml
templateDir: modules/openapi-generator/src/main/resources/python
additionalProperties:
useIndependentImplicitClients: true
packageName: petstore_api
useOneOfDiscriminatorLookup: "true"
disallowAdditionalPropertiesIfNotPresent: false
Expand Down
1 change: 1 addition & 0 deletions docs/generators/python.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl
|recursionLimit|Set the recursion limit. If not set, use the system default value.| |null|
|setEnsureAsciiToFalse|When set to true, add `ensure_ascii=False` in json.dumps when creating the HTTP request body.| |false|
|supportHttpxSync|Generate synchronous '_sync' variants of each API method (httpx library only). Each '_sync' method simply calls the corresponding async method and waits for its completion, so both synchronous and asynchronous methods are available from the same API class.| |false|
|useIndependentImplicitClients|Give API instances without an explicit or registered default ApiClient an owned client with a copied Configuration.| |false|
|useOneOfDiscriminatorLookup|Use the discriminator's mapping in oneOf to speed up the model lookup. IMPORTANT: Validation (e.g. one and only one match in oneOf's schemas) will be skipped.| |false|

## IMPORT MAPPING
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@
import org.openapitools.codegen.meta.GeneratorMetadata;
import org.openapitools.codegen.meta.Stability;
import org.openapitools.codegen.meta.features.*;
import org.openapitools.codegen.model.ModelMap;
import org.openapitools.codegen.model.OperationsMap;
import org.openapitools.codegen.utils.ModelUtils;
import org.openapitools.codegen.utils.ProcessUtils;
import org.slf4j.Logger;
Expand All @@ -33,9 +35,13 @@
import java.io.File;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;

import static org.openapitools.codegen.utils.StringUtils.camelize;
import static org.openapitools.codegen.utils.StringUtils.underscore;

/**
Expand All @@ -54,6 +60,15 @@ public class PythonClientCodegen extends AbstractPythonCodegen implements Codege
public static final String LAZY_IMPORTS = "lazyImports";
public static final String BUILD_SYSTEM = "buildSystem";
public static final String SUPPORT_HTTPX_SYNC = "supportHttpxSync";
public static final String USE_INDEPENDENT_IMPLICIT_CLIENTS = "useIndependentImplicitClients";
private static final Set<String> SYNC_API_LIFECYCLE_METHODS =
Set.of("close", "__enter__", "__exit__");
private static final Set<String> ASYNC_API_LIFECYCLE_METHODS =
Set.of("close", "__aenter__", "__aexit__");
private static final Set<String> HTTPX_SYNC_API_LIFECYCLE_METHODS =
Set.of("close_sync", "__enter__", "__exit__");
private static final Set<String> INDEPENDENT_API_MEMBER_NAMES =
Set.of("_owned_api_client");

@Setter protected String packageUrl;
protected String apiDocPath = "docs/";
Expand All @@ -62,6 +77,7 @@ public class PythonClientCodegen extends AbstractPythonCodegen implements Codege
@Setter protected String datetimeFormat = "%Y-%m-%dT%H:%M:%S.%f%z";
@Setter protected String dateFormat = "%Y-%m-%d";
@Setter protected boolean setEnsureAsciiToFalse = false;
@Setter protected boolean useIndependentImplicitClients = false;

private String testFolder;

Expand Down Expand Up @@ -162,6 +178,10 @@ public PythonClientCodegen() {
cliOptions.add(CliOption.newBoolean(SUPPORT_HTTPX_SYNC, "Generate synchronous '_sync' variants of each API method (httpx library only). " +
"Each '_sync' method simply calls the corresponding async method and waits for its completion, " +
"so both synchronous and asynchronous methods are available from the same API class.").defaultValue(Boolean.FALSE.toString()));
cliOptions.add(CliOption.newBoolean(USE_INDEPENDENT_IMPLICIT_CLIENTS,
"Give API instances without an explicit or registered default ApiClient " +
"an owned client with a copied Configuration.")
.defaultValue(Boolean.FALSE.toString()));

supportedLibraries.put("urllib3", "urllib3-based client");
supportedLibraries.put("asyncio", "asyncio-based client");
Expand Down Expand Up @@ -287,6 +307,14 @@ public void processOpts() {
}
}

if (additionalProperties.containsKey(USE_INDEPENDENT_IMPLICIT_CLIENTS)) {
setUseIndependentImplicitClients(
convertPropertyToBooleanAndWriteBack(USE_INDEPENDENT_IMPLICIT_CLIENTS));
} else {
additionalProperties.put(
USE_INDEPENDENT_IMPLICIT_CLIENTS, useIndependentImplicitClients);
}

String modelPath = packagePath() + File.separatorChar + modelPackage.replace('.', File.separatorChar);
String apiPath = packagePath() + File.separatorChar + apiPackage.replace('.', File.separatorChar);

Expand Down Expand Up @@ -485,6 +513,82 @@ protected void addAdditionPropertiesToCodeGenModel(CodegenModel codegenModel, Sc
}
}

@Override
public OperationsMap postProcessOperationsWithModels(
OperationsMap objs, List<ModelMap> allModels) {
if (useIndependentImplicitClients) {
renameIndependentClientOperationMembers(
objs.getOperations().getOperation());
}
return super.postProcessOperationsWithModels(objs, allModels);
}

private void renameIndependentClientOperationMembers(
List<CodegenOperation> operations) {
Set<String> apiMembers = independentClientApiMembers();
Set<String> occupiedMembers = new HashSet<>(apiMembers);
for (CodegenOperation operation : operations) {
if (!apiMembers.contains(operation.operationId)) {
occupiedMembers.addAll(generatedOperationMembers(operation.operationId));
}
}

for (CodegenOperation operation : operations) {
String originalName = operation.operationId;
if (!apiMembers.contains(originalName)) {
continue;
}

String candidate = "call_" + originalName;
int suffix = 2;
while (generatedOperationMembers(candidate).stream()
.anyMatch(occupiedMembers::contains)) {
candidate = "call_" + originalName + "_" + suffix;
suffix++;
}

LOGGER.warn("{} conflicts with a generated API member. Renamed to {}",
originalName, candidate);
operation.operationId = candidate;
operation.operationIdLowerCase = candidate.toLowerCase(Locale.ROOT);
operation.operationIdCamelCase = camelize(candidate);
operation.operationIdSnakeCase = underscore(candidate);
occupiedMembers.addAll(generatedOperationMembers(candidate));
}
}

private Set<String> independentClientApiMembers() {
Set<String> members = new HashSet<>(INDEPENDENT_API_MEMBER_NAMES);
boolean async = "asyncio".equals(getLibrary()) || "httpx".equals(getLibrary());
members.addAll(async
? ASYNC_API_LIFECYCLE_METHODS
: SYNC_API_LIFECYCLE_METHODS);
if (supportsHttpxSync()) {
members.addAll(HTTPX_SYNC_API_LIFECYCLE_METHODS);
}
return members;
}

private Set<String> generatedOperationMembers(String operationId) {
Set<String> members = new HashSet<>(Set.of(
operationId,
operationId + "_with_http_info",
operationId + "_without_preload_content",
"_" + operationId + "_serialize"));
if (supportsHttpxSync()) {
members.add(operationId + "_sync");
members.add(operationId + "_sync_with_http_info");
members.add(operationId + "_sync_without_preload_content");
}
return members;
}

private boolean supportsHttpxSync() {
return "httpx".equals(getLibrary())
&& Boolean.parseBoolean(String.valueOf(
additionalProperties.get(SUPPORT_HTTPX_SYNC)));
}

@Override
public String escapeReservedWord(String name) {
if (this.reservedWordsMappings().containsKey(name)) {
Expand Down
54 changes: 54 additions & 0 deletions modules/openapi-generator/src/main/resources/python/api.mustache
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,63 @@ class {{classname}}:
"""

def __init__(self, api_client=None) -> None:
{{#useIndependentImplicitClients}}
# api_client remains publicly assignable. Retain the client acquired at
# construction so reassignment cannot transfer or discard ownership.
if api_client is None:
api_client, owns_api_client = ApiClient._get_default_or_new()
else:
owns_api_client = False
{{/useIndependentImplicitClients}}
{{^useIndependentImplicitClients}}
if api_client is None:
api_client = ApiClient.get_default()
{{/useIndependentImplicitClients}}
self.api_client = api_client
{{#useIndependentImplicitClients}}
self._owned_api_client: Optional[ApiClient] = (
api_client if owns_api_client else None
)

{{#async}}
async def close(self) -> None:
owned_api_client = self._owned_api_client
self._owned_api_client = None
if owned_api_client is not None:
await owned_api_client.close()

async def __aenter__(self):
return self

async def __aexit__(self, exc_type, exc_value, traceback):
await self.close()
{{#supportHttpxSync}}

def close_sync(self) -> None:
run_sync(self.close())

def __enter__(self):
return self

def __exit__(self, exc_type, exc_value, traceback):
self.close_sync()
{{/supportHttpxSync}}
{{/async}}
{{^async}}

def close(self) -> None:
owned_api_client = self._owned_api_client
self._owned_api_client = None
if owned_api_client is not None:
owned_api_client.close()

def __enter__(self):
return self

def __exit__(self, exc_type, exc_value, traceback):
self.close()
{{/async}}
{{/useIndependentImplicitClients}}
{{#operation}}


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,12 @@ class ApiClient:
) -> None:
# use default configuration if none is provided
if configuration is None:
{{#useIndependentImplicitClients}}
configuration = Configuration.get_default_copy()
{{/useIndependentImplicitClients}}
{{^useIndependentImplicitClients}}
configuration = Configuration.get_default()
{{/useIndependentImplicitClients}}
self.configuration = configuration

self.rest_client = rest.RESTClientObject(configuration)
Expand All @@ -98,11 +103,21 @@ class ApiClient:
await self.rest_client.close()
{{/async}}
{{^async}}
{{#useIndependentImplicitClients}}
def close(self):
self.rest_client.close()

{{/useIndependentImplicitClients}}
def __enter__(self):
return self

def __exit__(self, exc_type, exc_value, traceback):
{{#useIndependentImplicitClients}}
self.close()
{{/useIndependentImplicitClients}}
{{^useIndependentImplicitClients}}
pass
{{/useIndependentImplicitClients}}
{{/async}}

@property
Expand All @@ -120,8 +135,21 @@ class ApiClient:

_default = None

{{#useIndependentImplicitClients}}
@classmethod
def _get_default_or_new(cls):
if cls._default is not None:
return cls._default, False
return cls(), True

{{/useIndependentImplicitClients}}
@classmethod
def get_default(cls):
{{#useIndependentImplicitClients}}
"""Return the registered default ApiClient, or a new client."""
return cls._get_default_or_new()[0]
{{/useIndependentImplicitClients}}
{{^useIndependentImplicitClients}}
"""Return new instance of ApiClient.

This method returns newly created, based on default constructor,
Expand All @@ -133,6 +161,7 @@ class ApiClient:
if cls._default is None:
cls._default = ApiClient()
return cls._default
{{/useIndependentImplicitClients}}

@classmethod
def set_default(cls, default):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,16 +118,37 @@ class RESTClientObject:
fields (trace_configs / client_session_kwargs) are read via getattr
so older Configuration objects remain compatible.
"""
extra = getattr(self.configuration, "client_session_kwargs", None)
kwargs: Dict[str, Any] = {
{{#useIndependentImplicitClients}}
# Preserve an explicit connector=None override. ClientSession then
# creates and owns its default connector instead of using ours:
# https://github.com/aio-libs/aiohttp/blob/v3.8.4/aiohttp/client.py#L238-L257
"connector": (
extra["connector"]
if extra is not None and "connector" in extra
else self._create_connector()
),
{{/useIndependentImplicitClients}}
{{^useIndependentImplicitClients}}
"connector": self._create_connector(),
{{/useIndependentImplicitClients}}
"trust_env": True,
}
trace_configs = getattr(self.configuration, "trace_configs", None)
if trace_configs is not None:
kwargs["trace_configs"] = trace_configs
extra = getattr(self.configuration, "client_session_kwargs", None)
if extra:
kwargs.update(extra)
{{#useIndependentImplicitClients}}
if extra.get("connector") is not None:
# Copied configurations can share a caller-supplied connector.
# aiohttp requires connector_owner=False for shared connectors:
# https://docs.aiohttp.org/en/stable/client_advanced.html#connectors
kwargs["connector_owner"] = False
else:
kwargs["connector_owner"] = True
{{/useIndependentImplicitClients}}
return aiohttp.ClientSession(**kwargs)

async def request(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,31 @@ from pprint import pprint
{{/-first}}{{/operation}}{{/operations}}{{/-first}}{{/apis}}{{/apiInfo}}
```

{{#useIndependentImplicitClients}}
An API instance constructed without an explicit or registered default
`ApiClient` owns its client. Use the API as a context manager:

```python
{{#apiInfo}}{{#apis}}{{#-first}}{{#operations}}{{#async}}async {{/async}}with {{{packageName}}}.{{{classname}}}() as api_instance:
# Call API methods through api_instance.
pass
{{/operations}}{{/-first}}{{/apis}}{{/apiInfo}}
```

Direct construction requires
`{{#async}}await {{/async}}api_instance.close()`. Closing an API constructed
with an explicit or registered default client is a no-op; the caller remains
responsible for that client. Configuration-owned values are copied for an
implicit client. Logging resources retain their identity. Configuration
containers and proxy-header mappings are copied; other stateful transport
extension objects remain caller-owned.

`Configuration.set_default()` stores a copy, so later changes to the supplied
object do not affect new implicit clients. `Configuration.get_default()`
returns that stored baseline, while `Configuration.get_default_copy()` returns
a copy.

{{/useIndependentImplicitClients}}
## Documentation for API Endpoints

All URIs are relative to *{{{basePath}}}*
Expand Down
Loading
Loading