Skip to content

Commit 64b246d

Browse files
authored
Reference schemas (#2)
* feat: Enable reference schema parsing * test: Add tests for reference schemas
1 parent e3f907b commit 64b246d

File tree

4 files changed

+124
-22
lines changed

4 files changed

+124
-22
lines changed

end_to_end_tests/functional_tests/generated_code_execution/test_properties.py

Lines changed: 68 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212

1313

1414
@with_generated_client_fixture(
15-
"""
15+
"""
1616
components:
1717
schemas:
1818
MyModel:
@@ -29,7 +29,8 @@
2929
properties:
3030
req3: {"type": "string"}
3131
required: ["req3"]
32-
""")
32+
"""
33+
)
3334
@with_generated_code_imports(
3435
".models.MyModel",
3536
".models.DerivedModel",
@@ -74,7 +75,7 @@ def test_type_hints(self, MyModel, Unset):
7475

7576

7677
@with_generated_client_fixture(
77-
"""
78+
"""
7879
components:
7980
schemas:
8081
MyModel:
@@ -89,7 +90,8 @@ def test_type_hints(self, MyModel, Unset):
8990
anyProp: {}
9091
AnyObject:
9192
type: object
92-
""")
93+
"""
94+
)
9395
@with_generated_code_imports(
9496
".models.MyModel",
9597
".models.AnyObject",
@@ -104,7 +106,7 @@ def test_decode_encode(self, MyModel, AnyObject):
104106
"intProp": 2,
105107
"anyObjectProp": {"d": 3},
106108
"nullProp": None,
107-
"anyProp": "e"
109+
"anyProp": "e",
108110
}
109111
expected_any_object = AnyObject()
110112
expected_any_object.additional_properties = {"d": 3}
@@ -116,10 +118,10 @@ def test_decode_encode(self, MyModel, AnyObject):
116118
string_prop="a",
117119
number_prop=1.5,
118120
int_prop=2,
119-
any_object_prop = expected_any_object,
121+
any_object_prop=expected_any_object,
120122
null_prop=None,
121123
any_prop="e",
122-
)
124+
),
123125
)
124126

