Skip to content

better support for merging properties with allOf #1096

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

Merged
merged 25 commits into from
Sep 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
89877d6
fix: better support for merging properties with allOf
eli-bl Aug 14, 2024
d9becd0
fix "ommitted" typo
eli-bl Aug 14, 2024
8c4ef97
lint
eli-bl Aug 21, 2024
b6d9d0a
fix type hinting problems
eli-bl Aug 21, 2024
1fab8ee
Merge branch 'main' into issue-1090-merge-allof
eli-bl Aug 21, 2024
61e015b
another type hinting fix
eli-bl Aug 21, 2024
8722001
add change description
eli-bl Aug 21, 2024
ad77302
fix enum property merge logic
eli-bl Aug 22, 2024
99a0c96
Try to improve type hints
dbanty Aug 25, 2024
90ad18b
Use itertools permutations for merge tests
dbanty Aug 25, 2024
d81fd5d
Stop checking string attributes that aren't actually used in generate…
dbanty Aug 25, 2024
4a7f603
Remove exceptions, support comparing internal props of lists.
dbanty Aug 25, 2024
770ebc2
Validate all default types
dbanty Aug 25, 2024
00670a0
Update changeset to new expected end-state
dbanty Aug 25, 2024
2e76e02
Merge branch 'main' into issue-1090-merge-allof
dbanty Aug 25, 2024
db90641
allow merge of str with date/file/etc., add end-to-end tests
eli-bl Aug 26, 2024
22361fe
add test for merging lists
eli-bl Aug 26, 2024
da76b1c
Merge branch 'main' into issue-1090-merge-allof
eli-bl Aug 26, 2024
9cfe04d
Merge branch 'main' into issue-1090-merge-allof
dbanty Sep 2, 2024
3bfaef9
Mention default in changeset
dbanty Sep 2, 2024
4209fb6
Fix merging inline objects
dbanty Sep 2, 2024
2f185fb
Merge branch 'main' into issue-1090-merge-allof
dbanty Sep 2, 2024
1e995b7
fix default computation for merged properties
eli-bl Sep 3, 2024
4046d03
workarounds for value/Value conversion inconsistencies
eli-bl Sep 3, 2024
a4f4a20
Merge branch 'main' into issue-1090-merge-allof
dbanty Sep 7, 2024
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
20 changes: 20 additions & 0 deletions .changeset/improved_property_merging.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
---
default: minor
---

# Improved property-merging behavior with `allOf`

When using `allOf` to extend a base object type, `openapi-python-client` is now able to handle some kinds of modifications to an existing property that would have previously caused an error:

- Overriding attributes that do not affect validation, such as `description`.
- Combining properties that this generator ignores, like `maxLength` or `pattern`.
- Combining a generic numeric type with `int` (resulting in `int`).
- Adding a `format` to a string.
- Combining `any` with a specific type (resulting in that specific type).
- Adding or overriding a `default`

> [!NOTE]
> `pattern` and `max_length` are no longer fields on `StringProperty`, which may impact custom templates.

