Skip to content

python: generate Pydantic v2 + typing complete code#16624

Merged
wing328 merged 32 commits intoOpenAPITools:masterfrom
multani:python-typing-1
Sep 28, 2023
Merged

python: generate Pydantic v2 + typing complete code#16624
wing328 merged 32 commits intoOpenAPITools:masterfrom
multani:python-typing-1

Conversation

@multani
Copy link
Contributor

@multani multani commented Sep 19, 2023

was: python: Generate fully described Python types

Warning

Sorry, it's a rather large change, but I think it all helps to produce better type information.

I'm looking for feedback to know if this is the good way to integrate this kind of change, and if this has a chance of being merged.

If needed, I can try to make this big change shorter by splitting this into multiple PRs; it may be harder to see the big picture with smaller PRs though.


The goal of this change is to replace the mypy incompatible types (conlist, conint, constr, etc.) by their more modern versions using Annotated types with Pydantic v2.

To do so, the code generator changes in several ways:

  • The generator introduces several new inner classes to help the code generation:
    • PythonType is a recursive object that hold the information about a particular type that the generator is trying to build
    • PydanticType, directly translated from the previous getPydanticType methods, creates PythonType instances from the OpenAPI specifications and produces the final Python types as strings.
    • An initial Imports class holds all the new Python's import statements that the generator produces along the way.

Via these new classes, the code generation gets improved by the following:

  • The string-based formater approach is now replaced by a tree of PythonType objects:
    • Each object can hold all its information: type constraints, metadata, etc.
    • By being a tree of itself, types can become naturally nested and allow to easily express complex types
  • Most of the code duplication between the 2 getPydanticType methods have been unified to remove duplication:
    • Each OpenAPI type definition has a matching OpenAPI type -> Python type translation method
    • Common type translations between CodegenParamerer and CodegenProperty have been unified
  • The new Imports class can be passed between calls to accumulate all the imports:
    • This removes several flags that were present only to track if something needed to be imported or not.
    • An instance of the class can be shared across multiple method calls to accumulate all the imports that have to be done
    • It reduces the amount of objects to carry around as it can hold any kind of imports.
    • Ultimately, it also allows to control better which Python resources are imported from which modules, and should help to reduce the number of unused imports in the generated code.

References I used to implement the new types:

Related:

Note that this improves only a subset of the code generator, but there are still several places that could be improved:

  • I think once generalized, usage of the Imports class instead of the numerous Set instances could help to track all the import dependencies, reduce the generated code (and unused imports) and the code generator itself.
  • A whole part of the code generator has not been touched and could benefit from the same kind of improvements.

