Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@


@with_generated_client_fixture(
"""
"""
components:
schemas:
MyModel:
Expand All @@ -29,7 +29,8 @@
properties:
req3: {"type": "string"}
required: ["req3"]
""")
"""
)
@with_generated_code_imports(
".models.MyModel",
".models.DerivedModel",
Expand Down Expand Up @@ -74,7 +75,7 @@ def test_type_hints(self, MyModel, Unset):


@with_generated_client_fixture(
"""
"""
components:
schemas:
MyModel:
Expand All @@ -89,7 +90,8 @@ def test_type_hints(self, MyModel, Unset):
anyProp: {}
AnyObject:
type: object
""")
"""
)
@with_generated_code_imports(
".models.MyModel",
".models.AnyObject",
Expand All @@ -104,7 +106,7 @@ def test_decode_encode(self, MyModel, AnyObject):
"intProp": 2,
"anyObjectProp": {"d": 3},
"nullProp": None,
"anyProp": "e"
"anyProp": "e",
}
expected_any_object = AnyObject()
expected_any_object.additional_properties = {"d": 3}
Expand All @@ -116,10 +118,10 @@ def test_decode_encode(self, MyModel, AnyObject):
string_prop="a",
number_prop=1.5,
int_prop=2,
any_object_prop = expected_any_object,
any_object_prop=expected_any_object,
null_prop=None,
any_prop="e",
)
),
)

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


@with_generated_client_fixture(
"""
"""
components:
schemas:
MyModel:
Expand All @@ -154,7 +156,8 @@ def test_type_hints(self, MyModel, Unset):
dateTimeProp: {"type": "string", "format": "date-time"}
uuidProp: {"type": "string", "format": "uuid"}
unknownFormatProp: {"type": "string", "format": "weird"}
""")
"""
)
@with_generated_code_imports(
".models.MyModel",
".types.Unset",
Expand Down Expand Up @@ -184,3 +187,59 @@ def test_type_hints(self, MyModel, Unset):
assert_model_property_type_hint(MyModel, "date_time_prop", Union[datetime.datetime, Unset])
assert_model_property_type_hint(MyModel, "uuid_prop", Union[uuid.UUID, Unset])
assert_model_property_type_hint(MyModel, "unknown_format_prop", Union[str, Unset])


@with_generated_client_fixture(
"""
components:
schemas:
MyModel:
type: object
properties:
booleanProp: {"type": "boolean"}
stringProp: {"type": "string"}
numberProp: {"type": "number"}
intProp: {"type": "integer"}
anyObjectProp: {"$ref": "#/components/schemas/AnyObject"}
nullProp: {"type": "null"}
anyProp: {}
AnyObject:
$ref: "#/components/schemas/OtherObject"
OtherObject:
$ref: "#/components/schemas/AnotherObject"
AnotherObject:
type: object
properties:
booleanProp: {"type": "boolean"}

"""
)
@with_generated_code_imports(
".models.MyModel",
".models.AnyObject",
".types.Unset",
)
class TestReferenceSchemaProperties:
def test_decode_encode(self, MyModel, AnyObject):
json_data = {
"booleanProp": True,
"stringProp": "a",
"numberProp": 1.5,
"intProp": 2,
"anyObjectProp": {"booleanProp": False},
"nullProp": None,
"anyProp": "e",
}
assert_model_decode_encode(
MyModel,
json_data,
MyModel(
boolean_prop=True,
string_prop="a",
number_prop=1.5,
int_prop=2,
any_object_prop=AnyObject(boolean_prop=False),
null_prop=None,
any_prop="e",
),
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import pytest

from end_to_end_tests.functional_tests.helpers import assert_bad_schema, with_generated_client_fixture


@with_generated_client_fixture(
"""
components:
schemas:
MyModel:
type: object
properties:
booleanProp: {"type": "boolean"}
stringProp: {"type": "string"}
numberProp: {"type": "number"}
intProp: {"type": "integer"}
anyObjectProp: {"$ref": "#/components/schemas/AnyObject"}
nullProp: {"type": "null"}
anyProp: {}
AnyObject:
$ref: "#/components/schemas/OtherObject"
OtherObject:
$ref: "#/components/schemas/AnyObject"

"""
)
class TestReferenceSchemaProperties:
def test_decode_encode(self, generated_client):
assert "Circular schema references found" in generated_client.generator_result.stdout
24 changes: 19 additions & 5 deletions openapi_python_client/parser/properties/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
Parameters,
ReferencePath,
Schemas,
get_reference_simple_name,
parse_reference_path,
update_parameters_with_data,
update_schemas_with_data,
Expand Down Expand Up @@ -324,17 +325,30 @@ def _create_schemas(
while still_making_progress:
still_making_progress = False
errors = []
next_round = []
next_round: list[tuple[str, oai.Reference | oai.Schema]] = []
# Only accumulate errors from the last round, since we might fix some along the way
for name, data in to_process:
if isinstance(data, oai.Reference):
schemas.errors.append(PropertyError(data=data, detail="Reference schemas are not supported."))
continue
schema_data: oai.Reference | oai.Schema | None = data
ref_path = parse_reference_path(f"#/components/schemas/{name}")
if isinstance(ref_path, ParseError):
schemas.errors.append(PropertyError(detail=ref_path.detail, data=data))
continue
schemas_or_err = update_schemas_with_data(ref_path=ref_path, data=data, schemas=schemas, config=config)
if isinstance(data, oai.Reference):
# Fully dereference reference schemas
seen = [name]
while isinstance(schema_data, oai.Reference):
data_ref_schema = get_reference_simple_name(schema_data.ref)
if data_ref_schema in seen:
schemas.errors.append(PropertyError(detail="Circular schema references found", data=data))
break
# use derefenced schema definition for this schema
schema_data = components.get(data_ref_schema)
if isinstance(schema_data, oai.Schema):
schemas_or_err = update_schemas_with_data(
ref_path=ref_path, data=schema_data, schemas=schemas, config=config
)
else:
schemas.errors.append(PropertyError(detail="Referent schema not found", data=data))
if isinstance(schemas_or_err, PropertyError):
next_round.append((name, data))
errors.append(schemas_or_err)
Expand Down
16 changes: 8 additions & 8 deletions tests/test_parser/test_properties/test_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -235,22 +235,22 @@ def test__string_based_property_binary_format(self, file_property_factory, confi


class TestCreateSchemas:
def test_skips_references_and_keeps_going(self, mocker, config):
components = {"a_ref": Reference.model_construct(), "a_schema": Schema.model_construct()}
def test_dereference_references(self, mocker, config):
components = {"a_ref": Reference(ref="#/components/schemas/a_schema"), "a_schema": Schema.model_construct()}
update_schemas_with_data = mocker.patch(f"{MODULE_NAME}.update_schemas_with_data")
parse_reference_path = mocker.patch(f"{MODULE_NAME}.parse_reference_path")
schemas = Schemas()

result = _create_schemas(components=components, schemas=schemas, config=config)
# Should not even try to parse a path for the Reference
parse_reference_path.assert_called_once_with("#/components/schemas/a_schema")
update_schemas_with_data.assert_called_once_with(

parse_reference_path.assert_has_calls(
[call("#/components/schemas/a_ref"), call("#/components/schemas/a_schema")]
)
update_schemas_with_data.assert_called_with(
ref_path=parse_reference_path.return_value,
config=config,
data=components["a_schema"],
schemas=Schemas(
errors=[PropertyError(detail="Reference schemas are not supported.", data=components["a_ref"])]
),
schemas=result,
)
assert result == update_schemas_with_data.return_value

Expand Down
Loading