Skip to content

Commit dc6971d

Browse files
committed
Switch properties to use attr.s instead of dataclass
1 parent 66fa9a8 commit dc6971d

File tree

7 files changed

+415
-563
lines changed

7 files changed

+415
-563
lines changed

openapi_python_client/parser/properties/__init__.py

Lines changed: 65 additions & 108 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,29 @@
1-
from dataclasses import dataclass, replace
21
from itertools import chain
32
from typing import Any, ClassVar, Dict, Generic, List, Optional, Set, Tuple, TypeVar, Union
43

5-
from dateutil.parser import isoparse
4+
import attr
65

76
from ... import schema as oai
87
from ... import utils
98
from ..errors import PropertyError, ValidationError
109
from ..reference import Reference
10+
from .converter import convert, convert_chain
1111
from .enum_property import EnumProperty
1212
from .model_property import ModelProperty
1313
from .property import Property
1414
from .schemas import Schemas
1515

1616

17-
@dataclass
17+
@attr.s(auto_attribs=True, frozen=True, slots=True)
1818
class StringProperty(Property):
1919
""" A property of type str """
2020

2121
max_length: Optional[int] = None
2222
pattern: Optional[str] = None
23-
2423
_type_string: ClassVar[str] = "str"
2524

26-
def _validate_default(self, default: Any) -> str:
27-
return f"{utils.remove_string_escapes(default)!r}"
28-
2925

30-
@dataclass
26+
@attr.s(auto_attribs=True, frozen=True, slots=True)
3127
class DateTimeProperty(Property):
3228
"""
3329
A property of type datetime.datetime
@@ -48,15 +44,8 @@ def get_imports(self, *, prefix: str) -> Set[str]:
4844
imports.update({"import datetime", "from typing import cast", "from dateutil.parser import isoparse"})
4945
return imports
5046

51-
def _validate_default(self, default: Any) -> str:
52-
try:
53-
isoparse(default)
54-
except (TypeError, ValueError) as e:
55-
raise ValidationError from e
56-
return f"isoparse({default!r})"
5747

58-
59-
@dataclass
48+
@attr.s(auto_attribs=True, frozen=True, slots=True)
6049
class DateProperty(Property):
6150
""" A property of type datetime.date """
6251

@@ -75,15 +64,8 @@ def get_imports(self, *, prefix: str) -> Set[str]:
7564
imports.update({"import datetime", "from typing import cast", "from dateutil.parser import isoparse"})
7665
return imports
7766

78-
def _validate_default(self, default: Any) -> str:
79-
try:
80-
isoparse(default).date()
81-
except (TypeError, ValueError) as e:
82-
raise ValidationError() from e
83-
return f"isoparse({default!r}).date()"
84-
8567

86-
@dataclass
68+
@attr.s(auto_attribs=True, frozen=True, slots=True)
8769
class FileProperty(Property):
8870
""" A property used for uploading files """
8971

@@ -103,49 +85,31 @@ def get_imports(self, *, prefix: str) -> Set[str]:
10385
return imports
10486

10587

106-
@dataclass
88+
@attr.s(auto_attribs=True, frozen=True, slots=True)
10789
class FloatProperty(Property):
10890
""" A property of type float """
10991

110-
default: Optional[float] = None
11192
_type_string: ClassVar[str] = "float"
11293

113-
def _validate_default(self, default: Any) -> float:
114-
try:
115-
return float(default)
116-
except (TypeError, ValueError) as e:
117-
raise ValidationError() from e
118-
11994

120-
@dataclass
95+
@attr.s(auto_attribs=True, frozen=True, slots=True)
12196
class IntProperty(Property):
12297
""" A property of type int """
12398

124-
default: Optional[int] = None
12599
_type_string: ClassVar[str] = "int"
126100

127-
def _validate_default(self, default: Any) -> int:
128-
try:
129-
return int(default)
130-
except (TypeError, ValueError) as e:
131-
raise ValidationError() from e
132-
133101

134-
@dataclass
102+
@attr.s(auto_attribs=True, frozen=True, slots=True)
135103
class BooleanProperty(Property):
136104
""" Property for bool """
137105

138106
_type_string: ClassVar[str] = "bool"
139107

140-
def _validate_default(self, default: Any) -> bool:
141-
# no try/except needed as anything that comes from the initial load from json/yaml will be boolable
142-
return bool(default)
143-
144108

145109
InnerProp = TypeVar("InnerProp", bound=Property)
146110

147111

148-
@dataclass
112+
@attr.s(auto_attribs=True, frozen=True, slots=True)
149113
class ListProperty(Property, Generic[InnerProp]):
150114
""" A property representing a list (array) of other properties """
151115

@@ -176,11 +140,8 @@ def get_imports(self, *, prefix: str) -> Set[str]:
176140
imports.add("from typing import List")
177141
return imports
178142

179-
def _validate_default(self, default: Any) -> None:
180-
return None
181-
182143

183-
@dataclass
144+
@attr.s(auto_attribs=True, frozen=True, slots=True)
184145
class UnionProperty(Property):
185146
""" A property representing a Union (anyOf) of other properties """
186147

@@ -214,41 +175,6 @@ def get_imports(self, *, prefix: str) -> Set[str]:
214175
imports.add("from typing import Union")
215176
return imports
216177

217-
def _validate_default(self, default: Any) -> Any:
218-
for property in self.inner_properties:
219-
try:
220-
val = property._validate_default(default)
221-
return val
222-
except ValidationError:
223-
continue
224-
raise ValidationError()
225-
226-
227-
@dataclass
228-
class DictProperty(Property):
229-
""" Property that is a general Dict """
230-
231-
_type_string: ClassVar[str] = "Dict[Any, Any]"
232-
template: ClassVar[str] = "dict_property.pyi"
233-
234-
def get_imports(self, *, prefix: str) -> Set[str]:
235-
"""
236-
Get a set of import strings that should be included when this property is used somewhere
237-
238-
Args:
239-
prefix: A prefix to put before any relative (local) module names. This should be the number of . to get
240-
back to the root of the generated client.
241-
"""
242-
imports = super().get_imports(prefix=prefix)
243-
imports.add("from typing import Dict")
244-
if self.default is not None:
245-
imports.add("from dataclasses import field")
246-
imports.add("from typing import cast")
247-
return imports
248-
249-
def _validate_default(self, default: Any) -> None:
250-
return None
251-
252178

253179
def _string_based_property(
254180
name: str, required: bool, data: oai.Schema
@@ -259,27 +185,27 @@ def _string_based_property(
259185
return DateTimeProperty(
260186
name=name,
261187
required=required,
262-
default=data.default,
188+
default=convert("datetime.datetime", data.default),
263189
nullable=data.nullable,
264190
)
265191
elif string_format == "date":
266192
return DateProperty(
267193
name=name,
268194
required=required,
269-
default=data.default,
195+
default=convert("datetime.date", data.default),
270196
nullable=data.nullable,
271197
)
272198
elif string_format == "binary":
273199
return FileProperty(
274200
name=name,
275201
required=required,
276-
default=data.default,
202+
default=None,
277203
nullable=data.nullable,
278204
)
279205
else:
280206
return StringProperty(
281207
name=name,
282-
default=data.default,
208+
default=convert("str", data.default),
283209
required=required,
284210
pattern=data.pattern,
285211
nullable=data.nullable,
@@ -327,23 +253,54 @@ def build_model_property(
327253
required=required,
328254
name=name,
329255
)
330-
schemas = replace(schemas, models={**schemas.models, prop.reference.class_name: prop})
256+
schemas = attr.evolve(schemas, models={**schemas.models, prop.reference.class_name: prop})
331257
return prop, schemas
332258

333259

334260
def build_enum_property(
335261
*, data: oai.Schema, name: str, required: bool, schemas: Schemas, enum: List[Union[str, int]]
336-
) -> Tuple[EnumProperty, Schemas]:
262+
) -> Tuple[Union[EnumProperty, PropertyError], Schemas]:
263+
264+
reference = Reference.from_ref(data.title or name)
265+
values = EnumProperty.values_from_list(enum)
266+
267+
dedup_counter = 0 # TODO: use the parent names instead of a counter for deduping
268+
while reference.class_name in schemas.enums:
269+
existing = schemas.enums[reference.class_name]
270+
if values == existing.values:
271+
break # This is the same Enum, we're good
272+
dedup_counter += 1
273+
reference = Reference.from_ref(f"{reference.class_name}{dedup_counter}")
274+
275+
for value in values.values():
276+
value_type = type(value)
277+
break
278+
else:
279+
return PropertyError(data=data, detail="No values provided for Enum"), schemas
280+
281+
default = None
282+
if data.default is not None:
283+
inverse_values = {v: k for k, v in values.items()}
284+
try:
285+
default = f"{reference.class_name}.{inverse_values[data.default]}"
286+
except KeyError:
287+
return (
288+
PropertyError(
289+
detail=f"{data.default} is an invalid default for enum {reference.class_name}", data=data
290+
),
291+
schemas,
292+
)
293+
337294
prop = EnumProperty(
338295
name=name,
339296
required=required,
340-
values=EnumProperty.values_from_list(enum),
341-
title=data.title or name,
342-
default=data.default,
297+
default=default,
343298
nullable=data.nullable,
344-
existing_enums=schemas.enums,
299+
reference=reference,
300+
values=values,
301+
value_type=value_type,
345302
)
346-
schemas = replace(schemas, enums={**schemas.enums, prop.reference.class_name: prop})
303+
schemas = attr.evolve(schemas, enums={**schemas.enums, prop.reference.class_name: prop})
347304
return prop, schemas
348305

349306

@@ -356,11 +313,13 @@ def build_union_property(
356313
if isinstance(sub_prop, PropertyError):
357314
return PropertyError(detail=f"Invalid property in union {name}", data=sub_prop_data), schemas
358315
sub_properties.append(sub_prop)
316+
317+
default = convert_chain((prop._type_string for prop in sub_properties), data.default)
359318
return (
360319
UnionProperty(
361320
name=name,
362321
required=required,
363-
default=data.default,
322+
default=default,
364323
inner_properties=sub_properties,
365324
nullable=data.nullable,
366325
),
@@ -380,7 +339,7 @@ def build_list_property(
380339
ListProperty(
381340
name=name,
382341
required=required,
383-
default=data.default,
342+
default=None,
384343
inner_property=inner_prop,
385344
nullable=data.nullable,
386345
),
@@ -398,29 +357,27 @@ def _property_from_data(
398357
name = utils.remove_string_escapes(name)
399358
if isinstance(data, oai.Reference):
400359
reference = Reference.from_ref(data.ref)
401-
if reference.class_name in schemas.enums:
402-
existing = schemas.enums[reference.class_name]
360+
existing = schemas.enums.get(reference.class_name) or schemas.models.get(reference.class_name)
361+
if existing:
403362
return (
404-
replace(existing, required=required, name=name, title=reference.class_name, existing_enums={}),
363+
attr.evolve(existing, required=required, name=name),
405364
schemas,
406365
)
407-
elif reference.class_name in schemas.models:
408-
return replace(schemas.models[reference.class_name], required=required, name=name), schemas
409-
else:
410-
return PropertyError(data=data, detail="Could not find reference in parsed models or enums"), schemas
366+
return PropertyError(data=data, detail="Could not find reference in parsed models or enums"), schemas
411367
if data.enum:
412368
return build_enum_property(data=data, name=name, required=required, schemas=schemas, enum=data.enum)
413369
if data.anyOf or data.oneOf:
414370
return build_union_property(data=data, name=name, required=required, schemas=schemas)
415371
if not data.type:
416372
return PropertyError(data=data, detail="Schemas must either have one of enum, anyOf, or type defined."), schemas
373+
417374
if data.type == "string":
418375
return _string_based_property(name=name, required=required, data=data), schemas
419376
elif data.type == "number":
420377
return (
421378
FloatProperty(
422379
name=name,
423-
default=data.default,
380+
default=convert("float", data.default),
424381
required=required,
425382
nullable=data.nullable,
426383
),
@@ -430,7 +387,7 @@ def _property_from_data(
430387
return (
431388
IntProperty(
432389
name=name,
433-
default=data.default,
390+
default=convert("int", data.default),
434391
required=required,
435392
nullable=data.nullable,
436393
),
@@ -441,7 +398,7 @@ def _property_from_data(
441398
BooleanProperty(
442399
name=name,
443400
required=required,
444-
default=data.default,
401+
default=convert("bool", data.default),
445402
nullable=data.nullable,
446403
),
447404
schemas,

0 commit comments

Comments
 (0)