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
51 changes: 51 additions & 0 deletions .github/workflows/samples-python-lazy-imports.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
name: "Python Client: Native lazy imports"

on:
pull_request:
paths:
- .github/workflows/samples-python-lazy-imports.yaml
- .mvn/**
- pom.xml
- modules/openapi-generator-core/**
- modules/openapi-generator/pom.xml
- modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractPythonCodegen.java
- modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/PythonClientCodegen.java
- modules/openapi-generator/src/main/resources/python/**
- modules/openapi-generator/src/test/java/org/openapitools/codegen/python/PythonClientCodegenTest.java
- modules/openapi-generator/src/test/resources/3_1/python/lazy*

jobs:
runtime:
name: Native lazy imports runtime contracts (Python 3.15)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- name: Set up JDK 11
uses: actions/setup-java@v6
with:
java-version: 11
distribution: temurin
cache: maven
- name: Set up Python 3.15
uses: actions/setup-python@v7
id: py
with:
python-version: "3.15"
- name: Install runtime dependencies
run: >-
python -m pip install
"pydantic>=2.14"
"python-dateutil>=2.8.2"
"typing-extensions>=4.7.1"
"urllib3>=2.7.0,<3"
"aiohttp>=3.14.1"
"aiohttp-retry>=2.8.3"
"httpx>=0.28.1"
"httpx2>=2.13.0,<3"
- name: Test native lazy imports
run: >-
./mvnw --batch-mode --no-snapshot-updates
-pl modules/openapi-generator -am test
-Dtest=PythonClientCodegenTest
-Dsurefire.failIfNoSpecifiedTests=false
-DpythonExecutable="${{ steps.py.outputs.python-path }}"
1 change: 1 addition & 0 deletions docs/generators/python.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl
|generateSourceCodeOnly|Specifies that only a library source code is to be generated.| |false|
|hideGenerationTimestamp|Hides the generation timestamp when files are generated.| |true|
|lazyImports|Enable lazy imports.| |false|
|lazyNativeImport|Use native lazy imports for generated APIs and models. Requires Python 3.15 or newer and Pydantic 2.14 or newer. Lazy imports load a module on first use rather than when the import statement is executed. Large clients can contain many Pydantic models that an application never uses; avoiding their eager loading can reduce startup work and initial memory use. The cost moves to first use, import errors and module initialization side effects may occur later, and gains depend on which APIs and models are used. Cannot be combined with lazyImports=true. Native imports are not guaranteed to use less memory than lazyImports.| |false|
|library|library template (sub-template) to use: asyncio, urllib3, httpx, httpx2| |urllib3|
|mapNumberTo|Map number to Union[StrictFloat, StrictInt], StrictFloat, float or Decimal.| |Union[StrictFloat, StrictInt]|
|packageName|python package name (convention: snake_case).| |openapi_client|
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ public class PythonClientCodegen extends AbstractPythonCodegen implements Codege
public static final String SET_ENSURE_ASCII_TO_FALSE = "setEnsureAsciiToFalse";
public static final String POETRY1_FALLBACK = "poetry1";
public static final String LAZY_IMPORTS = "lazyImports";
public static final String LAZY_NATIVE_IMPORT = "lazyNativeImport";
public static final String BUILD_SYSTEM = "buildSystem";
public static final String SUPPORT_HTTPX_SYNC = "supportHttpxSync";
public static final String USE_INDEPENDENT_IMPLICIT_CLIENTS = "useIndependentImplicitClients";
Expand Down Expand Up @@ -122,6 +123,7 @@ public class PythonClientCodegen extends AbstractPythonCodegen implements Codege
@Setter protected boolean setEnsureAsciiToFalse = false;
@Setter protected boolean useIndependentImplicitClients = false;
@Setter protected boolean compatibleWithPythonLegacy = false;
@Setter protected boolean lazyNativeImport = false;

private String sourceFolder;
private String testFolder;
Expand Down Expand Up @@ -231,6 +233,14 @@ public PythonClientCodegen() {
cliOptions.add(new CliOption(CodegenConstants.USE_ONEOF_DISCRIMINATOR_LOOKUP, CodegenConstants.USE_ONEOF_DISCRIMINATOR_LOOKUP_DESC).defaultValue("false"));
cliOptions.add(new CliOption(POETRY1_FALLBACK, "Fallback to formatting pyproject.toml to Poetry 1.x format."));
cliOptions.add(new CliOption(LAZY_IMPORTS, "Enable lazy imports.").defaultValue(Boolean.FALSE.toString()));
cliOptions.add(CliOption.newBoolean(LAZY_NATIVE_IMPORT,
"Use native lazy imports for generated APIs and models. Requires Python 3.15 or newer and Pydantic 2.14 or newer. " +
"Lazy imports load a module on first use rather than when the import statement is executed. " +
"Large clients can contain many Pydantic models that an application never uses; avoiding their eager loading " +
"can reduce startup work and initial memory use. The cost moves to first use, import errors and module " +
"initialization side effects may occur later, and gains depend on which APIs and models are used. " +
"Cannot be combined with lazyImports=true. Native imports are not guaranteed to use less memory than lazyImports.")
.defaultValue(Boolean.FALSE.toString()));
cliOptions.add(new CliOption(BUILD_SYSTEM, "Build system to use in pyproject.toml (setuptools, hatchling).").defaultValue("setuptools"));
cliOptions.add(new CliOption(CodegenConstants.SOURCE_FOLDER, "directory for generated python source code; 'src' for _src layout_, unset (or '.') for _flat layout_."));
cliOptions.add(CliOption.newBoolean(SUPPORT_HTTPX_SYNC, "Generate synchronous '_sync' variants of each API method (httpx and httpx2 libraries only). " +
Expand Down Expand Up @@ -372,6 +382,14 @@ public void processOpts() {
if (additionalProperties.containsKey(LAZY_IMPORTS)) {
additionalProperties.put(LAZY_IMPORTS, Boolean.valueOf(additionalProperties.get(LAZY_IMPORTS).toString()));
}
if (additionalProperties.containsKey(LAZY_NATIVE_IMPORT)) {
setLazyNativeImport(convertPropertyToBooleanAndWriteBack(LAZY_NATIVE_IMPORT));
} else {
additionalProperties.put(LAZY_NATIVE_IMPORT, lazyNativeImport);
}
if (lazyNativeImport && Boolean.TRUE.equals(additionalProperties.get(LAZY_IMPORTS))) {
throw new IllegalArgumentException("lazyNativeImport=true cannot be combined with lazyImports=true. Choose one lazy import strategy.");
}

if (additionalProperties.containsKey(COMPATIBLE_WITH_PYTHON_LEGACY)) {
setCompatibleWithPythonLegacy(convertPropertyToBooleanAndWriteBack(COMPATIBLE_WITH_PYTHON_LEGACY));
Expand Down Expand Up @@ -615,7 +633,7 @@ public String generatePackageName(String packageName) {

@Override
public String generatorLanguageVersion() {
return "3.11+";
return lazyNativeImport ? "3.15+" : "3.11+";
}

@Override
Expand Down Expand Up @@ -645,7 +663,16 @@ public OperationsMap postProcessOperationsWithModels(
objs.put("x-python-has-json-header", true);
}

return super.postProcessOperationsWithModels(objs, allModels);
OperationsMap result = super.postProcessOperationsWithModels(objs, allModels);
if (lazyNativeImport) {
for (Map<String, String> modelImport : result.getImports()) {
// Other imports in this list include stdlib and Pydantic types.
if (modelImport.get("import").startsWith("from " + packageName + ".models.")) {
modelImport.put("x-python-lazy-import", "true");
}
}
}
return result;
}

private void renameIndependentClientOperationMembers(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ For more information, please visit [{{{infoUrl}}}]({{{infoUrl}}})

Python {{{generatorLanguageVersion}}}

{{>lazy_configuration}}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: Native-enabled README output merges the final lazy-import paragraph with the following packaging paragraph because this partial is not followed by a blank line. Add an empty line after the partial so Markdown renders them separately.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At modules/openapi-generator/src/main/resources/python/README.mustache, line 23:

<comment>Native-enabled README output merges the final lazy-import paragraph with the following packaging paragraph because this partial is not followed by a blank line. Add an empty line after the partial so Markdown renders them separately.</comment>

<file context>
@@ -20,6 +20,7 @@ For more information, please visit [{{{infoUrl}}}]({{{infoUrl}}})
 
 Python {{{generatorLanguageVersion}}}
 
+{{>lazy_configuration}}
 {{#poetry1}}
 Packaging retains the deprecated `[tool.poetry.dev-dependencies]` table for
</file context>
Suggested change
{{>lazy_configuration}}
{{>lazy_configuration}}

{{#poetry1}}
Packaging retains the deprecated `[tool.poetry.dev-dependencies]` table for
compatibility with legacy Poetry 1 releases. Generate with `poetry1=false` to use
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ For more information, please visit [{{{infoUrl}}}]({{{infoUrl}}})

Python {{{generatorLanguageVersion}}}

{{>lazy_configuration}}
## Installation & Usage

This python library package is generated without supporting files like setup.py or requirements files
Expand All @@ -36,7 +37,7 @@ To be able to use it, you will need these dependencies in your own package that
* pem >= 19.3.0
* pycryptodome >= 3.9.0
{{/hasHttpSignatureMethods}}
* pydantic >= 2.11
* pydantic >= {{#lazyNativeImport}}2.14{{/lazyNativeImport}}{{^lazyNativeImport}}2.11{{/lazyNativeImport}}
* typing-extensions >= 4.7.1

## Getting Started
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ from typing import Any, Dict, List, Optional, Tuple, Union
from typing_extensions import Annotated

{{#imports}}
{{import}}
{{#x-python-lazy-import}}lazy {{/x-python-lazy-import}}{{import}}
{{/imports}}

from {{packageName}}.api_client import ApiClient, RequestSerialized
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
# import apis into api package
{{#apiInfo}}{{#apis}}from {{apiPackage}}.{{classFilename}} import {{classname}}
{{#apiInfo}}{{#apis}}{{#lazyNativeImport}}lazy {{/lazyNativeImport}}from {{apiPackage}}.{{classFilename}} import {{classname}}
{{/apis}}{{/apiInfo}}
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
# import models into model package
{{#models}}{{#model}}from {{modelPackage}}.{{classFilename}} import {{classname}}
{{#models}}{{#model}}{{#lazyNativeImport}}lazy {{/lazyNativeImport}}from {{modelPackage}}.{{classFilename}} import {{classname}}
{{/model}}{{/models}}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# import apis into sdk package
{{#apiInfo}}{{#apis}}from {{apiPackage}}.{{classFilename}} import {{classname}} as {{classname}}
{{#apiInfo}}{{#apis}}{{#lazyNativeImport}}lazy {{/lazyNativeImport}}from {{apiPackage}}.{{classFilename}} import {{classname}} as {{classname}}
{{/apis}}{{/apiInfo}}
# import ApiClient
from {{packageName}}.api_response import ApiResponse as ApiResponse
Expand All @@ -16,5 +16,5 @@ from {{packageName}}.signing import HttpSigningConfiguration as HttpSigningConfi
{{/hasHttpSignatureMethods}}

# import models into sdk package
{{#models}}{{#model}}from {{modelPackage}}.{{classFilename}} import {{classname}} as {{classname}}
{{#models}}{{#model}}{{#lazyNativeImport}}lazy {{/lazyNativeImport}}from {{modelPackage}}.{{classFilename}} import {{classname}} as {{classname}}
{{/model}}{{/models}}
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.11", "3.12", "3.13", "3.14", "3.15"]
python-version: [<%^lazyNativeImport%>"3.11", "3.12", "3.13", "3.14", <%/lazyNativeImport%>"3.15"]

steps:
- uses: actions/checkout@v4
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ stages:
- pip install -r test-requirements.txt
- pytest --cov={{{packageName}}}

{{^lazyNativeImport}}
pytest-3.11:
extends: .pytest
image: python:3.11-alpine
Expand All @@ -26,6 +27,7 @@ pytest-3.13:
pytest-3.14:
extends: .pytest
image: python:3.14-alpine
{{/lazyNativeImport}}
pytest-3.15:
extends: .pytest
image: python:3.15-alpine
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{{#lazyNativeImport}}
### Native lazy imports

This client was generated with `lazyNativeImport=true` and requires Python 3.15+
and Pydantic 2.14+. Its generated API and model imports use `lazy from`: modules
load on first use, so applications that use only a subset of a large client's
Pydantic models can avoid loading unused modules and reduce startup work and
initial memory use.

Import costs move to first use; import failures and module initialization side
effects can happen later. Evaluating type annotations, constructing schemas, or
using models can resolve lazy imports. Gains depend on the workload, and native
imports are not guaranteed to use less memory than `lazyImports=true`.
The two import options cannot be enabled together.
{{/lazyNativeImport}}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import re # noqa: F401
{{{.}}}
{{/vendorExtensions.x-py-other-imports}}
{{#vendorExtensions.x-py-model-imports}}
{{{.}}}
{{#lazyNativeImport}}lazy {{/lazyNativeImport}}{{{.}}}
{{/vendorExtensions.x-py-model-imports}}
from typing import Union, Any, List, Set, TYPE_CHECKING, Optional, Dict
from typing_extensions import Literal, Self
Expand Down Expand Up @@ -220,7 +220,7 @@ class {{classname}}({{#parent}}{{{.}}}{{/parent}}{{^parent}}BaseModel{{/parent}}

{{#vendorExtensions.x-py-postponed-model-imports.size}}
{{#vendorExtensions.x-py-postponed-model-imports}}
{{{.}}}
{{#lazyNativeImport}}lazy {{/lazyNativeImport}}{{{.}}}
{{/vendorExtensions.x-py-postponed-model-imports}}
# TODO: Rewrite to not use raise_errors
{{classname}}.model_rebuild(raise_errors=False)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import json
{{{.}}}
{{/vendorExtensions.x-py-other-imports}}
{{#vendorExtensions.x-py-model-imports}}
{{{.}}}
{{#lazyNativeImport}}lazy {{/lazyNativeImport}}{{{.}}}
{{/vendorExtensions.x-py-model-imports}}
from typing import Optional, Set
from typing_extensions import Self
Expand Down Expand Up @@ -660,7 +660,7 @@ https://github.com/OpenAPITools/openapi-generator/blob/c84b949df1a9ec04ba75989cb

{{#vendorExtensions.x-py-postponed-model-imports.size}}
{{#vendorExtensions.x-py-postponed-model-imports}}
{{{.}}}
{{#lazyNativeImport}}lazy {{/lazyNativeImport}}{{{.}}}
{{/vendorExtensions.x-py-postponed-model-imports}}
# TODO: Rewrite to not use raise_errors
{{classname}}.model_rebuild(raise_errors=False)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import pprint
{{{.}}}
{{/vendorExtensions.x-py-other-imports}}
{{#vendorExtensions.x-py-model-imports}}
{{{.}}}
{{#lazyNativeImport}}lazy {{/lazyNativeImport}}{{{.}}}
{{/vendorExtensions.x-py-model-imports}}
from pydantic import StrictStr, Field
from typing import Union, List, Set, Optional, Dict
Expand Down Expand Up @@ -248,7 +248,7 @@ class {{classname}}({{#parent}}{{{.}}}{{/parent}}{{^parent}}BaseModel{{/parent}}

{{#vendorExtensions.x-py-postponed-model-imports.size}}
{{#vendorExtensions.x-py-postponed-model-imports}}
{{{.}}}
{{#lazyNativeImport}}lazy {{/lazyNativeImport}}{{{.}}}
{{/vendorExtensions.x-py-postponed-model-imports}}
# TODO: Rewrite to not use raise_errors
{{classname}}.model_rebuild(raise_errors=False)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ packages = [
include = ["{{#sourceFolder}}{{{sourceFolder}}}/{{/sourceFolder}}{{packageName}}/py.typed"]

[tool.poetry.dependencies]
python = "^3.11"
python = "^{{#lazyNativeImport}}3.15{{/lazyNativeImport}}{{^lazyNativeImport}}3.11{{/lazyNativeImport}}"
{{^async}}
urllib3 = ">= 2.7.0, < 3.0.0"
{{/async}}
Expand All @@ -56,11 +56,11 @@ httpx2 = ">= 2.13.0, < 3"
pem = ">= 19.3.0"
pycryptodome = ">= 3.9.0"
{{/hasHttpSignatureMethods}}
pydantic = ">= 2.11"
pydantic = ">= {{#lazyNativeImport}}2.14{{/lazyNativeImport}}{{^lazyNativeImport}}2.11{{/lazyNativeImport}}"
typing-extensions = ">= 4.7.1"
{{/poetry1}}
{{^poetry1}}
requires-python = ">=3.11"
requires-python = ">={{#lazyNativeImport}}3.15{{/lazyNativeImport}}{{^lazyNativeImport}}3.11{{/lazyNativeImport}}"

dependencies = [
{{^async}}
Expand All @@ -81,7 +81,7 @@ dependencies = [
"pem (>=19.3.0)",
"pycryptodome (>=3.9.0)",
{{/hasHttpSignatureMethods}}
"pydantic (>=2.11)",
"pydantic (>={{#lazyNativeImport}}2.14{{/lazyNativeImport}}{{^lazyNativeImport}}2.11{{/lazyNativeImport}})",
"typing-extensions (>=4.7.1)",
]

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,5 @@ httpx2 >= 2.13.0, < 3
pem >= 19.3.0
pycryptodome >= 3.9.0
{{/hasHttpSignatureMethods}}
pydantic >= 2.11
pydantic >= {{#lazyNativeImport}}2.14{{/lazyNativeImport}}{{^lazyNativeImport}}2.11{{/lazyNativeImport}}
typing-extensions >= 4.7.1
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ from setuptools import setup, find_packages # noqa: H301
# http://pypi.python.org/pypi/setuptools
NAME = "{{{projectName}}}"
VERSION = "{{packageVersion}}"
PYTHON_REQUIRES = ">= 3.11"
PYTHON_REQUIRES = ">= {{#lazyNativeImport}}3.15{{/lazyNativeImport}}{{^lazyNativeImport}}3.11{{/lazyNativeImport}}"
REQUIRES = [
{{^async}}
"urllib3 >= 2.7.0, < 3.0.0",
Expand All @@ -30,7 +30,7 @@ REQUIRES = [
"pem >= 19.3.0",
"pycryptodome >= 3.9.0",
{{/hasHttpSignatureMethods}}
"pydantic >= 2.11",
"pydantic >= {{#lazyNativeImport}}2.14{{/lazyNativeImport}}{{^lazyNativeImport}}2.11{{/lazyNativeImport}}",
"typing-extensions >= 4.7.1",
]

Expand All @@ -43,6 +43,9 @@ setup(
url="{{packageUrl}}",
keywords=["OpenAPI", "OpenAPI-Generator", "{{{appName}}}"],
install_requires=REQUIRES,
{{#lazyNativeImport}}
python_requires=PYTHON_REQUIRES,
{{/lazyNativeImport}}
{{#sourceFolder}}
package_dir={"": "{{{sourceFolder}}}"},
{{/sourceFolder}}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
# ref: https://docs.travis-ci.com/user/languages/python
language: python
python:
{{^lazyNativeImport}}
- "3.11"
- "3.12"
- "3.13"
- "3.14"
{{/lazyNativeImport}}
- "3.15"
# uncomment the following if needed
#- "3.14-dev" # 3.14 development branch
Expand Down
Loading
Loading