Skip to content
Merged
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
4 changes: 4 additions & 0 deletions .github/workflows/samples-python-client-echo-api.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,7 @@ jobs:
- name: Test
working-directory: ${{ matrix.sample }}
run: python -m pytest

- name: mypy
working-directory: ${{ matrix.sample }}
run: python -m mypy
4 changes: 4 additions & 0 deletions .github/workflows/samples-python-petstore.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,7 @@ jobs:
- name: Test
working-directory: ${{ matrix.sample }}
run: poetry run pytest -v

- name: mypy
working-directory: ${{ matrix.sample }}
run: poetry run mypy
6 changes: 6 additions & 0 deletions bin/configs/python-aiohttp.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,9 @@ library: asyncio
additionalProperties:
packageName: petstore_api
mapNumberTo: float
nameMappings:
_type: underscore_type
type_: type_with_underscore
modelNameMappings:
# The OpenAPI spec ApiResponse conflicts with the internal ApiResponse
ApiResponse: ModelApiResponse
3 changes: 3 additions & 0 deletions bin/configs/python.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,6 @@ additionalProperties:
nameMappings:
_type: underscore_type
type_: type_with_underscore
modelNameMappings:
# The OpenAPI spec ApiResponse conflicts with the internal ApiResponse
ApiResponse: ModelApiResponse
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,7 @@
import warnings
from pydantic import validate_call, Field, StrictFloat, StrictStr, StrictInt
from typing import Any, Dict, List, Optional, Tuple, Union

try:
from typing import Annotated
except ImportError:
from typing_extensions import Annotated
from typing_extensions import Annotated
Copy link
Contributor Author

Choose a reason for hiding this comment

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

mypy doesn't like the try/except form and after re-reading the typing_extensions documentation, the module already does the pass through to the stdlib if the type is implemented there, so we don't need to do this try/import here actually.