This also fixes a bug where properties of inline objects (as opposed to references) were not using the
merge logic, but were simply overwriting previous definitions of the same property.
50 changes: 50 additions & 0 deletions end_to_end_tests/baseline_openapi_3.0.json
Original file line number Diff line number Diff line change
Expand Up @@ -2152,6 +2152,56 @@
"additionalProperties": {}
}
},
"ModelWithMergedProperties": {
"title": "ModelWithMergedProperties",
"allOf": [
{
"type": "object",
"properties": {
"simpleString": {
"type": "string",
"description": "base simpleString description"
},
"stringToEnum": {
"type": "string",
"default": "a"
},
"stringToDate": {
"type": "string"
},
"numberToInt": {
"type": "number"
},
"anyToString": {}
}
},
{
"type": "object",
"properties": {
"simpleString": {
"type": "string",
"description": "extended simpleString description",
"default": "new default"
},
"stringToEnum": {
"type": "string",
"enum": ["a", "b"]
},
"stringToDate": {
"type": "string",
"format": "date"
},
"numberToInt": {
"type": "integer"
},
"anyToString": {
"type": "string",
"default": "x"
}
}
}
]
},
"ModelWithPrimitiveAdditionalProperties": {
"title": "ModelWithPrimitiveAdditionalProperties",
"type": "object",
Expand Down
50 changes: 50 additions & 0 deletions end_to_end_tests/baseline_openapi_3.1.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2146,6 +2146,56 @@ info:
"additionalProperties": { }
}
},
"ModelWithMergedProperties": {
"title": "ModelWithMergedProperties",
"allOf": [
{
"type": "object",
"properties": {
"simpleString": {
"type": "string",
"description": "base simpleString description"
},
"stringToEnum": {
"type": "string",
"default": "a"
},
"stringToDate": {
"type": "string"
},
"numberToInt": {
"type": "number"
},
"anyToString": {}
}
},
{
"type": "object",
"properties": {
"simpleString": {
"type": "string",
"description": "extended simpleString description",
"default": "new default"
},
"stringToEnum": {
"type": "string",
"enum": ["a", "b"]
},
"stringToDate": {
"type": "string",
"format": "date"
},
"numberToInt": {
"type": "integer"
},
"anyToString": {
"type": "string",
"default": "x"
}
}
}
]
},
"ModelWithPrimitiveAdditionalProperties": {
"title": "ModelWithPrimitiveAdditionalProperties",
"type": "object",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@
from .model_with_circular_ref_in_additional_properties_b import ModelWithCircularRefInAdditionalPropertiesB
from .model_with_date_time_property import ModelWithDateTimeProperty
from .model_with_discriminated_union import ModelWithDiscriminatedUnion
from .model_with_merged_properties import ModelWithMergedProperties
from .model_with_merged_properties_string_to_enum import ModelWithMergedPropertiesStringToEnum
from .model_with_no_properties import ModelWithNoProperties
from .model_with_primitive_additional_properties import ModelWithPrimitiveAdditionalProperties
from .model_with_primitive_additional_properties_a_date_holder import ModelWithPrimitiveAdditionalPropertiesADateHolder
Expand Down Expand Up @@ -138,6 +140,8 @@
"ModelWithCircularRefInAdditionalPropertiesB",
"ModelWithDateTimeProperty",
"ModelWithDiscriminatedUnion",
"ModelWithMergedProperties",
"ModelWithMergedPropertiesStringToEnum",
"ModelWithNoProperties",
"ModelWithPrimitiveAdditionalProperties",
"ModelWithPrimitiveAdditionalPropertiesADateHolder",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import datetime
from typing import Any, Dict, List, Type, TypeVar, Union

from attrs import define as _attrs_define
from attrs import field as _attrs_field
from dateutil.parser import isoparse

from ..models.model_with_merged_properties_string_to_enum import ModelWithMergedPropertiesStringToEnum
from ..types import UNSET, Unset

T = TypeVar("T", bound="ModelWithMergedProperties")


@_attrs_define
class ModelWithMergedProperties:
"""
Attributes:
simple_string (Union[Unset, str]): extended simpleString description Default: 'new default'.
string_to_enum (Union[Unset, ModelWithMergedPropertiesStringToEnum]): Default:
ModelWithMergedPropertiesStringToEnum.A.
string_to_date (Union[Unset, datetime.date]):
number_to_int (Union[Unset, int]):
any_to_string (Union[Unset, str]): Default: 'x'.
"""

simple_string: Union[Unset, str] = "new default"
string_to_enum: Union[Unset, ModelWithMergedPropertiesStringToEnum] = ModelWithMergedPropertiesStringToEnum.A
string_to_date: Union[Unset, datetime.date] = UNSET
number_to_int: Union[Unset, int] = UNSET
any_to_string: Union[Unset, str] = "x"
additional_properties: Dict[str, Any] = _attrs_field(init=False, factory=dict)

def to_dict(self) -> Dict[str, Any]:
simple_string = self.simple_string

string_to_enum: Union[Unset, str] = UNSET
if not isinstance(self.string_to_enum, Unset):
string_to_enum = self.string_to_enum.value

string_to_date: Union[Unset, str] = UNSET
if not isinstance(self.string_to_date, Unset):
string_to_date = self.string_to_date.isoformat()

number_to_int = self.number_to_int

any_to_string = self.any_to_string

field_dict: Dict[str, Any] = {}
field_dict.update(self.additional_properties)
field_dict.update({})
if simple_string is not UNSET:
field_dict["simpleString"] = simple_string
if string_to_enum is not UNSET:
field_dict["stringToEnum"] = string_to_enum
if string_to_date is not UNSET:
field_dict["stringToDate"] = string_to_date
if number_to_int is not UNSET:
field_dict["numberToInt"] = number_to_int
if any_to_string is not UNSET:
field_dict["anyToString"] = any_to_string

return field_dict

@classmethod
def from_dict(cls: Type[T], src_dict: Dict[str, Any]) -> T:
d = src_dict.copy()
simple_string = d.pop("simpleString", UNSET)

_string_to_enum = d.pop("stringToEnum", UNSET)
string_to_enum: Union[Unset, ModelWithMergedPropertiesStringToEnum]
if isinstance(_string_to_enum, Unset):
string_to_enum = UNSET
else:
string_to_enum = ModelWithMergedPropertiesStringToEnum(_string_to_enum)

_string_to_date = d.pop("stringToDate", UNSET)
string_to_date: Union[Unset, datetime.date]
if isinstance(_string_to_date, Unset):
string_to_date = UNSET
else:
string_to_date = isoparse(_string_to_date).date()

number_to_int = d.pop("numberToInt", UNSET)

any_to_string = d.pop("anyToString", UNSET)

model_with_merged_properties = cls(
simple_string=simple_string,
string_to_enum=string_to_enum,
string_to_date=string_to_date,
number_to_int=number_to_int,
any_to_string=any_to_string,
)

model_with_merged_properties.additional_properties = d
return model_with_merged_properties

@property
def additional_keys(self) -> List[str]:
return list(self.additional_properties.keys())

def __getitem__(self, key: str) -> Any:
return self.additional_properties[key]

def __setitem__(self, key: str, value: Any) -> None:
self.additional_properties[key] = value

def __delitem__(self, key: str) -> None:
del self.additional_properties[key]

def __contains__(self, key: str) -> bool:
return key in self.additional_properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from enum import Enum


class ModelWithMergedPropertiesStringToEnum(str, Enum):
A = "a"
B = "b"

def __str__(self) -> str:
return str(self.value)
4 changes: 2 additions & 2 deletions openapi_python_client/parser/openapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ def _add_responses(
ParseError(
detail=(
f"Invalid response status code {code} (not a valid HTTP "
f"status code), response will be ommitted from generated "
f"status code), response will be omitted from generated "
f"client"
)
)
Expand All @@ -175,7 +175,7 @@ def _add_responses(
ParseError(
detail=(
f"Cannot parse response for status code {status_code}{detail_suffix}, "
f"response will be ommitted from generated client"
f"response will be omitted from generated client"
),
data=response.data,
)
Expand Down
1 change: 0 additions & 1 deletion openapi_python_client/parser/properties/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,6 @@ def _string_based_property(
name=name,
default=data.default,
required=required,
pattern=data.pattern,
python_name=python_name,
description=data.description,
example=data.example,
Expand Down
8 changes: 7 additions & 1 deletion openapi_python_client/parser/properties/enum_property.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from __future__ import annotations

__all__ = ["EnumProperty"]
__all__ = ["EnumProperty", "ValueType"]

from typing import Any, ClassVar, List, Union, cast

Expand Down Expand Up @@ -164,6 +164,12 @@ def convert_value(self, value: Any) -> Value | PropertyError | None:
return PropertyError(detail=f"Value {value} is not valid for enum {self.name}")
return PropertyError(detail=f"Cannot convert {value} to enum {self.name} of type {self.value_type}")

def default_to_raw(self) -> ValueType | None:
if self.default is None:
return None
key = self.default.split(".")[1]
return self.values[key]

def get_base_type_string(self, *, quoted: bool = False) -> str:
return self.class_info.name

Expand Down
7 changes: 5 additions & 2 deletions openapi_python_client/parser/properties/int.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,10 +60,13 @@ def convert_value(cls, value: Any) -> Value | None | PropertyError:
return value
if isinstance(value, str):
try:
int(value)
value = float(value)
except ValueError:
return PropertyError(f"Invalid int value: {value}")
return Value(value)
if isinstance(value, float):
as_int = int(value)
if value == as_int:
value = as_int
if isinstance(value, int) and not isinstance(value, bool):
return Value(str(value))
return PropertyError(f"Invalid int value: {value}")
Loading
Loading