-
-
Notifications
You must be signed in to change notification settings - Fork 6.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
python: generate Pydantic v2 + typing complete code #16624
Conversation
bdc8789
to
e067ce3
Compare
e067ce3
to
23617df
Compare
I removed the |
cc @OpenAPITools/generator-core-team |
There was a problem hiding this 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.
modules/openapi-generator/src/main/resources/python/requirements.mustache
Show resolved
Hide resolved
@@ -30,11 +32,11 @@ class Color(BaseModel): | |||
RGB array, RGBA array, or hex string. | |||
""" | |||
# 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.") |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I will check this 👍
There was a problem hiding this comment.
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 👍
There was a problem hiding this comment.
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 :)
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 👍 |
This is still not done, but the code seems to be working and this could be done in a 2nd step.
I "fixed" this by not calling
I "fixed" this by commenting the This is fixable, but in the general case, it's a bit hard:
It's not impossible, but a bit of work. I deferred that to later, there are
This is fixed now. I was using Pydantic's I think the generated could be simplified and improved by reusing more Pydantic methods instead of implementing our own. This could also be improved later on.
I think this could be looked at again once we have fixed all (most of) the warnings for Pydantic v2. There are still a lot of typing errors, but more than 60% should go away once the migration has progressed further. |
if (cp.hasValidation) { | ||
List<String> fieldCustomization = new ArrayList<>(); | ||
List<String> intFieldCustomization = new ArrayList<>(); | ||
public String toEnumVariableName(String name, String datatype) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is from old version, line 1615
return pt.getType(cp); | ||
} | ||
|
||
public void setMapNumberTo(String mapNumberTo) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is form old version, line 1605
LOGGER.warn("Codegen property is null (e.g. map/dict of undefined type). Default to typing.Any."); | ||
typingImports.add("Any"); | ||
return "Any"; | ||
void createImportMapOfSet(String modelName, Map<String, CodegenModel> codegenModelMap) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is from old version, line 1660
elif isinstance(obj, set): | ||
return {self.sanitize_for_serialization(sub_obj) | ||
for sub_obj in obj} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not useful for now. Annotated[List, Field(unique_items=True)]
should be replaced by Annotated[Set]
, but this has further implications (sets lose the ordering, not serializable as JSON automatically, etc.)
elif isinstance(obj, set): | |
return {self.sanitize_for_serialization(sub_obj) | |
for sub_obj in obj} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
thanks for the heads-up. later we can add some tests in https://github.com/OpenAPITools/openapi-generator/blob/master/samples/openapi3/client/petstore/python/tests/test_api_client.py#L159 to cover set
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually, I wanted to remove this block of code, as it's unused right now.
There's a comment in the related part of the Java code:
private PythonType arrayType(IJsonSchemaValidationProperties cp) {
PythonType pt = new PythonType();
if (cp.getMaxItems() != null) {
pt.constrain("max_items", cp.getMaxItems());
}
if (cp.getMinItems()!= null) {
pt.constrain("min_items", cp.getMinItems());
}
if (cp.getUniqueItems()) {
// A unique "array" is a set
// TODO: pydantic v2: Pydantic suggest to convert this to a set, but this has some implications:
// https://github.com/pydantic/pydantic-core/issues/296
// Also, having a set instead of list creates complications:
// random JSON serialization order, unable to easily serialize
// to JSON, etc.
//pt.setType("Set");
//typingImports.add("Set");
pt.setType("List");
typingImports.add("List");
} else {
pt.setType("List");
typingImports.add("List");
}
pt.addTypeParam(getType(cp.getItems()));
return pt;
}
modules/openapi-generator/src/main/resources/python/model_anyof.mustache
Outdated
Show resolved
Hide resolved
if (cp.baseName != null && !cp.baseName.equals(cp.name)) { // base name not the same as name | ||
pt.annotate("serialization_alias", cp.baseName); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This used to be alias
, but in Pydantic v2, alias
is both a validator and a serializer.
From the tests, I assume alias
was used for serialization only (to produce the aliased field), not for validation (we still want to be able to read the original Pythonic field)
Note that in Pydantic v1, both forms (the field name and the alias name) could be used to create a new instance of the model. This is not possible anymore, only the field name is accepted.
@@ -7,9 +7,6 @@ import io | |||
import warnings | |||
|
|||
from pydantic import validate_arguments, ValidationError | |||
{{#asyncio}} | |||
from typing import overload, Optional, Union, Awaitable | |||
{{/asyncio}} |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👌
@@ -1009,19 +997,27 @@ private ModelsMap postProcessModelsMap(ModelsMap objs) { | |||
model.getVendorExtensions().putIfAbsent("x-py-readonly", readOnlyFields); | |||
|
|||
// import models one by one | |||
if (!modelImports.isEmpty()) { | |||
{ |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
* python: improve type generation with more specific typing * Annotate function parameters * Remove unused imports * remove unused files * remove temporary hack * remove lock file * fix Annotated import * support Python 3.7 * Regenerate code with typing-extensions * Fix setup.py * More Pydantic v2 compatibility * depend on pydantic v2 * fix client_echo tests * fix JSON serialization * Fix references * Skip circular dependency tests for now * Temporarily hide the "float" property The "float" property aliases the "float" type and completely breaks the model: all the properties that were "float" now become the type of the "float" property instead. * Fix errors * Import Literal from typing_extensions * Fix GitHub Action workflows * Fix Python 3.7 failure * Fix quotes * Apply suggestions from code review * Fix tests * split model imports from other modules imports * fix workflow * Comment the array unique items convertion, remove set translation * Replace alias usage
@@ -24,16 +28,16 @@ class {{classname}}({{#parent}}{{{.}}}{{/parent}}{{^parent}}BaseModel{{/parent}} | |||
{{vendorExtensions.x-py-name}}: {{{vendorExtensions.x-py-typing}}} | |||
{{/composedSchemas.anyOf}} | |||
if TYPE_CHECKING: | |||
actual_instance: Union[{{#anyOf}}{{{.}}}{{^-last}}, {{/-last}}{{/anyOf}}] | |||
actual_instance: Optional[Union[{{#anyOf}}{{{.}}}{{^-last}}, {{/-last}}{{/anyOf}}]] = None |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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?
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.
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.
was:
python: Generate fully described Python types
The goal of this change is to replace the mypy incompatible types (
conlist
,conint
,constr
, etc.) by their more modern versions usingAnnotated
types with Pydantic v2.To do so, the code generator changes in several ways:
PythonType
is a recursive object that hold the information about a particular type that the generator is trying to buildPydanticType
, directly translated from the previousgetPydanticType
methods, createsPythonType
instances from the OpenAPI specifications and produces the final Python types as strings.Imports
class holds all the new Python'simport
statements that the generator produces along the way.Via these new classes, the code generation gets improved by the following:
PythonType
objects:getPydanticType
methods have been unified to remove duplication:OpenAPI type -> Python type
translation methodCodegenParamerer
andCodegenProperty
have been unifiedImports
class can be passed between calls to accumulate all the imports:References I used to implement the new types:
Annotated
: mypy annotation and constrained lists recommendation pydantic/pydantic#975)Related:
Note that this improves only a subset of the code generator, but there are still several places that could be improved:
Imports
class instead of the numerousSet
instances could help to track all the import dependencies, reduce the generated code (and unused imports) and the code generator itself.PR checklist
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.
master
(upcoming 7.1.0 minor release - breaking changes with fallbacks),8.0.x
(breaking changes without fallbacks)@krjakbrjak as Python tech-comittee
@wing328 as the previous major editor of the code generator