From dcdf59e4941bb25686892526276a8211670245b7 Mon Sep 17 00:00:00 2001 From: Emil Styrke Date: Mon, 10 Jun 2024 12:50:14 +0200 Subject: [PATCH 1/6] feat: add UUID format for string properties Used by FastAPI and possibly others --- end_to_end_tests/baseline_openapi_3.0.json | 18 ++++ end_to_end_tests/baseline_openapi_3.1.yaml | 24 ++++++ .../my_test_api_client/models/a_model.py | 50 +++++++++++ .../parser/properties/__init__.py | 12 ++- .../parser/properties/property.py | 2 + .../parser/properties/uuid.py | 82 +++++++++++++++++++ .../property_templates/uuid_property.py.jinja | 38 +++++++++ 7 files changed, 225 insertions(+), 1 deletion(-) create mode 100644 openapi_python_client/parser/properties/uuid.py create mode 100644 openapi_python_client/templates/property_templates/uuid_property.py.jinja diff --git a/end_to_end_tests/baseline_openapi_3.0.json b/end_to_end_tests/baseline_openapi_3.0.json index 468e3532d..6335d1483 100644 --- a/end_to_end_tests/baseline_openapi_3.0.json +++ b/end_to_end_tests/baseline_openapi_3.0.json @@ -1714,6 +1714,8 @@ "aCamelDateTime", "a_date", "a_nullable_date", + "a_uuid", + "a_nullable_uuid", "required_nullable", "required_not_nullable", "model", @@ -1784,6 +1786,22 @@ "type": "string", "format": "date" }, + "a_uuid": { + "title": "A Uuid", + "type": "string", + "format": "uuid" + }, + "a_nullable_uuid": { + "title": "A Nullable Uuid", + "type": "string", + "format": "uuid", + "nullable": true + }, + "a_not_required_uuid": { + "title": "A Not Required Uuid", + "type": "string", + "format": "uuid" + }, "1_leading_digit": { "title": "Leading Digit", "type": "string" diff --git a/end_to_end_tests/baseline_openapi_3.1.yaml b/end_to_end_tests/baseline_openapi_3.1.yaml index 5b01d9e29..f4dcf331f 100644 --- a/end_to_end_tests/baseline_openapi_3.1.yaml +++ b/end_to_end_tests/baseline_openapi_3.1.yaml @@ -1699,6 +1699,8 @@ info: "aCamelDateTime", "a_date", "a_nullable_date", + "a_uuid", + "a_nullable_uuid", "required_nullable", "required_not_nullable", "model", @@ -1771,6 +1773,28 @@ info: "type": "string", "format": "date" }, + "a_uuid": { + "title": "A Uuid", + "type": "string", + "format": "uuid" + }, + "a_nullable_uuid": { + "title": "A Nullable Uuid", + "anyOf": [ + { + "type": "string", + "format": "uuid" + }, + { + "type": "null" + } + ] + }, + "a_not_required_uuid": { + "title": "A Not Required Uuid", + "type": "string", + "format": "uuid" + }, "1_leading_digit": { "title": "Leading Digit", "type": "string" diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/a_model.py b/end_to_end_tests/golden-record/my_test_api_client/models/a_model.py index 054e6bfa7..4286a8239 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/a_model.py +++ b/end_to_end_tests/golden-record/my_test_api_client/models/a_model.py @@ -1,4 +1,5 @@ import datetime +import uuid from typing import TYPE_CHECKING, Any, Dict, List, Type, TypeVar, Union, cast from attrs import define as _attrs_define @@ -27,6 +28,8 @@ class AModel: a_camel_date_time (Union[datetime.date, datetime.datetime]): a_date (datetime.date): a_nullable_date (Union[None, datetime.date]): + a_uuid (uuid.UUID): + a_nullable_uuid (Union[None, uuid.UUID]): required_nullable (Union[None, str]): required_not_nullable (str): one_of_models (Union['FreeFormModel', 'ModelWithUnionProperty', Any]): @@ -37,6 +40,7 @@ class AModel: an_optional_allof_enum (Union[Unset, AnAllOfEnum]): nested_list_of_enums (Union[Unset, List[List[DifferentEnum]]]): a_not_required_date (Union[Unset, datetime.date]): + a_not_required_uuid (Union[Unset, uuid.UUID]): attr_1_leading_digit (Union[Unset, str]): attr_leading_underscore (Union[Unset, str]): not_required_nullable (Union[None, Unset, str]): @@ -51,6 +55,8 @@ class AModel: a_camel_date_time: Union[datetime.date, datetime.datetime] a_date: datetime.date a_nullable_date: Union[None, datetime.date] + a_uuid: uuid.UUID + a_nullable_uuid: Union[None, uuid.UUID] required_nullable: Union[None, str] required_not_nullable: str one_of_models: Union["FreeFormModel", "ModelWithUnionProperty", Any] @@ -62,6 +68,7 @@ class AModel: an_optional_allof_enum: Union[Unset, AnAllOfEnum] = UNSET nested_list_of_enums: Union[Unset, List[List[DifferentEnum]]] = UNSET a_not_required_date: Union[Unset, datetime.date] = UNSET + a_not_required_uuid: Union[Unset, uuid.UUID] = UNSET attr_1_leading_digit: Union[Unset, str] = UNSET attr_leading_underscore: Union[Unset, str] = UNSET not_required_nullable: Union[None, Unset, str] = UNSET @@ -93,6 +100,14 @@ def to_dict(self) -> Dict[str, Any]: else: a_nullable_date = self.a_nullable_date + a_uuid = str(self.a_uuid) + + a_nullable_uuid: Union[None, str] + if isinstance(self.a_nullable_uuid, uuid.UUID): + a_nullable_uuid = str(self.a_nullable_uuid) + else: + a_nullable_uuid = self.a_nullable_uuid + required_nullable: Union[None, str] required_nullable = self.required_nullable @@ -143,6 +158,10 @@ def to_dict(self) -> Dict[str, Any]: if not isinstance(self.a_not_required_date, Unset): a_not_required_date = self.a_not_required_date.isoformat() + a_not_required_uuid: Union[Unset, str] = UNSET + if not isinstance(self.a_not_required_uuid, Unset): + a_not_required_uuid = str(self.a_not_required_uuid) + attr_1_leading_digit = self.attr_1_leading_digit attr_leading_underscore = self.attr_leading_underscore @@ -193,6 +212,8 @@ def to_dict(self) -> Dict[str, Any]: "aCamelDateTime": a_camel_date_time, "a_date": a_date, "a_nullable_date": a_nullable_date, + "a_uuid": a_uuid, + "a_nullable_uuid": a_nullable_uuid, "required_nullable": required_nullable, "required_not_nullable": required_not_nullable, "one_of_models": one_of_models, @@ -209,6 +230,8 @@ def to_dict(self) -> Dict[str, Any]: field_dict["nested_list_of_enums"] = nested_list_of_enums if a_not_required_date is not UNSET: field_dict["a_not_required_date"] = a_not_required_date + if a_not_required_uuid is not UNSET: + field_dict["a_not_required_uuid"] = a_not_required_uuid if attr_1_leading_digit is not UNSET: field_dict["1_leading_digit"] = attr_1_leading_digit if attr_leading_underscore is not UNSET: @@ -272,6 +295,23 @@ def _parse_a_nullable_date(data: object) -> Union[None, datetime.date]: a_nullable_date = _parse_a_nullable_date(d.pop("a_nullable_date")) + a_uuid = uuid.UUID(d.pop("a_uuid")) + + def _parse_a_nullable_uuid(data: object) -> Union[None, uuid.UUID]: + if data is None: + return data + try: + if not isinstance(data, str): + raise TypeError() + a_nullable_uuid_type_0 = uuid.UUID(data) + + return a_nullable_uuid_type_0 + except: # noqa: E722 + pass + return cast(Union[None, uuid.UUID], data) + + a_nullable_uuid = _parse_a_nullable_uuid(d.pop("a_nullable_uuid")) + def _parse_required_nullable(data: object) -> Union[None, str]: if data is None: return data @@ -370,6 +410,13 @@ def _parse_nullable_model(data: object) -> Union["ModelWithUnionProperty", None] else: a_not_required_date = isoparse(_a_not_required_date).date() + _a_not_required_uuid = d.pop("a_not_required_uuid", UNSET) + a_not_required_uuid: Union[Unset, uuid.UUID] + if isinstance(_a_not_required_uuid, Unset): + a_not_required_uuid = UNSET + else: + a_not_required_uuid = uuid.UUID(_a_not_required_uuid) + attr_1_leading_digit = d.pop("1_leading_digit", UNSET) attr_leading_underscore = d.pop("_leading_underscore", UNSET) @@ -463,6 +510,8 @@ def _parse_not_required_nullable_model(data: object) -> Union["ModelWithUnionPro a_camel_date_time=a_camel_date_time, a_date=a_date, a_nullable_date=a_nullable_date, + a_uuid=a_uuid, + a_nullable_uuid=a_nullable_uuid, required_nullable=required_nullable, required_not_nullable=required_not_nullable, one_of_models=one_of_models, @@ -473,6 +522,7 @@ def _parse_not_required_nullable_model(data: object) -> Union["ModelWithUnionPro an_optional_allof_enum=an_optional_allof_enum, nested_list_of_enums=nested_list_of_enums, a_not_required_date=a_not_required_date, + a_not_required_uuid=a_not_required_uuid, attr_1_leading_digit=attr_1_leading_digit, attr_leading_underscore=attr_leading_underscore, not_required_nullable=not_required_nullable, diff --git a/openapi_python_client/parser/properties/__init__.py b/openapi_python_client/parser/properties/__init__.py index 02a6fdafe..723a882fa 100644 --- a/openapi_python_client/parser/properties/__init__.py +++ b/openapi_python_client/parser/properties/__init__.py @@ -44,11 +44,12 @@ ) from .string import StringProperty from .union import UnionProperty +from .uuid import UuidProperty def _string_based_property( name: str, required: bool, data: oai.Schema, config: Config -) -> StringProperty | DateProperty | DateTimeProperty | FileProperty | PropertyError: +) -> StringProperty | DateProperty | DateTimeProperty | FileProperty | UuidProperty | PropertyError: """Construct a Property from the type "string" """ string_format = data.schema_format python_name = utils.PythonIdentifier(value=name, prefix=config.field_prefix) @@ -79,6 +80,15 @@ def _string_based_property( description=data.description, example=data.example, ) + if string_format == "uuid": + return UuidProperty.build( + name=name, + required=required, + default=None, + python_name=python_name, + description=data.description, + example=data.example, + ) return StringProperty.build( name=name, default=data.default, diff --git a/openapi_python_client/parser/properties/property.py b/openapi_python_client/parser/properties/property.py index fa3a26beb..aeac32a7f 100644 --- a/openapi_python_client/parser/properties/property.py +++ b/openapi_python_client/parser/properties/property.py @@ -18,6 +18,7 @@ from .none import NoneProperty from .string import StringProperty from .union import UnionProperty +from .uuid import UuidProperty Property: TypeAlias = Union[ AnyProperty, @@ -34,4 +35,5 @@ NoneProperty, StringProperty, UnionProperty, + UuidProperty, ] diff --git a/openapi_python_client/parser/properties/uuid.py b/openapi_python_client/parser/properties/uuid.py new file mode 100644 index 000000000..1ba6dd822 --- /dev/null +++ b/openapi_python_client/parser/properties/uuid.py @@ -0,0 +1,82 @@ +from __future__ import annotations + +from typing import Any, ClassVar +from uuid import UUID + +from attr import define + +from ... import schema as oai +from ...utils import PythonIdentifier +from ..errors import PropertyError +from .protocol import PropertyProtocol, Value + + +@define +class UuidProperty(PropertyProtocol): + """A property of type uuid.UUID""" + + name: str + required: bool + default: Value | None + python_name: PythonIdentifier + description: str | None + example: str | None + + _type_string: ClassVar[str] = "uuid.UUID" + _json_type_string: ClassVar[str] = "str" + _allowed_locations: ClassVar[set[oai.ParameterLocation]] = { + oai.ParameterLocation.QUERY, + oai.ParameterLocation.PATH, + oai.ParameterLocation.COOKIE, + oai.ParameterLocation.HEADER, + } + template: ClassVar[str] = "uuid_property.py.jinja" + + @classmethod + def build( + cls, + name: str, + required: bool, + default: Any, + python_name: PythonIdentifier, + description: str | None, + example: str | None, + ) -> UuidProperty | PropertyError: + checked_default = cls.convert_value(default) + if isinstance(checked_default, PropertyError): + return checked_default + + return cls( + name=name, + required=required, + default=checked_default, + python_name=python_name, + description=description, + example=example, + ) + + @classmethod + def convert_value(cls, value: Any) -> Value | None | PropertyError: + if value is None or isinstance(value, Value): + return value + if isinstance(value, str): + try: + UUID(value) + except ValueError: + return PropertyError(f"Invalid UUID value: {value}") + return Value(value) + if isinstance(value, UUID): + return Value(str(value)) + return PropertyError(f"Invalid UUID value: {value}") + + def get_imports(self, *, prefix: str) -> set[str]: + """ + Get a set of import strings that should be included when this property is used somewhere + + Args: + prefix: A prefix to put before any relative (local) module names. This should be the number of . to get + back to the root of the generated client. + """ + imports = super().get_imports(prefix=prefix) + imports.update({"import uuid"}) + return imports diff --git a/openapi_python_client/templates/property_templates/uuid_property.py.jinja b/openapi_python_client/templates/property_templates/uuid_property.py.jinja new file mode 100644 index 000000000..c73a45586 --- /dev/null +++ b/openapi_python_client/templates/property_templates/uuid_property.py.jinja @@ -0,0 +1,38 @@ +{% macro construct_function(property, source) %} +uuid.UUID({{ source }}) +{% endmacro %} + +{% from "property_templates/property_macros.py.jinja" import construct_template %} + +{% macro construct(property, source) %} +{{ construct_template(construct_function, property, source) }} +{% endmacro %} + +{% macro check_type_for_construct(property, source) %}isinstance({{ source }}, str){% endmacro %} + +{% macro transform(property, source, destination, declare_type=True) %} +{% set transformed = "str(" + source + ")" %} +{% if property.required %} +{{ destination }} = {{ transformed }} +{%- else %} +{% if declare_type %} +{% set type_annotation = property.get_type_string(json=True) %} +{{ destination }}: {{ type_annotation }} = UNSET +{% else %} +{{ destination }} = UNSET +{% endif %} +if not isinstance({{ source }}, Unset): + {{ destination }} = {{ transformed }} +{%- endif %} +{% endmacro %} + +{% macro transform_multipart(property, source, destination) %} +{% if property.required %} +{{ destination }} = str({{ source }}) +{%- else %} +{% set type_annotation = property.get_type_string(json=True) | replace("str", "bytes") %} +{{ destination }}: {{ type_annotation }} = UNSET +if not isinstance({{ source }}, Unset): + {{ destination }} = str({{ source }}) +{%- endif %} +{% endmacro %} From ab7fa6b87a8505b0ec744c64b88b91a1da247221 Mon Sep 17 00:00:00 2001 From: Dylan Anthony Date: Sun, 20 Oct 2024 15:09:14 -0600 Subject: [PATCH 2/6] Fix uuid defaults --- end_to_end_tests/baseline_openapi_3.0.json | 3 ++- end_to_end_tests/baseline_openapi_3.1.yaml | 5 +++-- openapi_python_client/parser/properties/uuid.py | 4 +--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/end_to_end_tests/baseline_openapi_3.0.json b/end_to_end_tests/baseline_openapi_3.0.json index 6335d1483..e5bbaf6fc 100644 --- a/end_to_end_tests/baseline_openapi_3.0.json +++ b/end_to_end_tests/baseline_openapi_3.0.json @@ -1795,7 +1795,8 @@ "title": "A Nullable Uuid", "type": "string", "format": "uuid", - "nullable": true + "nullable": true, + "default": "07EF8B4D-AA09-4FFA-898D-C710796AFF41" }, "a_not_required_uuid": { "title": "A Not Required Uuid", diff --git a/end_to_end_tests/baseline_openapi_3.1.yaml b/end_to_end_tests/baseline_openapi_3.1.yaml index f4dcf331f..b6a6941e2 100644 --- a/end_to_end_tests/baseline_openapi_3.1.yaml +++ b/end_to_end_tests/baseline_openapi_3.1.yaml @@ -1783,12 +1783,13 @@ info: "anyOf": [ { "type": "string", - "format": "uuid" + "format": "uuid", }, { "type": "null" } - ] + ], + "default": "07EF8B4D-AA09-4FFA-898D-C710796AFF41" }, "a_not_required_uuid": { "title": "A Not Required Uuid", diff --git a/openapi_python_client/parser/properties/uuid.py b/openapi_python_client/parser/properties/uuid.py index 1ba6dd822..d0ea95f88 100644 --- a/openapi_python_client/parser/properties/uuid.py +++ b/openapi_python_client/parser/properties/uuid.py @@ -64,9 +64,7 @@ def convert_value(cls, value: Any) -> Value | None | PropertyError: UUID(value) except ValueError: return PropertyError(f"Invalid UUID value: {value}") - return Value(value) - if isinstance(value, UUID): - return Value(str(value)) + return Value(python_code=f"UUID('{value}')", raw_value=value) return PropertyError(f"Invalid UUID value: {value}") def get_imports(self, *, prefix: str) -> set[str]: From 1762d6ecab7bffddf46a761a66d0c4d98779b896 Mon Sep 17 00:00:00 2001 From: Dylan Anthony Date: Sun, 20 Oct 2024 15:09:35 -0600 Subject: [PATCH 3/6] Import `UUID` class instead of `uuid` module to reduce chance of conflict with property name --- .../my_test_api_client/models/a_model.py | 28 +++++------ .../my_test_api_client/models/extended.py | 50 +++++++++++++++++++ .../parser/properties/uuid.py | 4 +- .../property_templates/uuid_property.py.jinja | 2 +- 4 files changed, 67 insertions(+), 17 deletions(-) diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/a_model.py b/end_to_end_tests/golden-record/my_test_api_client/models/a_model.py index 4286a8239..a14400c9d 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/a_model.py +++ b/end_to_end_tests/golden-record/my_test_api_client/models/a_model.py @@ -1,6 +1,6 @@ import datetime -import uuid from typing import TYPE_CHECKING, Any, Dict, List, Type, TypeVar, Union, cast +from uuid import UUID from attrs import define as _attrs_define from dateutil.parser import isoparse @@ -28,8 +28,8 @@ class AModel: a_camel_date_time (Union[datetime.date, datetime.datetime]): a_date (datetime.date): a_nullable_date (Union[None, datetime.date]): - a_uuid (uuid.UUID): - a_nullable_uuid (Union[None, uuid.UUID]): + a_uuid (UUID): + a_nullable_uuid (Union[None, UUID]): Default: UUID('07EF8B4D-AA09-4FFA-898D-C710796AFF41'). required_nullable (Union[None, str]): required_not_nullable (str): one_of_models (Union['FreeFormModel', 'ModelWithUnionProperty', Any]): @@ -40,7 +40,7 @@ class AModel: an_optional_allof_enum (Union[Unset, AnAllOfEnum]): nested_list_of_enums (Union[Unset, List[List[DifferentEnum]]]): a_not_required_date (Union[Unset, datetime.date]): - a_not_required_uuid (Union[Unset, uuid.UUID]): + a_not_required_uuid (Union[Unset, UUID]): attr_1_leading_digit (Union[Unset, str]): attr_leading_underscore (Union[Unset, str]): not_required_nullable (Union[None, Unset, str]): @@ -55,8 +55,7 @@ class AModel: a_camel_date_time: Union[datetime.date, datetime.datetime] a_date: datetime.date a_nullable_date: Union[None, datetime.date] - a_uuid: uuid.UUID - a_nullable_uuid: Union[None, uuid.UUID] + a_uuid: UUID required_nullable: Union[None, str] required_not_nullable: str one_of_models: Union["FreeFormModel", "ModelWithUnionProperty", Any] @@ -64,11 +63,12 @@ class AModel: model: "ModelWithUnionProperty" nullable_model: Union["ModelWithUnionProperty", None] an_allof_enum_with_overridden_default: AnAllOfEnum = AnAllOfEnum.OVERRIDDEN_DEFAULT + a_nullable_uuid: Union[None, UUID] = UUID("07EF8B4D-AA09-4FFA-898D-C710796AFF41") any_value: Union[Unset, Any] = "default" an_optional_allof_enum: Union[Unset, AnAllOfEnum] = UNSET nested_list_of_enums: Union[Unset, List[List[DifferentEnum]]] = UNSET a_not_required_date: Union[Unset, datetime.date] = UNSET - a_not_required_uuid: Union[Unset, uuid.UUID] = UNSET + a_not_required_uuid: Union[Unset, UUID] = UNSET attr_1_leading_digit: Union[Unset, str] = UNSET attr_leading_underscore: Union[Unset, str] = UNSET not_required_nullable: Union[None, Unset, str] = UNSET @@ -103,7 +103,7 @@ def to_dict(self) -> Dict[str, Any]: a_uuid = str(self.a_uuid) a_nullable_uuid: Union[None, str] - if isinstance(self.a_nullable_uuid, uuid.UUID): + if isinstance(self.a_nullable_uuid, UUID): a_nullable_uuid = str(self.a_nullable_uuid) else: a_nullable_uuid = self.a_nullable_uuid @@ -295,20 +295,20 @@ def _parse_a_nullable_date(data: object) -> Union[None, datetime.date]: a_nullable_date = _parse_a_nullable_date(d.pop("a_nullable_date")) - a_uuid = uuid.UUID(d.pop("a_uuid")) + a_uuid = UUID(d.pop("a_uuid")) - def _parse_a_nullable_uuid(data: object) -> Union[None, uuid.UUID]: + def _parse_a_nullable_uuid(data: object) -> Union[None, UUID]: if data is None: return data try: if not isinstance(data, str): raise TypeError() - a_nullable_uuid_type_0 = uuid.UUID(data) + a_nullable_uuid_type_0 = UUID(data) return a_nullable_uuid_type_0 except: # noqa: E722 pass - return cast(Union[None, uuid.UUID], data) + return cast(Union[None, UUID], data) a_nullable_uuid = _parse_a_nullable_uuid(d.pop("a_nullable_uuid")) @@ -411,11 +411,11 @@ def _parse_nullable_model(data: object) -> Union["ModelWithUnionProperty", None] a_not_required_date = isoparse(_a_not_required_date).date() _a_not_required_uuid = d.pop("a_not_required_uuid", UNSET) - a_not_required_uuid: Union[Unset, uuid.UUID] + a_not_required_uuid: Union[Unset, UUID] if isinstance(_a_not_required_uuid, Unset): a_not_required_uuid = UNSET else: - a_not_required_uuid = uuid.UUID(_a_not_required_uuid) + a_not_required_uuid = UUID(_a_not_required_uuid) attr_1_leading_digit = d.pop("1_leading_digit", UNSET) diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/extended.py b/end_to_end_tests/golden-record/my_test_api_client/models/extended.py index c3659222b..324513d3a 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/extended.py +++ b/end_to_end_tests/golden-record/my_test_api_client/models/extended.py @@ -1,5 +1,6 @@ import datetime from typing import TYPE_CHECKING, Any, Dict, List, Type, TypeVar, Union, cast +from uuid import UUID from attrs import define as _attrs_define from attrs import field as _attrs_field @@ -27,6 +28,8 @@ class Extended: a_camel_date_time (Union[datetime.date, datetime.datetime]): a_date (datetime.date): a_nullable_date (Union[None, datetime.date]): + a_uuid (UUID): + a_nullable_uuid (Union[None, UUID]): Default: UUID('07EF8B4D-AA09-4FFA-898D-C710796AFF41'). required_nullable (Union[None, str]): required_not_nullable (str): one_of_models (Union['FreeFormModel', 'ModelWithUnionProperty', Any]): @@ -37,6 +40,7 @@ class Extended: an_optional_allof_enum (Union[Unset, AnAllOfEnum]): nested_list_of_enums (Union[Unset, List[List[DifferentEnum]]]): a_not_required_date (Union[Unset, datetime.date]): + a_not_required_uuid (Union[Unset, UUID]): attr_1_leading_digit (Union[Unset, str]): attr_leading_underscore (Union[Unset, str]): not_required_nullable (Union[None, Unset, str]): @@ -52,6 +56,7 @@ class Extended: a_camel_date_time: Union[datetime.date, datetime.datetime] a_date: datetime.date a_nullable_date: Union[None, datetime.date] + a_uuid: UUID required_nullable: Union[None, str] required_not_nullable: str one_of_models: Union["FreeFormModel", "ModelWithUnionProperty", Any] @@ -59,10 +64,12 @@ class Extended: model: "ModelWithUnionProperty" nullable_model: Union["ModelWithUnionProperty", None] an_allof_enum_with_overridden_default: AnAllOfEnum = AnAllOfEnum.OVERRIDDEN_DEFAULT + a_nullable_uuid: Union[None, UUID] = UUID("07EF8B4D-AA09-4FFA-898D-C710796AFF41") any_value: Union[Unset, Any] = "default" an_optional_allof_enum: Union[Unset, AnAllOfEnum] = UNSET nested_list_of_enums: Union[Unset, List[List[DifferentEnum]]] = UNSET a_not_required_date: Union[Unset, datetime.date] = UNSET + a_not_required_uuid: Union[Unset, UUID] = UNSET attr_1_leading_digit: Union[Unset, str] = UNSET attr_leading_underscore: Union[Unset, str] = UNSET not_required_nullable: Union[None, Unset, str] = UNSET @@ -96,6 +103,14 @@ def to_dict(self) -> Dict[str, Any]: else: a_nullable_date = self.a_nullable_date + a_uuid = str(self.a_uuid) + + a_nullable_uuid: Union[None, str] + if isinstance(self.a_nullable_uuid, UUID): + a_nullable_uuid = str(self.a_nullable_uuid) + else: + a_nullable_uuid = self.a_nullable_uuid + required_nullable: Union[None, str] required_nullable = self.required_nullable @@ -146,6 +161,10 @@ def to_dict(self) -> Dict[str, Any]: if not isinstance(self.a_not_required_date, Unset): a_not_required_date = self.a_not_required_date.isoformat() + a_not_required_uuid: Union[Unset, str] = UNSET + if not isinstance(self.a_not_required_uuid, Unset): + a_not_required_uuid = str(self.a_not_required_uuid) + attr_1_leading_digit = self.attr_1_leading_digit attr_leading_underscore = self.attr_leading_underscore @@ -199,6 +218,8 @@ def to_dict(self) -> Dict[str, Any]: "aCamelDateTime": a_camel_date_time, "a_date": a_date, "a_nullable_date": a_nullable_date, + "a_uuid": a_uuid, + "a_nullable_uuid": a_nullable_uuid, "required_nullable": required_nullable, "required_not_nullable": required_not_nullable, "one_of_models": one_of_models, @@ -215,6 +236,8 @@ def to_dict(self) -> Dict[str, Any]: field_dict["nested_list_of_enums"] = nested_list_of_enums if a_not_required_date is not UNSET: field_dict["a_not_required_date"] = a_not_required_date + if a_not_required_uuid is not UNSET: + field_dict["a_not_required_uuid"] = a_not_required_uuid if attr_1_leading_digit is not UNSET: field_dict["1_leading_digit"] = attr_1_leading_digit if attr_leading_underscore is not UNSET: @@ -280,6 +303,23 @@ def _parse_a_nullable_date(data: object) -> Union[None, datetime.date]: a_nullable_date = _parse_a_nullable_date(d.pop("a_nullable_date")) + a_uuid = UUID(d.pop("a_uuid")) + + def _parse_a_nullable_uuid(data: object) -> Union[None, UUID]: + if data is None: + return data + try: + if not isinstance(data, str): + raise TypeError() + a_nullable_uuid_type_0 = UUID(data) + + return a_nullable_uuid_type_0 + except: # noqa: E722 + pass + return cast(Union[None, UUID], data) + + a_nullable_uuid = _parse_a_nullable_uuid(d.pop("a_nullable_uuid")) + def _parse_required_nullable(data: object) -> Union[None, str]: if data is None: return data @@ -378,6 +418,13 @@ def _parse_nullable_model(data: object) -> Union["ModelWithUnionProperty", None] else: a_not_required_date = isoparse(_a_not_required_date).date() + _a_not_required_uuid = d.pop("a_not_required_uuid", UNSET) + a_not_required_uuid: Union[Unset, UUID] + if isinstance(_a_not_required_uuid, Unset): + a_not_required_uuid = UNSET + else: + a_not_required_uuid = UUID(_a_not_required_uuid) + attr_1_leading_digit = d.pop("1_leading_digit", UNSET) attr_leading_underscore = d.pop("_leading_underscore", UNSET) @@ -473,6 +520,8 @@ def _parse_not_required_nullable_model(data: object) -> Union["ModelWithUnionPro a_camel_date_time=a_camel_date_time, a_date=a_date, a_nullable_date=a_nullable_date, + a_uuid=a_uuid, + a_nullable_uuid=a_nullable_uuid, required_nullable=required_nullable, required_not_nullable=required_not_nullable, one_of_models=one_of_models, @@ -483,6 +532,7 @@ def _parse_not_required_nullable_model(data: object) -> Union["ModelWithUnionPro an_optional_allof_enum=an_optional_allof_enum, nested_list_of_enums=nested_list_of_enums, a_not_required_date=a_not_required_date, + a_not_required_uuid=a_not_required_uuid, attr_1_leading_digit=attr_1_leading_digit, attr_leading_underscore=attr_leading_underscore, not_required_nullable=not_required_nullable, diff --git a/openapi_python_client/parser/properties/uuid.py b/openapi_python_client/parser/properties/uuid.py index d0ea95f88..86d7d6a0a 100644 --- a/openapi_python_client/parser/properties/uuid.py +++ b/openapi_python_client/parser/properties/uuid.py @@ -22,7 +22,7 @@ class UuidProperty(PropertyProtocol): description: str | None example: str | None - _type_string: ClassVar[str] = "uuid.UUID" + _type_string: ClassVar[str] = "UUID" _json_type_string: ClassVar[str] = "str" _allowed_locations: ClassVar[set[oai.ParameterLocation]] = { oai.ParameterLocation.QUERY, @@ -76,5 +76,5 @@ def get_imports(self, *, prefix: str) -> set[str]: back to the root of the generated client. """ imports = super().get_imports(prefix=prefix) - imports.update({"import uuid"}) + imports.update({"from uuid import UUID"}) return imports diff --git a/openapi_python_client/templates/property_templates/uuid_property.py.jinja b/openapi_python_client/templates/property_templates/uuid_property.py.jinja index c73a45586..1f91c1e3a 100644 --- a/openapi_python_client/templates/property_templates/uuid_property.py.jinja +++ b/openapi_python_client/templates/property_templates/uuid_property.py.jinja @@ -1,5 +1,5 @@ {% macro construct_function(property, source) %} -uuid.UUID({{ source }}) +UUID({{ source }}) {% endmacro %} {% from "property_templates/property_macros.py.jinja" import construct_template %} From ede14fa647ee812de2b555323cba976cf1afc0af Mon Sep 17 00:00:00 2001 From: Dylan Anthony Date: Sun, 20 Oct 2024 15:35:07 -0600 Subject: [PATCH 4/6] Add tests for invalid UUID defaults --- .../__snapshots__/test_end_to_end.ambr | 21 +++++++++++++ .../invalid-uuid-defaults.yaml | 30 +++++++++++++++++++ pyproject.toml | 2 +- 3 files changed, 52 insertions(+), 1 deletion(-) create mode 100644 end_to_end_tests/documents_with_errors/invalid-uuid-defaults.yaml diff --git a/end_to_end_tests/__snapshots__/test_end_to_end.ambr b/end_to_end_tests/__snapshots__/test_end_to_end.ambr index dac0127aa..0bdbbb887 100644 --- a/end_to_end_tests/__snapshots__/test_end_to_end.ambr +++ b/end_to_end_tests/__snapshots__/test_end_to_end.ambr @@ -9,6 +9,27 @@ Circular $ref in request body + If you believe this was a mistake or this tool is missing a feature you need, please open an issue at https://github.com/openapi-generators/openapi-python-client/issues/new/choose + + ''' +# --- +# name: test_documents_with_errors[invalid-uuid-defaults] + ''' + Generating /test-documents-with-errors + Warning(s) encountered while generating. Client was generated, but some pieces may be missing + + WARNING parsing PUT / within default. Endpoint will not be generated. + + cannot parse parameter of endpoint put_: Invalid UUID value: 3 + + Schema(title=None, multipleOf=None, maximum=None, exclusiveMaximum=None, minimum=None, exclusiveMinimum=None, maxLength=None, minLength=None, pattern=None, maxItems=None, minItems=None, uniqueItems=None, maxProperties=None, minProperties=None, required=None, enum=None, const=None, type=[, ], allOf=[], oneOf=[], anyOf=[], schema_not=None, items=None, properties=None, additionalProperties=None, description=None, schema_format='uuid', default=3, nullable=False, discriminator=None, readOnly=None, writeOnly=None, xml=None, externalDocs=None, example=None, deprecated=None) + + WARNING parsing POST / within default. Endpoint will not be generated. + + cannot parse parameter of endpoint post_: Invalid UUID value: notauuid + + Schema(title=None, multipleOf=None, maximum=None, exclusiveMaximum=None, minimum=None, exclusiveMinimum=None, maxLength=None, minLength=None, pattern=None, maxItems=None, minItems=None, uniqueItems=None, maxProperties=None, minProperties=None, required=None, enum=None, const=None, type=[, ], allOf=[], oneOf=[], anyOf=[], schema_not=None, items=None, properties=None, additionalProperties=None, description=None, schema_format='uuid', default='notauuid', nullable=False, discriminator=None, readOnly=None, writeOnly=None, xml=None, externalDocs=None, example=None, deprecated=None) + If you believe this was a mistake or this tool is missing a feature you need, please open an issue at https://github.com/openapi-generators/openapi-python-client/issues/new/choose ''' diff --git a/end_to_end_tests/documents_with_errors/invalid-uuid-defaults.yaml b/end_to_end_tests/documents_with_errors/invalid-uuid-defaults.yaml new file mode 100644 index 000000000..4a1e7adfc --- /dev/null +++ b/end_to_end_tests/documents_with_errors/invalid-uuid-defaults.yaml @@ -0,0 +1,30 @@ +openapi: "3.1.0" +info: + title: "Circular Body Ref" + version: "0.1.0" +paths: + /: + post: + parameters: + - name: id + in: query + required: true + schema: + type: ["null", string] + format: uuid + default: "notauuid" + responses: + "200": + description: "Successful Response" + put: + parameters: + - name: another_id + in: query + required: true + schema: + type: ["null", string] + format: uuid + default: 3 + responses: + "200": + description: "Successful Response" \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 582e0806f..e9cdda65d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -114,7 +114,7 @@ mypy = "mypy openapi_python_client" check = { composite = ["lint", "format", "mypy", "test"] } regen = {composite = ["regen_e2e", "regen_integration"]} e2e = "pytest openapi_python_client end_to_end_tests/test_end_to_end.py" -re = {composite = ["regen_e2e", "e2e"]} +re = {composite = ["regen_e2e", "e2e --snapshot-update"]} regen_e2e = "python -m end_to_end_tests.regen_golden_record" [tool.pdm.scripts.test] From aea871dacc9c310d1eeb5f9ba57d401dceaee457 Mon Sep 17 00:00:00 2001 From: Dylan Anthony Date: Sun, 20 Oct 2024 15:44:59 -0600 Subject: [PATCH 5/6] Fix UUID defaults for non-nullable properties --- end_to_end_tests/__snapshots__/test_end_to_end.ambr | 2 -- .../documents_with_errors/invalid-uuid-defaults.yaml | 8 ++++---- .../golden-record/my_test_api_client/models/a_model.py | 4 ++-- .../golden-record/my_test_api_client/models/extended.py | 4 ++-- openapi_python_client/parser/properties/__init__.py | 2 +- 5 files changed, 9 insertions(+), 11 deletions(-) diff --git a/end_to_end_tests/__snapshots__/test_end_to_end.ambr b/end_to_end_tests/__snapshots__/test_end_to_end.ambr index 0bdbbb887..01e5b471c 100644 --- a/end_to_end_tests/__snapshots__/test_end_to_end.ambr +++ b/end_to_end_tests/__snapshots__/test_end_to_end.ambr @@ -22,13 +22,11 @@ cannot parse parameter of endpoint put_: Invalid UUID value: 3 - Schema(title=None, multipleOf=None, maximum=None, exclusiveMaximum=None, minimum=None, exclusiveMinimum=None, maxLength=None, minLength=None, pattern=None, maxItems=None, minItems=None, uniqueItems=None, maxProperties=None, minProperties=None, required=None, enum=None, const=None, type=[, ], allOf=[], oneOf=[], anyOf=[], schema_not=None, items=None, properties=None, additionalProperties=None, description=None, schema_format='uuid', default=3, nullable=False, discriminator=None, readOnly=None, writeOnly=None, xml=None, externalDocs=None, example=None, deprecated=None) WARNING parsing POST / within default. Endpoint will not be generated. cannot parse parameter of endpoint post_: Invalid UUID value: notauuid - Schema(title=None, multipleOf=None, maximum=None, exclusiveMaximum=None, minimum=None, exclusiveMinimum=None, maxLength=None, minLength=None, pattern=None, maxItems=None, minItems=None, uniqueItems=None, maxProperties=None, minProperties=None, required=None, enum=None, const=None, type=[, ], allOf=[], oneOf=[], anyOf=[], schema_not=None, items=None, properties=None, additionalProperties=None, description=None, schema_format='uuid', default='notauuid', nullable=False, discriminator=None, readOnly=None, writeOnly=None, xml=None, externalDocs=None, example=None, deprecated=None) If you believe this was a mistake or this tool is missing a feature you need, please open an issue at https://github.com/openapi-generators/openapi-python-client/issues/new/choose diff --git a/end_to_end_tests/documents_with_errors/invalid-uuid-defaults.yaml b/end_to_end_tests/documents_with_errors/invalid-uuid-defaults.yaml index 4a1e7adfc..dd768de4f 100644 --- a/end_to_end_tests/documents_with_errors/invalid-uuid-defaults.yaml +++ b/end_to_end_tests/documents_with_errors/invalid-uuid-defaults.yaml @@ -8,9 +8,9 @@ paths: parameters: - name: id in: query - required: true + required: false schema: - type: ["null", string] + type: string format: uuid default: "notauuid" responses: @@ -20,9 +20,9 @@ paths: parameters: - name: another_id in: query - required: true + required: false schema: - type: ["null", string] + type: string format: uuid default: 3 responses: diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/a_model.py b/end_to_end_tests/golden-record/my_test_api_client/models/a_model.py index a14400c9d..6b1d5cec5 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/a_model.py +++ b/end_to_end_tests/golden-record/my_test_api_client/models/a_model.py @@ -40,7 +40,7 @@ class AModel: an_optional_allof_enum (Union[Unset, AnAllOfEnum]): nested_list_of_enums (Union[Unset, List[List[DifferentEnum]]]): a_not_required_date (Union[Unset, datetime.date]): - a_not_required_uuid (Union[Unset, UUID]): + a_not_required_uuid (Union[Unset, UUID]): Default: UUID('07EF8B4D-AA09-4FFA-898D-C710796AFF41'). attr_1_leading_digit (Union[Unset, str]): attr_leading_underscore (Union[Unset, str]): not_required_nullable (Union[None, Unset, str]): @@ -68,7 +68,7 @@ class AModel: an_optional_allof_enum: Union[Unset, AnAllOfEnum] = UNSET nested_list_of_enums: Union[Unset, List[List[DifferentEnum]]] = UNSET a_not_required_date: Union[Unset, datetime.date] = UNSET - a_not_required_uuid: Union[Unset, UUID] = UNSET + a_not_required_uuid: Union[Unset, UUID] = UUID("07EF8B4D-AA09-4FFA-898D-C710796AFF41") attr_1_leading_digit: Union[Unset, str] = UNSET attr_leading_underscore: Union[Unset, str] = UNSET not_required_nullable: Union[None, Unset, str] = UNSET diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/extended.py b/end_to_end_tests/golden-record/my_test_api_client/models/extended.py index 324513d3a..320127ade 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/extended.py +++ b/end_to_end_tests/golden-record/my_test_api_client/models/extended.py @@ -40,7 +40,7 @@ class Extended: an_optional_allof_enum (Union[Unset, AnAllOfEnum]): nested_list_of_enums (Union[Unset, List[List[DifferentEnum]]]): a_not_required_date (Union[Unset, datetime.date]): - a_not_required_uuid (Union[Unset, UUID]): + a_not_required_uuid (Union[Unset, UUID]): Default: UUID('07EF8B4D-AA09-4FFA-898D-C710796AFF41'). attr_1_leading_digit (Union[Unset, str]): attr_leading_underscore (Union[Unset, str]): not_required_nullable (Union[None, Unset, str]): @@ -69,7 +69,7 @@ class Extended: an_optional_allof_enum: Union[Unset, AnAllOfEnum] = UNSET nested_list_of_enums: Union[Unset, List[List[DifferentEnum]]] = UNSET a_not_required_date: Union[Unset, datetime.date] = UNSET - a_not_required_uuid: Union[Unset, UUID] = UNSET + a_not_required_uuid: Union[Unset, UUID] = UUID("07EF8B4D-AA09-4FFA-898D-C710796AFF41") attr_1_leading_digit: Union[Unset, str] = UNSET attr_leading_underscore: Union[Unset, str] = UNSET not_required_nullable: Union[None, Unset, str] = UNSET diff --git a/openapi_python_client/parser/properties/__init__.py b/openapi_python_client/parser/properties/__init__.py index 723a882fa..c1e94c3c8 100644 --- a/openapi_python_client/parser/properties/__init__.py +++ b/openapi_python_client/parser/properties/__init__.py @@ -84,7 +84,7 @@ def _string_based_property( return UuidProperty.build( name=name, required=required, - default=None, + default=data.default, python_name=python_name, description=data.description, example=data.example, From 84665bae2d1b6d9b6c2fb1dc08cc244300604757 Mon Sep 17 00:00:00 2001 From: Dylan Anthony Date: Sun, 20 Oct 2024 15:48:13 -0600 Subject: [PATCH 6/6] Fix snapshots --- .../golden-record/my_test_api_client/models/a_model.py | 4 ++-- .../golden-record/my_test_api_client/models/extended.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/a_model.py b/end_to_end_tests/golden-record/my_test_api_client/models/a_model.py index 6b1d5cec5..a14400c9d 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/a_model.py +++ b/end_to_end_tests/golden-record/my_test_api_client/models/a_model.py @@ -40,7 +40,7 @@ class AModel: an_optional_allof_enum (Union[Unset, AnAllOfEnum]): nested_list_of_enums (Union[Unset, List[List[DifferentEnum]]]): a_not_required_date (Union[Unset, datetime.date]): - a_not_required_uuid (Union[Unset, UUID]): Default: UUID('07EF8B4D-AA09-4FFA-898D-C710796AFF41'). + a_not_required_uuid (Union[Unset, UUID]): attr_1_leading_digit (Union[Unset, str]): attr_leading_underscore (Union[Unset, str]): not_required_nullable (Union[None, Unset, str]): @@ -68,7 +68,7 @@ class AModel: an_optional_allof_enum: Union[Unset, AnAllOfEnum] = UNSET nested_list_of_enums: Union[Unset, List[List[DifferentEnum]]] = UNSET a_not_required_date: Union[Unset, datetime.date] = UNSET - a_not_required_uuid: Union[Unset, UUID] = UUID("07EF8B4D-AA09-4FFA-898D-C710796AFF41") + a_not_required_uuid: Union[Unset, UUID] = UNSET attr_1_leading_digit: Union[Unset, str] = UNSET attr_leading_underscore: Union[Unset, str] = UNSET not_required_nullable: Union[None, Unset, str] = UNSET diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/extended.py b/end_to_end_tests/golden-record/my_test_api_client/models/extended.py index 320127ade..324513d3a 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/extended.py +++ b/end_to_end_tests/golden-record/my_test_api_client/models/extended.py @@ -40,7 +40,7 @@ class Extended: an_optional_allof_enum (Union[Unset, AnAllOfEnum]): nested_list_of_enums (Union[Unset, List[List[DifferentEnum]]]): a_not_required_date (Union[Unset, datetime.date]): - a_not_required_uuid (Union[Unset, UUID]): Default: UUID('07EF8B4D-AA09-4FFA-898D-C710796AFF41'). + a_not_required_uuid (Union[Unset, UUID]): attr_1_leading_digit (Union[Unset, str]): attr_leading_underscore (Union[Unset, str]): not_required_nullable (Union[None, Unset, str]): @@ -69,7 +69,7 @@ class Extended: an_optional_allof_enum: Union[Unset, AnAllOfEnum] = UNSET nested_list_of_enums: Union[Unset, List[List[DifferentEnum]]] = UNSET a_not_required_date: Union[Unset, datetime.date] = UNSET - a_not_required_uuid: Union[Unset, UUID] = UUID("07EF8B4D-AA09-4FFA-898D-C710796AFF41") + a_not_required_uuid: Union[Unset, UUID] = UNSET attr_1_leading_digit: Union[Unset, str] = UNSET attr_leading_underscore: Union[Unset, str] = UNSET not_required_nullable: Union[None, Unset, str] = UNSET