Skip to content
Draft
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
77 changes: 77 additions & 0 deletions openapi_python_client/parser/properties/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,16 @@ def property_from_data( # noqa: PLR0911, PLR0912
process_properties=process_properties,
roots=roots,
)
if data.allOf and _all_of_is_values(data.allOf, schemas):
return _property_from_values_all_of(
name=name,
required=required,
data=data,
schemas=schemas,
parent_name=parent_name,
config=config,
roots=roots,
)
if data.type == oai.DataType.OBJECT or data.allOf or (data.type is None and data.properties):
return ModelProperty.build(
data=data,
Expand Down Expand Up @@ -472,3 +482,70 @@ def build_parameters(

parameters.errors.extend(errors)
return parameters


def _all_of_is_values(parts: list[oai.Reference | oai.Schema], schemas: Schemas) -> bool:
"""Whether every part of an allOf describes a value (an enum, string, number...) rather than an object.

Such an allOf narrows a value, for example a reference to an enum plus `enum: [ONE_VALUE]`. It is merged
property by property instead of being built as a model.
"""
for part in parts:
if isinstance(part, oai.Reference):
ref_path = parse_reference_path(part.ref)
if isinstance(ref_path, ParseError):
return False
existing = schemas.classes_by_reference.get(ref_path)
if existing is None or isinstance(existing, ModelProperty):
return False
continue
if part.type == oai.DataType.OBJECT or part.properties or part.allOf or part.anyOf or part.oneOf:
return False
return True


def _property_from_values_all_of(
*,
name: UntrustedString,
required: bool,
data: oai.Schema,
schemas: Schemas,
parent_name: str,
config: Config,
roots: set[ReferencePath | strings.ClassName],
) -> tuple[Property | PropertyError, Schemas]:
"""Merge the parts of a value allOf into one property, using the same rules as merging model properties."""
from .merge_properties import merge_properties # noqa: PLC0415

merged: Property | None = None
for part in data.allOf:
prop, schemas = property_from_data(
name=name,
required=required,
data=part,
schemas=schemas,
parent_name=parent_name,
config=config,
roots=roots,
)
if isinstance(prop, PropertyError):
return prop, schemas
if merged is None:
merged = prop
continue
result = merge_properties(merged, prop)
if isinstance(result, PropertyError):
result.data = data
return result, schemas
merged = result
assert merged is not None
return (
evolve(
merged,
required=required,
name=name,
python_name=strings.PythonIdentifier(value=name, prefix=config.field_prefix),
description=data.description or merged.description,
),
schemas,
)
4 changes: 4 additions & 0 deletions openapi_python_client/parser/properties/model_property.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,10 @@ def build(
python_name=strings.PythonIdentifier(value=name, prefix=config.field_prefix),
example=data.example,
)
existing = schemas.classes_by_name.get(class_info.name)
if isinstance(existing, ModelProperty) and existing.data == data:
# The same schema reached again, e.g. through a one-option oneOf: reuse its class.
return evolve(existing, required=required, name=name, python_name=prop.python_name), schemas
if class_info.name in schemas.classes_by_name:
error = PropertyError(
data=data, detail=f'Attempted to generate duplicate models with name "{class_info.name}"'
Expand Down