{{#imports}}
{{import}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -280,15 +280,13 @@ class ApiClient:
)

except ApiException as e:
if e.body:
e.body = e.body.decode('utf-8')
raise e

return response_data

def response_deserialize(
self,
response_data: rest.RESTResponse = None,
response_data: rest.RESTResponse,
response_types_map=None
) -> ApiResponse:
"""Deserializes response into an object.
Expand All @@ -297,6 +295,8 @@ class ApiClient:
:return: ApiResponse
"""

msg = "RESTResponse.read() must be called before passing it to response_deserialize()"
assert response_data.data is not None, msg

response_type = response_types_map.get(str(response_data.status), None)
if not response_type and isinstance(response_data.status, int) and 100 <= response_data.status <= 599:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
"""API response object."""

from __future__ import annotations
from typing import Dict, Optional, Generic, TypeVar
from pydantic import Field, StrictInt, StrictStr, StrictBytes, BaseModel
from typing import Optional, Generic, Mapping, TypeVar
from pydantic import Field, StrictInt, StrictBytes, BaseModel

T = TypeVar("T")

Expand All @@ -12,7 +12,7 @@ class ApiResponse(BaseModel, Generic[T]):
"""

status_code: StrictInt = Field(description="HTTP status code")
headers: Optional[Dict[StrictStr, StrictStr]] = Field(None, description="HTTP headers")
headers: Optional[Mapping[str, str]] = Field(None, description="HTTP headers")
data: T = Field(description="Deserialized data given the data type")
raw_data: StrictBytes = Field(description="Raw data (HTTP response body)")

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import io
import json
import re
import ssl
from typing import Optional

import aiohttp
import aiohttp_retry
Expand Down Expand Up @@ -72,6 +73,7 @@ class RESTClientObject:
)

retries = configuration.retries
self.retry_client: Optional[aiohttp_retry.RetryClient]
Copy link
Contributor Author

Choose a reason for hiding this comment

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

The retry client can be None just below.

if retries is not None:
self.retry_client = aiohttp_retry.RetryClient(
client_session=self.pool_manager,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

{{>partial_header}}
from typing import Any, Optional

from typing_extensions import Self

class OpenApiException(Exception):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import re # noqa: F401
{{/vendorExtensions.x-py-model-imports}}
from typing import Union, Any, List, TYPE_CHECKING, Optional, Dict
from typing_extensions import Literal, Self
from pydantic import Field

{{#lambda.uppercase}}{{{classname}}}{{/lambda.uppercase}}_ANY_OF_SCHEMAS = [{{#anyOf}}"{{.}}"{{^-last}}, {{/-last}}{{/anyOf}}]

Expand All @@ -27,7 +28,7 @@ class {{classname}}({{#parent}}{{{.}}}{{/parent}}{{^parent}}BaseModel{{/parent}}
actual_instance: Optional[Union[{{#anyOf}}{{{.}}}{{^-last}}, {{/-last}}{{/anyOf}}]] = None
else:
actual_instance: Any = None
any_of_schemas: List[str] = Literal[{{#lambda.uppercase}}{{{classname}}}{{/lambda.uppercase}}_ANY_OF_SCHEMAS]
any_of_schemas: List[str] = Field(default=Literal[{{#anyOf}}"{{.}}"{{^-last}}, {{/-last}}{{/anyOf}}])

model_config = {
"validate_assignment": True,
Expand Down Expand Up @@ -153,22 +154,20 @@ class {{classname}}({{#parent}}{{{.}}}{{/parent}}{{^parent}}BaseModel{{/parent}}
if self.actual_instance is None:
return "null"

to_json = getattr(self.actual_instance, "to_json", None)
if callable(to_json):
if hasattr(self.actual_instance, "to_json") and callable(self.actual_instance.to_json):
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is the same construct as before, but actually "types" correctly (this effectively bypasses all the typing information though :D )

return self.actual_instance.to_json()
else:
return json.dumps(self.actual_instance)

def to_dict(self) -> Dict:
def to_dict(self) -> Optional[Union[Dict, {{#anyOf}}{{.}}{{^-last}}, {{/-last}}{{/anyOf}}]]:
Copy link
Contributor Author

Choose a reason for hiding this comment

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

The result type is a bit complicated here:

  • The goal is to return a dict
  • But it can also return None (cf. 3 lines below)
  • Or the actual instance value, a few more lines below too.

"""Returns the dict representation of the actual instance"""
if self.actual_instance is None:
return "null"
return None

to_json = getattr(self.actual_instance, "to_json", None)
if callable(to_json):
if hasattr(self.actual_instance, "to_dict") and callable(self.actual_instance.to_dict):
return self.actual_instance.to_dict()
else:
return json.dumps(self.actual_instance)
return self.actual_instance
Comment on lines +165 to +170
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 think this was a mistake, duplicated code from the to_json() method above.

I fixed this to be the same code as the model_oneof template.


def to_str(self) -> str:
"""Returns the string representation of the actual instance"""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,8 @@ import json
{{#vendorExtensions.x-py-model-imports}}
{{{.}}}
{{/vendorExtensions.x-py-model-imports}}
try:
from typing import Self
except ImportError:
from typing_extensions import Self
from typing import Optional, Set
from typing_extensions import Self

class {{classname}}({{#parent}}{{{.}}}{{/parent}}{{^parent}}BaseModel{{/parent}}):
"""
Expand Down Expand Up @@ -87,15 +85,15 @@ class {{classname}}({{#parent}}{{{.}}}{{/parent}}{{^parent}}BaseModel{{/parent}}
{{#hasChildren}}
{{#discriminator}}
# JSON field name that stores the object type
__discriminator_property_name: ClassVar[List[str]] = '{{discriminator.propertyBaseName}}'
__discriminator_property_name: ClassVar[str] = '{{discriminator.propertyBaseName}}'
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Typing was wrong here


# discriminator mappings
__discriminator_value_class_map: ClassVar[Dict[str, str]] = {
{{#mappedModels}}'{{{mappingName}}}': '{{{modelName}}}'{{^-last}},{{/-last}}{{/mappedModels}}
}

@classmethod
def get_discriminator_value(cls, obj: Dict) -> str:
def get_discriminator_value(cls, obj: Dict) -> Optional[str]:
"""Returns the discriminator value (object type) of the data"""
discriminator_value = obj[cls.__discriminator_property_name]
if discriminator_value:
Expand All @@ -115,7 +113,7 @@ class {{classname}}({{#parent}}{{{.}}}{{/parent}}{{^parent}}BaseModel{{/parent}}
return json.dumps(self.to_dict())

@classmethod
def from_json(cls, json_str: str) -> {{^hasChildren}}Self{{/hasChildren}}{{#hasChildren}}{{#discriminator}}Union[{{#children}}Self{{^-last}}, {{/-last}}{{/children}}]{{/discriminator}}{{^discriminator}}Self{{/discriminator}}{{/hasChildren}}:
def from_json(cls, json_str: str) -> Optional[{{^hasChildren}}Self{{/hasChildren}}{{#hasChildren}}{{#discriminator}}Union[{{#children}}Self{{^-last}}, {{/-last}}{{/children}}]{{/discriminator}}{{^discriminator}}Self{{/discriminator}}{{/hasChildren}}]:
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 changed the signature to follow te signature of from_dict() the method calls.

"""Create an instance of {{{classname}}} from a JSON string"""
return cls.from_dict(json.loads(json_str))

Expand All @@ -135,16 +133,18 @@ class {{classname}}({{#parent}}{{{.}}}{{/parent}}{{^parent}}BaseModel{{/parent}}
* Fields in `self.additional_properties` are added to the output dict.
{{/isAdditionalPropertiesTrue}}
"""
excluded_fields: Set[str] = set([
{{#vendorExtensions.x-py-readonly}}
"{{{.}}}",
{{/vendorExtensions.x-py-readonly}}
{{#isAdditionalPropertiesTrue}}
"additional_properties",
{{/isAdditionalPropertiesTrue}}
])

_dict = self.model_dump(
by_alias=True,
exclude={
{{#vendorExtensions.x-py-readonly}}
"{{{.}}}",
{{/vendorExtensions.x-py-readonly}}
{{#isAdditionalPropertiesTrue}}
"additional_properties",
{{/isAdditionalPropertiesTrue}}
},
exclude=excluded_fields,
Comment on lines +136 to +147
Copy link
Contributor Author

@multani multani Jan 2, 2024

Choose a reason for hiding this comment

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

There were several bugs here:

If the value passed to exclude was empty (exclude={}), then:

  • mypy complained about a invalid type dict(unknown, unknown)
  • The value was effectively an empty dict, instead of an empty set

I took a simpler way by setting the value outside, "forcing" it's type and passing it down to the method call.

exclude_none=True,
)
{{#allVars}}
Expand Down Expand Up @@ -234,10 +234,10 @@ class {{classname}}({{#parent}}{{{.}}}{{/parent}}{{^parent}}BaseModel{{/parent}}
{{/allVars}}
return _dict

{{#hasChildren}}
@classmethod
def from_dict(cls, obj: Dict) -> {{^hasChildren}}Self{{/hasChildren}}{{#hasChildren}}{{#discriminator}}Union[{{#children}}Self{{^-last}}, {{/-last}}{{/children}}]{{/discriminator}}{{^discriminator}}Self{{/discriminator}}{{/hasChildren}}:
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 split this long signature into 2 parts:

  • sometimes, it can take an optional obj parameter
  • sometimes not

def from_dict(cls, obj: Dict) -> Optional[{{#discriminator}}Union[{{#children}}Self{{^-last}}, {{/-last}}{{/children}}]{{/discriminator}}{{^discriminator}}Self{{/discriminator}}]:
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Optional is not 100% needed here, but there are some code that have hasChilden but not discriminator and effectively don't have a from_dict() function body, for instance:

@classmethod
def from_dict(cls, obj: Dict) -> Self:
"""Create an instance of Query from a dict"""

I'm not sure how to properly fix this though.

"""Create an instance of {{{classname}}} from a dict"""
{{#hasChildren}}
{{#discriminator}}
# look up the object type based on discriminator mapping
object_type = cls.get_discriminator_value(obj)
Expand All @@ -249,8 +249,11 @@ class {{classname}}({{#parent}}{{{.}}}{{/parent}}{{^parent}}BaseModel{{/parent}}
json.dumps(obj) + ". Discriminator property name: " + cls.__discriminator_property_name +
", mapping: " + json.dumps(cls.__discriminator_value_class_map))
{{/discriminator}}
{{/hasChildren}}
{{^hasChildren}}
{{/hasChildren}}
{{^hasChildren}}
@classmethod
def from_dict(cls, obj: Optional[Dict]) -> Optional[Self]:
"""Create an instance of {{{classname}}} from a dict"""
if obj is None:
return None

Expand All @@ -277,7 +280,7 @@ class {{classname}}({{#parent}}{{{.}}}{{/parent}}{{^parent}}BaseModel{{/parent}}
{{^items.items.isPrimitiveType}}
"{{{baseName}}}": [
[{{{items.items.dataType}}}.from_dict(_inner_item) for _inner_item in _item]
for _item in obj.get("{{{baseName}}}")
for _item in obj["{{{baseName}}}"]
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 changed those because the value is actually checked just after and .get() returns an Optional that is not iterable by default.

] if obj.get("{{{baseName}}}") is not None else None{{^-last}},{{/-last}}
{{/items.items.isPrimitiveType}}
{{/items.isArray}}
Expand All @@ -287,7 +290,7 @@ class {{classname}}({{#parent}}{{{.}}}{{/parent}}{{^parent}}BaseModel{{/parent}}
"{{{baseName}}}": obj.get("{{{baseName}}}"){{^-last}},{{/-last}}
{{/items.isEnumOrRef}}
{{^items.isEnumOrRef}}
"{{{baseName}}}": [{{{items.dataType}}}.from_dict(_item) for _item in obj.get("{{{baseName}}}")] if obj.get("{{{baseName}}}") is not None else None{{^-last}},{{/-last}}
"{{{baseName}}}": [{{{items.dataType}}}.from_dict(_item) for _item in obj["{{{baseName}}}"]] if obj.get("{{{baseName}}}") is not None else None{{^-last}},{{/-last}}
{{/items.isEnumOrRef}}
{{/items.isPrimitiveType}}
{{#items.isPrimitiveType}}
Expand Down Expand Up @@ -320,14 +323,14 @@ class {{classname}}({{#parent}}{{{.}}}{{/parent}}{{^parent}}BaseModel{{/parent}}
if _v is not None
else None
)
for _k, _v in obj.get("{{{baseName}}}").items()
for _k, _v in obj.get("{{{baseName}}}", {}).items()
Copy link
Contributor Author

Choose a reason for hiding this comment

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

.get() returns None if the value is not there, I supposed the code meant to iterate over an empty dict instead.

){{^-last}},{{/-last}}
{{/items.isArray}}
{{/items.isContainer}}
{{^items.isContainer}}
"{{{baseName}}}": dict(
(_k, {{{items.dataType}}}.from_dict(_v))
for _k, _v in obj.get("{{{baseName}}}").items()
for _k, _v in obj["{{{baseName}}}"].items()
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Same as above: .get() is not needed here as the value is checked.

)
if obj.get("{{{baseName}}}") is not None
else None{{^-last}},{{/-last}}
Expand All @@ -345,7 +348,7 @@ class {{classname}}({{#parent}}{{{.}}}{{/parent}}{{^parent}}BaseModel{{/parent}}
{{^isContainer}}
{{^isPrimitiveType}}
{{^isEnumOrRef}}
"{{{baseName}}}": {{{dataType}}}.from_dict(obj.get("{{{baseName}}}")) if obj.get("{{{baseName}}}") is not None else None{{^-last}},{{/-last}}
"{{{baseName}}}": {{{dataType}}}.from_dict(obj["{{{baseName}}}"]) if obj.get("{{{baseName}}}") is not None else None{{^-last}},{{/-last}}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Same as above: .get() is not needed here as the value is checked.

{{/isEnumOrRef}}
{{#isEnumOrRef}}
"{{{baseName}}}": obj.get("{{{baseName}}}"){{^-last}},{{/-last}}
Expand All @@ -370,7 +373,7 @@ class {{classname}}({{#parent}}{{{.}}}{{/parent}}{{^parent}}BaseModel{{/parent}}

{{/isAdditionalPropertiesTrue}}
return _obj
{{/hasChildren}}
{{/hasChildren}}

{{#vendorExtensions.x-py-postponed-model-imports.size}}
{{#vendorExtensions.x-py-postponed-model-imports}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ class {{classname}}({{#parent}}{{{.}}}{{/parent}}{{^parent}}BaseModel{{/parent}}
{{vendorExtensions.x-py-name}}: {{{vendorExtensions.x-py-typing}}}
{{/composedSchemas.oneOf}}
actual_instance: Optional[Union[{{#oneOf}}{{{.}}}{{^-last}}, {{/-last}}{{/oneOf}}]] = None
one_of_schemas: List[str] = Literal[{{#oneOf}}"{{.}}"{{^-last}}, {{/-last}}{{/oneOf}}]
one_of_schemas: List[str] = Field(default=Literal[{{#oneOf}}"{{.}}"{{^-last}}, {{/-last}}{{/oneOf}}])

model_config = {
"validate_assignment": True,
Expand Down Expand Up @@ -175,19 +175,17 @@ class {{classname}}({{#parent}}{{{.}}}{{/parent}}{{^parent}}BaseModel{{/parent}}
if self.actual_instance is None:
return "null"

to_json = getattr(self.actual_instance, "to_json", None)
if callable(to_json):
if hasattr(self.actual_instance, "to_json") and callable(self.actual_instance.to_json):
return self.actual_instance.to_json()
else:
return json.dumps(self.actual_instance)

def to_dict(self) -> Dict:
def to_dict(self) -> Optional[Union[Dict, {{#oneOf}}{{{.}}}{{^-last}}, {{/-last}}{{/oneOf}}]]:
"""Returns the dict representation of the actual instance"""
if self.actual_instance is None:
return None

to_dict = getattr(self.actual_instance, "to_dict", None)
if callable(to_dict):
if hasattr(self.actual_instance, "to_dict") and callable(self.actual_instance.to_dict):
return self.actual_instance.to_dict()
else:
# primitive type
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,20 @@ typing-extensions = ">=4.7.1"
pytest = ">=7.2.1"
tox = ">=3.9.0"
flake8 = ">=4.0.0"
types-python-dateutil = ">=2.8.19.14"
mypy = "1.4.1"


[build-system]
requires = ["setuptools"]
build-backend = "setuptools.build_meta"

[tool.pylint.'MESSAGES CONTROL']
extension-pkg-whitelist = "pydantic"

[tool.mypy]
files = [
"{{{packageName}}}",
#"test", # auto-generated tests
"tests", # hand-written tests
]
Loading