125127
@pytest.mark.parametrize(
@@ -144,7 +146,7 @@ def test_type_hints(self, MyModel, Unset):
144146

145147

146148
@with_generated_client_fixture(
147-
"""
149+
"""
148150
components:
149151
schemas:
150152
MyModel:
@@ -154,7 +156,8 @@ def test_type_hints(self, MyModel, Unset):
154156
dateTimeProp: {"type": "string", "format": "date-time"}
155157
uuidProp: {"type": "string", "format": "uuid"}
156158
unknownFormatProp: {"type": "string", "format": "weird"}
157-
""")
159+
"""
160+
)
158161
@with_generated_code_imports(
159162
".models.MyModel",
160163
".types.Unset",
@@ -184,3 +187,59 @@ def test_type_hints(self, MyModel, Unset):
184187
assert_model_property_type_hint(MyModel, "date_time_prop", Union[datetime.datetime, Unset])
185188
assert_model_property_type_hint(MyModel, "uuid_prop", Union[uuid.UUID, Unset])
186189
assert_model_property_type_hint(MyModel, "unknown_format_prop", Union[str, Unset])
190+
191+
192+
@with_generated_client_fixture(
193+
"""
194+
components:
195+
schemas:
196+
MyModel:
197+
type: object
198+
properties:
199+
booleanProp: {"type": "boolean"}
200+
stringProp: {"type": "string"}
201+
numberProp: {"type": "number"}
202+
intProp: {"type": "integer"}
203+
anyObjectProp: {"$ref": "#/components/schemas/AnyObject"}
204+
nullProp: {"type": "null"}
205+
anyProp: {}
206+
AnyObject:
207+
$ref: "#/components/schemas/OtherObject"
208+
OtherObject:
209+
$ref: "#/components/schemas/AnotherObject"
210+
AnotherObject:
211+
type: object
212+
properties:
213+
booleanProp: {"type": "boolean"}
214+
215+
"""
216+
)
217+
@with_generated_code_imports(
218+
".models.MyModel",
219+
".models.AnyObject",
220+
".types.Unset",
221+
)
222+
class TestReferenceSchemaProperties:
223+
def test_decode_encode(self, MyModel, AnyObject):
224+
json_data = {
225+
"booleanProp": True,
226+
"stringProp": "a",
227+
"numberProp": 1.5,
228+
"intProp": 2,
229+
"anyObjectProp": {"booleanProp": False},
230+
"nullProp": None,
231+
"anyProp": "e",
232+
}
233+
assert_model_decode_encode(
234+
MyModel,
235+
json_data,
236+
MyModel(
237+
boolean_prop=True,
238+
string_prop="a",
239+
number_prop=1.5,
240+
int_prop=2,
241+
any_object_prop=AnyObject(boolean_prop=False),
242+
null_prop=None,
243+
any_prop="e",
244+
),
245+
)
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import pytest
2+
3+
from end_to_end_tests.functional_tests.helpers import assert_bad_schema, with_generated_client_fixture
4+
5+
6+
@with_generated_client_fixture(
7+
"""
8+
components:
9+
schemas:
10+
MyModel:
11+
type: object
12+
properties:
13+
booleanProp: {"type": "boolean"}
14+
stringProp: {"type": "string"}
15+
numberProp: {"type": "number"}
16+
intProp: {"type": "integer"}
17+
anyObjectProp: {"$ref": "#/components/schemas/AnyObject"}
18+
nullProp: {"type": "null"}
19+
anyProp: {}
20+
AnyObject:
21+
$ref: "#/components/schemas/OtherObject"
22+
OtherObject:
23+
$ref: "#/components/schemas/AnyObject"
24+
25+
"""
26+
)
27+
class TestReferenceSchemaProperties:
28+
def test_decode_encode(self, generated_client):
29+
assert "Circular schema references found" in generated_client.generator_result.stdout

openapi_python_client/parser/properties/__init__.py

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
Parameters,
4141
ReferencePath,
4242
Schemas,
43+
get_reference_simple_name,
4344
parse_reference_path,
4445
update_parameters_with_data,
4546
update_schemas_with_data,
@@ -324,17 +325,30 @@ def _create_schemas(
324325
while still_making_progress:
325326
still_making_progress = False
326327
errors = []
327-
next_round = []
328+
next_round: list[tuple[str, oai.Reference | oai.Schema]] = []
328329
# Only accumulate errors from the last round, since we might fix some along the way
329330
for name, data in to_process:
330-
if isinstance(data, oai.Reference):
331-
schemas.errors.append(PropertyError(data=data, detail="Reference schemas are not supported."))
332-
continue
331+
schema_data: oai.Reference | oai.Schema | None = data
333332
ref_path = parse_reference_path(f"#/components/schemas/{name}")
334333
if isinstance(ref_path, ParseError):
335334
schemas.errors.append(PropertyError(detail=ref_path.detail, data=data))
336335
continue
337-
schemas_or_err = update_schemas_with_data(ref_path=ref_path, data=data, schemas=schemas, config=config)
336+
if isinstance(data, oai.Reference):
337+
# Fully dereference reference schemas
338+
seen = [name]
339+
while isinstance(schema_data, oai.Reference):
340+
data_ref_schema = get_reference_simple_name(schema_data.ref)
341+
if data_ref_schema in seen:
342+
schemas.errors.append(PropertyError(detail="Circular schema references found", data=data))
343+
break
344+
# use derefenced schema definition for this schema
345+
schema_data = components.get(data_ref_schema)
346+
if isinstance(schema_data, oai.Schema):
347+
schemas_or_err = update_schemas_with_data(
348+
ref_path=ref_path, data=schema_data, schemas=schemas, config=config
349+
)
350+
else:
351+
schemas.errors.append(PropertyError(detail="Referent schema not found", data=data))
338352
if isinstance(schemas_or_err, PropertyError):
339353
next_round.append((name, data))
340354
errors.append(schemas_or_err)

tests/test_parser/test_properties/test_init.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -235,22 +235,22 @@ def test__string_based_property_binary_format(self, file_property_factory, confi
235235

236236

237237
class TestCreateSchemas:
238-
def test_skips_references_and_keeps_going(self, mocker, config):
239-
components = {"a_ref": Reference.model_construct(), "a_schema": Schema.model_construct()}
238+
def test_dereference_references(self, mocker, config):
239+
components = {"a_ref": Reference(ref="#/components/schemas/a_schema"), "a_schema": Schema.model_construct()}
240240
update_schemas_with_data = mocker.patch(f"{MODULE_NAME}.update_schemas_with_data")
241241
parse_reference_path = mocker.patch(f"{MODULE_NAME}.parse_reference_path")
242242
schemas = Schemas()
243243

244244
result = _create_schemas(components=components, schemas=schemas, config=config)
245-
# Should not even try to parse a path for the Reference
246-
parse_reference_path.assert_called_once_with("#/components/schemas/a_schema")
247-
update_schemas_with_data.assert_called_once_with(
245+
246+
parse_reference_path.assert_has_calls(
247+
[call("#/components/schemas/a_ref"), call("#/components/schemas/a_schema")]
248+
)
249+
update_schemas_with_data.assert_called_with(
248250
ref_path=parse_reference_path.return_value,
249251
config=config,
250252
data=components["a_schema"],
251-
schemas=Schemas(
252-
errors=[PropertyError(detail="Reference schemas are not supported.", data=components["a_ref"])]
253-
),
253+
schemas=result,
254254
)
255255
assert result == update_schemas_with_data.return_value
256256

0 commit comments

Comments
 (0)