PR checklist

  • Read the contribution guidelines.
  • Pull Request title clearly describes the work in the pull request and Pull Request description provides details about how to validate the work. Missing information here may result in delayed response from the community.
  • Run the following to build the project and update samples:
    ./mvnw clean package 
    ./bin/generate-samples.sh ./bin/configs/*.yaml
    ./bin/utils/export_docs_generators.sh
    
    Commit all changed files.
    This is important, as CI jobs will verify all generator outputs of your HEAD commit as it would merge with master.
    These must match the expectations made by your contribution.
    You may regenerate an individual generator by passing the relevant config(s) as an argument to the script, for example ./bin/generate-samples.sh bin/configs/java*.
    For Windows users, please run the script in Git BASH.
  • File the PR against the correct branch: master (upcoming 7.1.0 minor release - breaking changes with fallbacks), 8.0.x (breaking changes without fallbacks)
  • If your PR is targeting a particular programming language, @mention the technical committee members, so they are more likely to review the pull request.

@krjakbrjak as Python tech-comittee
@wing328 as the previous major editor of the code generator

@multani multani marked this pull request as ready for review September 21, 2023 20:03
@multani
Copy link
Contributor Author

multani commented Sep 21, 2023

I removed the poetry.lock files I added during my experiment, and it divided the size of the pull request by 2 :D

@wing328
Copy link
Member

wing328 commented Sep 22, 2023

cc @OpenAPITools/generator-core-team

Copy link
Contributor

@robertschweizer robertschweizer left a comment

Choose a reason for hiding this comment

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

Thanks a lot for the PR! I checked some of the generated classes.

"""
# data type: List[int]
oneof_schema_1_validator: Optional[conlist(conint(strict=True, le=255, ge=0), max_items=3, min_items=3)] = Field(None, description="RGB three element array with values 0-255.")
oneof_schema_1_validator: Optional[Annotated[List[Annotated[int, Field(le=255, strict=True, ge=0)]], Field(min_items=3, max_items=3)]] = Field(default=None, description="RGB three element array with values 0-255.")
Copy link
Contributor

Choose a reason for hiding this comment

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

I do not get an error if I construct this with oneof_schema_1_validator=[1, 2]. Maybe pydantic is thrown off by Field() being used on both sides of =? Also pydantic might not take into account Field() restrictions in the first argument of Annotated[].

Best add a unit-test to make sure validation works, or is there one already?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I will check this 👍

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I added more tests: this works with Pydantic v2 but not with Pydantic v1. We'll focus on the former anyway and I'll merge my PR in a Pydantic v2-only generator 👍

Copy link
Contributor

Choose a reason for hiding this comment

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

This is great, thanks a lot for moving forward to pydantic v2! Then things work of course :)

@multani
Copy link
Contributor Author

multani commented Sep 22, 2023

Thanks to the merge of #16643, I have plenty of test failures now :)

I will turn this back to draft while I'm fixing the comments and the tests 👍

@multani multani marked this pull request as draft September 22, 2023 10:33
from pydantic import validate_arguments, ValidationError
{{#asyncio}}
from typing import overload, Optional, Union, Awaitable
{{/asyncio}}
Copy link
Member

Choose a reason for hiding this comment

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

why these lines are removed?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

2 reasons:

  • These imports are not asyncio-specific
  • The imported names are not used in the template

The names could be used by the code generated by the Java code, in which case the imports would be provided from the Java code instead.

Copy link
Member

Choose a reason for hiding this comment

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

👌


// import models one by one
if (!modelImports.isEmpty()) {
{
Copy link
Member

Choose a reason for hiding this comment

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

why removed the check here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I don't remember, I will add it back.

I need to change the place where x-py-model-imports (there could be resources to imports even if there no dependencies between models), I will move it outside the if block.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I re-added the condition and moved the otherImports.exports() imports out of the block.

@wing328 wing328 merged commit 04fa53b into OpenAPITools:master Sep 28, 2023
@multani multani deleted the python-typing-1 branch September 30, 2023 07:23
{{/composedSchemas.anyOf}}
if TYPE_CHECKING:
actual_instance: Union[{{#anyOf}}{{{.}}}{{^-last}}, {{/-last}}{{/anyOf}}]
actual_instance: Optional[Union[{{#anyOf}}{{{.}}}{{^-last}}, {{/-last}}{{/anyOf}}]] = None
Copy link

@b0g3r b0g3r Dec 12, 2023

Choose a reason for hiding this comment

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

@multani Hi Jonathan! Do you remember, by any chance, why did you introduce Optional here? Is it possible to have None value here even with one_of validator?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hey 👋 Sorry, I don't remember the details of this particular change :/ Does it create a problem for you?

Copy link

Choose a reason for hiding this comment

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

Yes, unfortunately. Before this changes non-optional anyOf had correct typing (Union[A, B]), but now it has additional None in the union, which isn't right and stops me from using proper typing. Should I try to change it back and create PR for it?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, of course, feel free to create a pull request!

I just checked again, and there's also a is None check in the to_dict method at the end of the file, it's probably why I added the Optional type in the first place. If you remove Optional, can you also remove that check?

multani added a commit to multani/openapi-generator that referenced this pull request Dec 31, 2023
In OpenAPITools#16624, I introduced a new mechanism to record imports to other
modules, instead of having specialized datetime/typing/pydantic objects
to manage imports for these modules.

This change reuses the mechanism from OpenAPITools#16624 and replace the specialized
import managers by the generic one. Unused imports from various
.mustache templates are also cleaned up.
@multani multani mentioned this pull request Dec 31, 2023
5 tasks
wing328 pushed a commit that referenced this pull request Jan 3, 2024
In #16624, I introduced a new mechanism to record imports to other
modules, instead of having specialized datetime/typing/pydantic objects
to manage imports for these modules.

This change reuses the mechanism from #16624 and replace the specialized
import managers by the generic one. Unused imports from various
.mustache templates are also cleaned up.
kaxil added a commit to astronomer/airflow that referenced this pull request May 19, 2025
kaxil added a commit to apache/airflow that referenced this pull request May 19, 2025
kaxil added a commit to apache/airflow that referenced this pull request May 19, 2025
kaxil added a commit to apache/airflow that referenced this pull request May 19, 2025
dadonnelly316 pushed a commit to dadonnelly316/airflow that referenced this pull request May 26, 2025
kaxil added a commit to apache/airflow that referenced this pull request Jun 3, 2025
sanederchik pushed a commit to sanederchik/airflow that referenced this pull request Jun 7, 2025
kosteev pushed a commit to GoogleCloudPlatform/composer-airflow that referenced this pull request Sep 25, 2025
Python Client now need pydantic as we use the new version of the OpenAPI python client generator: https://github.com/OpenAPITools/openapi-generator/blob/master/modules/openapi-generator/src/main/resources/python/requirements.mustache#L14

It was added in OpenAPITools/openapi-generator#16624

(cherry picked from commit d176113630d6b7d4bc36511fe3f8cb013060d675)

GitOrigin-RevId: 4e84838785f49f7d0a646018ba22992d82e65a76
kosteev pushed a commit to GoogleCloudPlatform/composer-airflow that referenced this pull request Oct 22, 2025
Python Client now need pydantic as we use the new version of the OpenAPI python client generator: https://github.com/OpenAPITools/openapi-generator/blob/master/modules/openapi-generator/src/main/resources/python/requirements.mustache#L14

It was added in OpenAPITools/openapi-generator#16624

GitOrigin-RevId: d176113630d6b7d4bc36511fe3f8cb013060d675
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants