Skip to content

Commit 2e0d00c

Browse files
committed
fix: correctly resolve references to a type that is itself just a single allOf reference
1 parent 3fb5fb2 commit 2e0d00c

File tree

5 files changed

+92
-16
lines changed

5 files changed

+92
-16
lines changed

openapi_python_client/parser/bodies.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ def body_from_data(
117117
**schemas.classes_by_name,
118118
prop.class_info.name: prop,
119119
},
120+
models_to_process=[*schemas.models_to_process, prop],
120121
)
121122
bodies.append(
122123
Body(

openapi_python_client/parser/properties/__init__.py

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ def _property_from_ref(
126126
return prop, schemas
127127

128128

129-
def property_from_data( # noqa: PLR0911
129+
def property_from_data( # noqa: PLR0911, PLR0912
130130
name: str,
131131
required: bool,
132132
data: oai.Reference | oai.Schema,
@@ -153,7 +153,7 @@ def property_from_data( # noqa: PLR0911
153153
sub_data: list[oai.Schema | oai.Reference] = data.allOf + data.anyOf + data.oneOf
154154
# A union of a single reference should just be passed through to that reference (don't create copy class)
155155
if len(sub_data) == 1 and isinstance(sub_data[0], oai.Reference):
156-
return _property_from_ref(
156+
prop, schemas = _property_from_ref(
157157
name=name,
158158
required=required,
159159
parent=data,
@@ -162,6 +162,17 @@ def property_from_data( # noqa: PLR0911
162162
config=config,
163163
roots=roots,
164164
)
165+
if not isinstance(prop, PropertyError):
166+
# We won't be generating a separate Python class for this schema - references to it will just use
167+
# the class for the schema it's referencing - so we don't add it to classes_by_name; but we do
168+
# add it to models_to_process, if it's a model, because its properties still need to be resolved.
169+
if isinstance(prop, ModelProperty):
170+
schemas = evolve(
171+
schemas,
172+
models_to_process=[*schemas.models_to_process, prop],
173+
)
174+
return prop, schemas
175+
165176
if data.type == oai.DataType.BOOLEAN:
166177
return (
167178
BooleanProperty.build(
@@ -341,7 +352,7 @@ def _process_model_errors(
341352

342353

343354
def _process_models(*, schemas: Schemas, config: Config) -> Schemas:
344-
to_process = (prop for prop in schemas.classes_by_name.values() if isinstance(prop, ModelProperty))
355+
to_process = schemas.models_to_process
345356
still_making_progress = True
346357
final_model_errors: list[tuple[ModelProperty, PropertyError]] = []
347358
latest_model_errors: list[tuple[ModelProperty, PropertyError]] = []
@@ -368,12 +379,11 @@ def _process_models(*, schemas: Schemas, config: Config) -> Schemas:
368379
continue
369380
schemas = schemas_or_err
370381
still_making_progress = True
371-
to_process = (prop for prop in next_round)
382+
to_process = next_round
372383

373384
final_model_errors.extend(latest_model_errors)
374385
errors = _process_model_errors(final_model_errors, schemas=schemas)
375-
schemas.errors.extend(errors)
376-
return schemas
386+
return evolve(schemas, errors=[*schemas.errors, *errors], models_to_process=to_process)
377387

378388

379389
def build_schemas(

openapi_python_client/parser/properties/model_property.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,11 @@ def build(
119119
)
120120
return error, schemas
121121

122-
schemas = evolve(schemas, classes_by_name={**schemas.classes_by_name, class_info.name: prop})
122+
schemas = evolve(
123+
schemas,
124+
classes_by_name={**schemas.classes_by_name, class_info.name: prop},
125+
models_to_process=[*schemas.models_to_process, prop],
126+
)
123127
return prop, schemas
124128

125129
@classmethod

openapi_python_client/parser/properties/schemas.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,10 @@
2222
from ..errors import ParameterError, ParseError, PropertyError
2323

2424
if TYPE_CHECKING: # pragma: no cover
25+
from .model_property import ModelProperty
2526
from .property import Property
2627
else:
28+
ModelProperty = "ModelProperty"
2729
Property = "Property"
2830

2931

@@ -77,6 +79,7 @@ class Schemas:
7779
classes_by_reference: Dict[ReferencePath, Property] = field(factory=dict)
7880
dependencies: Dict[ReferencePath, Set[Union[ReferencePath, ClassName]]] = field(factory=dict)
7981
classes_by_name: Dict[ClassName, Property] = field(factory=dict)
82+
models_to_process: List[ModelProperty] = field(factory=list)
8083
errors: List[ParseError] = field(factory=list)
8184

8285
def add_dependencies(self, ref_path: ReferencePath, roots: Set[Union[ReferencePath, ClassName]]) -> None:

tests/test_parser/test_properties/test_init.py

Lines changed: 67 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -530,6 +530,7 @@ def test_property_from_data_ref_enum_with_overridden_default(self, enum_property
530530
prop, new_schemas = property_from_data(
531531
name=name, required=required, data=data, schemas=schemas, parent_name="", config=config
532532
)
533+
new_schemas = attr.evolve(new_schemas, models_to_process=[]) # intermediate state irrelevant to this test
533534

534535
assert prop == enum_property_factory(
535536
name="some_enum",
@@ -918,15 +919,18 @@ def test_retries_failing_models_while_making_progress(
918919

919920
first_model = model_property_factory()
920921
second_class_name = ClassName("second", "")
922+
second_model = model_property_factory()
923+
any_prop = any_property_factory()
921924
schemas = Schemas(
922925
classes_by_name={
923926
ClassName("first", ""): first_model,
924-
second_class_name: model_property_factory(),
925-
ClassName("non-model", ""): any_property_factory(),
926-
}
927+
second_class_name: second_model,
928+
ClassName("non-model", ""): any_prop,
929+
},
930+
models_to_process=[first_model, second_model],
927931
)
928932
process_model = mocker.patch(
929-
f"{MODULE_NAME}.process_model", side_effect=[PropertyError(), Schemas(), PropertyError()]
933+
f"{MODULE_NAME}.process_model", side_effect=[PropertyError(), schemas, PropertyError()]
930934
)
931935
process_model_errors = mocker.patch(f"{MODULE_NAME}._process_model_errors", return_value=["error"])
932936

@@ -935,8 +939,8 @@ def test_retries_failing_models_while_making_progress(
935939
process_model.assert_has_calls(
936940
[
937941
call(first_model, schemas=schemas, config=config),
938-
call(schemas.classes_by_name[second_class_name], schemas=schemas, config=config),
939-
call(first_model, schemas=result, config=config),
942+
call(second_model, schemas=schemas, config=config),
943+
call(first_model, schemas=schemas, config=config),
940944
]
941945
)
942946
assert process_model_errors.was_called_once_with([(first_model, PropertyError())])
@@ -950,14 +954,16 @@ def test_detect_recursive_allof_reference_no_retry(self, mocker, model_property_
950954
recursive_model = model_property_factory(
951955
class_info=Class(name=class_name, module_name=PythonIdentifier("module_name", ""))
952956
)
957+
second_model = model_property_factory()
953958
schemas = Schemas(
954959
classes_by_name={
955960
"recursive": recursive_model,
956-
"second": model_property_factory(),
957-
}
961+
"second": second_model,
962+
},
963+
models_to_process=[recursive_model, second_model],
958964
)
959965
recursion_error = PropertyError(data=Reference.model_construct(ref=f"#/{class_name}"))
960-
process_model = mocker.patch(f"{MODULE_NAME}.process_model", side_effect=[recursion_error, Schemas()])
966+
process_model = mocker.patch(f"{MODULE_NAME}.process_model", side_effect=[recursion_error, schemas])
961967
process_model_errors = mocker.patch(f"{MODULE_NAME}._process_model_errors", return_value=["error"])
962968

963969
result = _process_models(schemas=schemas, config=config)
@@ -972,6 +978,58 @@ def test_detect_recursive_allof_reference_no_retry(self, mocker, model_property_
972978
assert all(error in result.errors for error in process_model_errors.return_value)
973979
assert "\n\nRecursive allOf reference found" in recursion_error.detail
974980

981+
def test_resolve_reference_to_single_allof_reference(self, config, model_property_factory):
982+
# test for https://github.com/openapi-generators/openapi-python-client/issues/1091
983+
from openapi_python_client.parser.properties import Schemas, build_schemas
984+
985+
components = {
986+
"Model1": oai.Schema.model_construct(
987+
type="object",
988+
properties={
989+
"prop1": oai.Schema.model_construct(type="string"),
990+
},
991+
),
992+
"Model2": oai.Schema.model_construct(
993+
allOf=[
994+
oai.Reference.model_construct(ref="#/components/schemas/Model1"),
995+
]
996+
),
997+
"Model3": oai.Schema.model_construct(
998+
allOf=[
999+
oai.Reference.model_construct(ref="#/components/schemas/Model2"),
1000+
oai.Schema.model_construct(
1001+
type="object",
1002+
properties={
1003+
"prop2": oai.Schema.model_construct(type="string"),
1004+
},
1005+
),
1006+
],
1007+
),
1008+
}
1009+
schemas = Schemas()
1010+
1011+
result = build_schemas(components=components, schemas=schemas, config=config)
1012+
1013+
assert result.errors == []
1014+
assert result.models_to_process == []
1015+
1016+
# Classes should only be generated for Model1 and Model3
1017+
assert result.classes_by_name.keys() == {"Model1", "Model3"}
1018+
1019+
# References to Model2 should be resolved to the same class as Model1
1020+
assert result.classes_by_reference.keys() == {
1021+
"/components/schemas/Model1",
1022+
"/components/schemas/Model2",
1023+
"/components/schemas/Model3",
1024+
}
1025+
assert (
1026+
result.classes_by_reference["/components/schemas/Model2"].class_info
1027+
== result.classes_by_reference["/components/schemas/Model1"].class_info
1028+
)
1029+
1030+
# Verify that Model3 extended the properties from Model1
1031+
assert [p.name for p in result.classes_by_name["Model3"].optional_properties] == ["prop1", "prop2"]
1032+
9751033

9761034
class TestPropogateRemoval:
9771035
def test_propogate_removal_class_name(self):

0 commit comments

Comments
 (0)