Skip to content

Commit 9600afc

Browse files
committed
refactor: Clean up ModelProperty code
1 parent 772dc24 commit 9600afc

File tree

6 files changed

+273
-240
lines changed

6 files changed

+273
-240
lines changed

end_to_end_tests/openapi.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -716,7 +716,7 @@
716716
"a_not_required_date": {
717717
"title": "A Nullable Date",
718718
"type": "string",
719-
"format": "date",
719+
"format": "date"
720720
},
721721
"1_leading_digit": {
722722
"title": "Leading Digit",

openapi_python_client/parser/properties/__init__.py

Lines changed: 1 addition & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from ..reference import Reference
1010
from .converter import convert, convert_chain
1111
from .enum_property import EnumProperty
12-
from .model_property import ModelProperty
12+
from .model_property import ModelProperty, build_model_property
1313
from .property import Property
1414
from .schemas import Schemas
1515

@@ -234,98 +234,6 @@ def _string_based_property(
234234
)
235235

236236

237-
def build_model_property(
238-
*, data: oai.Schema, name: str, schemas: Schemas, required: bool, parent_name: Optional[str]
239-
) -> Tuple[Union[ModelProperty, PropertyError], Schemas]:
240-
"""
241-
A single ModelProperty from its OAI data
242-
243-
Args:
244-
data: Data of a single Schema
245-
name: Name by which the schema is referenced, such as a model name.
246-
Used to infer the type name if a `title` property is not available.
247-
schemas: Existing Schemas which have already been processed (to check name conflicts)
248-
"""
249-
required_set = set(data.required or [])
250-
required_properties: List[Property] = []
251-
optional_properties: List[Property] = []
252-
relative_imports: Set[str] = set()
253-
254-
class_name = data.title or name
255-
if parent_name:
256-
class_name = f"{utils.pascal_case(parent_name)}{utils.pascal_case(class_name)}"
257-
ref = Reference.from_ref(class_name)
258-
259-
all_props = data.properties or {}
260-
for sub_prop in data.allOf or []:
261-
if isinstance(sub_prop, oai.Reference):
262-
source_name = Reference.from_ref(sub_prop.ref).class_name
263-
sub_model = schemas.models.get(source_name)
264-
if sub_model is None:
265-
return PropertyError(f"Reference {sub_prop.ref} not found"), schemas
266-
required_properties.extend(sub_model.required_properties)
267-
optional_properties.extend(sub_model.optional_properties)
268-
relative_imports.update(sub_model.relative_imports)
269-
else:
270-
all_props.update(sub_prop.properties or {})
271-
required_set.update(sub_prop.required or [])
272-
273-
for key, value in all_props.items():
274-
prop_required = key in required_set
275-
prop, schemas = property_from_data(
276-
name=key, required=prop_required, data=value, schemas=schemas, parent_name=class_name
277-
)
278-
if isinstance(prop, PropertyError):
279-
return prop, schemas
280-
if prop_required and not prop.nullable:
281-
required_properties.append(prop)
282-
else:
283-
optional_properties.append(prop)
284-
relative_imports.update(prop.get_imports(prefix=".."))
285-
286-
additional_properties: Union[bool, Property, PropertyError]
287-
if data.additionalProperties is None:
288-
additional_properties = True
289-
elif isinstance(data.additionalProperties, bool):
290-
additional_properties = data.additionalProperties
291-
elif isinstance(data.additionalProperties, oai.Schema) and not any(data.additionalProperties.dict().values()):
292-
# An empty schema
293-
additional_properties = True
294-
else:
295-
assert isinstance(data.additionalProperties, (oai.Schema, oai.Reference))
296-
additional_properties, schemas = property_from_data(
297-
name="AdditionalProperty",
298-
required=True, # in the sense that if present in the dict will not be None
299-
data=data.additionalProperties,
300-
schemas=schemas,
301-
parent_name=class_name,
302-
)
303-
if isinstance(additional_properties, PropertyError):
304-
return additional_properties, schemas
305-
relative_imports.update(additional_properties.get_imports(prefix=".."))
306-
307-
prop = ModelProperty(
308-
reference=ref,
309-
required_properties=required_properties,
310-
optional_properties=optional_properties,
311-
relative_imports=relative_imports,
312-
description=data.description or "",
313-
default=None,
314-
nullable=data.nullable,
315-
required=required,
316-
name=name,
317-
additional_properties=additional_properties,
318-
)
319-
if prop.reference.class_name in schemas.models:
320-
error = PropertyError(
321-
data=data, detail=f'Attempted to generate duplicate models with name "{prop.reference.class_name}"'
322-
)
323-
return error, schemas
324-
325-
schemas = attr.evolve(schemas, models={**schemas.models, prop.reference.class_name: prop})
326-
return prop, schemas
327-
328-
329237
def build_enum_property(
330238
*,
331239
data: oai.Schema,

openapi_python_client/parser/properties/model_property.py

Lines changed: 124 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
1-
from typing import ClassVar, List, Set, Union
1+
from typing import ClassVar, List, NamedTuple, Optional, Set, Tuple, Union
22

33
import attr
44

5+
from ... import schema as oai
6+
from ... import utils
7+
from ..errors import PropertyError
58
from ..reference import Reference
69
from .property import Property
10+
from .schemas import Schemas
711

812

913
@attr.s(auto_attribs=True, frozen=True)
@@ -48,3 +52,122 @@ def get_imports(self, *, prefix: str) -> Set[str]:
4852
}
4953
)
5054
return imports
55+
56+
57+
class _PropertyData(NamedTuple):
58+
optional_props: List[Property]
59+
required_props: List[Property]
60+
relative_imports: Set[str]
61+
schemas: Schemas
62+
63+
64+
def _process_properties(*, data: oai.Schema, schemas: Schemas, class_name: str) -> Union[_PropertyData, PropertyError]:
65+
from . import property_from_data
66+
67+
required_properties: List[Property] = []
68+
optional_properties: List[Property] = []
69+
relative_imports: Set[str] = set()
70+
required_set = set(data.required or [])
71+
72+
all_props = data.properties or {}
73+
for sub_prop in data.allOf or []:
74+
if isinstance(sub_prop, oai.Reference):
75+
source_name = Reference.from_ref(sub_prop.ref).class_name
76+
sub_model = schemas.models.get(source_name)
77+
if sub_model is None:
78+
return PropertyError(f"Reference {sub_prop.ref} not found")
79+
required_properties.extend(sub_model.required_properties)
80+
optional_properties.extend(sub_model.optional_properties)
81+
relative_imports.update(sub_model.relative_imports)
82+
else:
83+
all_props.update(sub_prop.properties or {})
84+
required_set.update(sub_prop.required or [])
85+
86+
for key, value in all_props.items():
87+
prop_required = key in required_set
88+
prop, schemas = property_from_data(
89+
name=key, required=prop_required, data=value, schemas=schemas, parent_name=class_name
90+
)
91+
if isinstance(prop, PropertyError):
92+
return prop
93+
if prop_required and not prop.nullable:
94+
required_properties.append(prop)
95+
else:
96+
optional_properties.append(prop)
97+
relative_imports.update(prop.get_imports(prefix=".."))
98+
99+
return _PropertyData(
100+
optional_props=optional_properties,
101+
required_props=required_properties,
102+
relative_imports=relative_imports,
103+
schemas=schemas,
104+
)
105+
106+
107+
def build_model_property(
108+
*, data: oai.Schema, name: str, schemas: Schemas, required: bool, parent_name: Optional[str]
109+
) -> Tuple[Union[ModelProperty, PropertyError], Schemas]:
110+
"""
111+
A single ModelProperty from its OAI data
112+
113+
Args:
114+
data: Data of a single Schema
115+
name: Name by which the schema is referenced, such as a model name.
116+
Used to infer the type name if a `title` property is not available.
117+
schemas: Existing Schemas which have already been processed (to check name conflicts)
118+
required: Whether or not this property is required by the parent (affects typing)
119+
parent_name: The name of the property that this property is inside of (affects class naming)
120+
"""
121+
from . import property_from_data
122+
123+
class_name = data.title or name
124+
if parent_name:
125+
class_name = f"{utils.pascal_case(parent_name)}{utils.pascal_case(class_name)}"
126+
ref = Reference.from_ref(class_name)
127+
128+
property_data = _process_properties(data=data, schemas=schemas, class_name=class_name)
129+
if isinstance(property_data, PropertyError):
130+
return property_data, schemas
131+
schemas = property_data.schemas
132+
133+
additional_properties: Union[bool, Property, PropertyError]
134+
if data.additionalProperties is None:
135+
additional_properties = True
136+
elif isinstance(data.additionalProperties, bool):
137+
additional_properties = data.additionalProperties
138+
elif isinstance(data.additionalProperties, oai.Schema) and not any(data.additionalProperties.dict().values()):
139+
# An empty schema
140+
additional_properties = True
141+
else:
142+
assert isinstance(data.additionalProperties, (oai.Schema, oai.Reference))
143+
additional_properties, schemas = property_from_data(
144+
name="AdditionalProperty",
145+
required=True, # in the sense that if present in the dict will not be None
146+
data=data.additionalProperties,
147+
schemas=schemas,
148+
parent_name=class_name,
149+
)
150+
if isinstance(additional_properties, PropertyError):
151+
return additional_properties, schemas
152+
property_data.relative_imports.update(additional_properties.get_imports(prefix=".."))
153+
154+
prop = ModelProperty(
155+
reference=ref,
156+
required_properties=property_data.required_props,
157+
optional_properties=property_data.optional_props,
158+
relative_imports=property_data.relative_imports,
159+
description=data.description or "",
160+
default=None,
161+
nullable=data.nullable,
162+
required=required,
163+
name=name,
164+
additional_properties=additional_properties,
165+
)
166+
if prop.reference.class_name in schemas.models:
167+
error = PropertyError(
168+
data=data, detail=f'Attempted to generate duplicate models with name "{prop.reference.class_name}"'
169+
)
170+
return error, schemas
171+
172+
schemas = attr.evolve(schemas, models={**schemas.models, prop.reference.class_name: prop})
173+
return prop, schemas

openapi_python_client/parser/properties/schemas.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
11
__all__ = ["Schemas"]
22

3-
from typing import Dict, List
3+
from typing import TYPE_CHECKING, Dict, List
44

55
import attr
66

77
from ..errors import ParseError
8-
from .enum_property import EnumProperty
9-
from .model_property import ModelProperty
8+
9+
if TYPE_CHECKING:
10+
from .enum_property import EnumProperty
11+
from .model_property import ModelProperty
12+
else:
13+
EnumProperty = "EnumProperty"
14+
ModelProperty = "ModelProperty"
1015

1116

1217
@attr.s(auto_attribs=True, frozen=True)

0 commit comments

Comments
 (0)