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
66 changes: 66 additions & 0 deletions src/datamodel_code_generator/parser/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -485,6 +485,71 @@ def sort_data_models( # noqa: PLR0912, PLR0915
return unresolved_references, sorted_data_models, require_update_action_models


def sort_base_classes_for_mro(sorted_data_models: SortedDataModels) -> None:
"""Sort base classes in each model to ensure valid Python MRO.

When a class inherits from multiple base classes where some bases inherit
from others, Python's C3 linearization requires that child classes appear
before their parent classes in the inheritance list.

For example, if B inherits from A, then class C(A, B) is invalid but
class C(B, A) is valid.
"""
for model in sorted_data_models.values():
base_classes = model.base_classes
if len(base_classes) <= 1:
continue

# Build set of base class paths for quick lookup
base_class_paths = {b.reference.path for b in base_classes if b.reference}

def get_ancestors(
ref_path: str,
base_class_paths: set[str] = base_class_paths,
) -> set[str]:
"""Get all ancestor paths that are in our base class list."""
ancestors: set[str] = set()
source_model = sorted_data_models.get(ref_path)
if source_model is None: # pragma: no cover
return ancestors
to_visit = [
bc.reference.path
for bc in source_model.base_classes
if bc.reference and bc.reference.path in base_class_paths
]
while to_visit:
parent_path = to_visit.pop()
if parent_path in ancestors:
continue
ancestors.add(parent_path)
parent_model = sorted_data_models.get(parent_path)
if not parent_model:
continue
to_visit.extend(
bc.reference.path
for bc in parent_model.base_classes
if bc.reference and bc.reference.path in base_class_paths
)
return ancestors

# Build ancestor map for each base class
ancestor_map = {b.reference.path: get_ancestors(b.reference.path) for b in base_classes if b.reference}

def sort_key(
bc: BaseClassDataType,
ancestor_map: dict[str, set[str]] = ancestor_map,
) -> int:
"""Sort key: classes that are ancestors of others come later."""
if not bc.reference:
return 0
path = bc.reference.path
# Count how many other base classes have this one as an ancestor
return sum(1 for other_path in ancestor_map if path in ancestor_map.get(other_path, set()))

# Use stable sort to preserve original order for elements with equal keys
model.base_classes = sorted(base_classes, key=sort_key)


def relative(
current_module: str,
reference: str,
Expand Down Expand Up @@ -3161,6 +3226,7 @@ def parse( # noqa: PLR0913, PLR0914, PLR0917
)

_, sorted_data_models, require_update_action_models = sort_data_models(self.results)
sort_base_classes_for_mro(sorted_data_models)

(
module_models,
Expand Down
121 changes: 121 additions & 0 deletions tests/data/expected/main/graphql/interface_mro.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
# generated by datamodel-codegen:
# filename: interface_mro.graphql
# timestamp: 2019-07-26T00:00:00+00:00

from __future__ import annotations

from typing import Annotated, Literal

from pydantic import BaseModel, Field
from typing_extensions import TypeAliasType

Boolean = TypeAliasType("Boolean", bool)
"""
The `Boolean` scalar type represents `true` or `false`.
"""


ID = TypeAliasType("ID", str)
"""
The `ID` scalar type represents a unique identifier, often used to refetch an object or as key for a cache. The ID type appears in a JSON response as a String; however, it is not intended to be human-readable. When expected as an input type, any string (such as `"4"`) or integer (such as `4`) input value will be accepted as an ID.
"""


String = TypeAliasType("String", str)
"""
The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text.
"""


class BaseInterface(BaseModel):
base_field: Annotated[String | None, Field(alias='baseField')] = None
typename__: Annotated[
Literal['BaseInterface'] | None, Field(alias='__typename')
] = 'BaseInterface'


class Entity(BaseModel):
id: ID
typename__: Annotated[Literal['Entity'] | None, Field(alias='__typename')] = (
'Entity'
)


class GrandParent(BaseModel):
grand_field: Annotated[String | None, Field(alias='grandField')] = None
typename__: Annotated[Literal['GrandParent'] | None, Field(alias='__typename')] = (
'GrandParent'
)


class LeftBranch(BaseInterface):
base_field: Annotated[String | None, Field(alias='baseField')] = None
left_field: Annotated[String | None, Field(alias='leftField')] = None
typename__: Annotated[Literal['LeftBranch'] | None, Field(alias='__typename')] = (
'LeftBranch'
)


class Node(BaseModel):
id: ID
typename__: Annotated[Literal['Node'] | None, Field(alias='__typename')] = 'Node'


class Notification(Node):
id: ID
read_at: Annotated[String | None, Field(alias='readAt')] = None
typename__: Annotated[Literal['Notification'] | None, Field(alias='__typename')] = (
'Notification'
)


class Parent(GrandParent):
grand_field: Annotated[String | None, Field(alias='grandField')] = None
parent_field: Annotated[String | None, Field(alias='parentField')] = None
typename__: Annotated[Literal['Parent'] | None, Field(alias='__typename')] = (
'Parent'
)


class RightBranch(BaseInterface):
base_field: Annotated[String | None, Field(alias='baseField')] = None
right_field: Annotated[String | None, Field(alias='rightField')] = None
typename__: Annotated[Literal['RightBranch'] | None, Field(alias='__typename')] = (
'RightBranch'
)


class CustomerNeedNotification(Entity, Notification, Node):
customer: String | None = None
id: ID
read_at: Annotated[String | None, Field(alias='readAt')] = None
typename__: Annotated[
Literal['CustomerNeedNotification'] | None, Field(alias='__typename')
] = 'CustomerNeedNotification'


class DiamondType(LeftBranch, RightBranch, BaseInterface):
base_field: Annotated[String | None, Field(alias='baseField')] = None
diamond_field: Annotated[String | None, Field(alias='diamondField')] = None
left_field: Annotated[String | None, Field(alias='leftField')] = None
right_field: Annotated[String | None, Field(alias='rightField')] = None
typename__: Annotated[Literal['DiamondType'] | None, Field(alias='__typename')] = (
'DiamondType'
)


class Child(Parent, GrandParent):
child_field: Annotated[String | None, Field(alias='childField')] = None
grand_field: Annotated[String | None, Field(alias='grandField')] = None
parent_field: Annotated[String | None, Field(alias='parentField')] = None
typename__: Annotated[Literal['Child'] | None, Field(alias='__typename')] = 'Child'


class LeafType(Child, Parent, GrandParent):
child_field: Annotated[String | None, Field(alias='childField')] = None
grand_field: Annotated[String | None, Field(alias='grandField')] = None
leaf_field: Annotated[String | None, Field(alias='leafField')] = None
parent_field: Annotated[String | None, Field(alias='parentField')] = None
typename__: Annotated[Literal['LeafType'] | None, Field(alias='__typename')] = (
'LeafType'
)
67 changes: 67 additions & 0 deletions tests/data/expected/main/graphql/union_class_name_prefix.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# generated by datamodel-codegen:
# filename: union_class_name_prefix.graphql
# timestamp: 2019-07-26T00:00:00+00:00

from __future__ import annotations

from typing import Annotated, Literal, Union

from pydantic import BaseModel, Field
from typing_extensions import TypeAliasType

FooBoolean = TypeAliasType("FooBoolean", bool)
"""
The `Boolean` scalar type represents `true` or `false`.
"""


FooID = TypeAliasType("FooID", str)
"""
The `ID` scalar type represents a unique identifier, often used to refetch an object or as key for a cache. The ID type appears in a JSON response as a String; however, it is not intended to be human-readable. When expected as an input type, any string (such as `"4"`) or integer (such as `4`) input value will be accepted as an ID.
"""


FooInt = TypeAliasType("FooInt", int)
"""
The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1.
"""


FooString = TypeAliasType("FooString", str)
"""
The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text.
"""


class FooIResource(BaseModel):
id: FooID
typename__: Annotated[Literal['IResource'] | None, Field(alias='__typename')] = (
'IResource'
)


class FooCar(FooIResource):
id: FooID
passenger_capacity: Annotated[FooInt, Field(alias='passengerCapacity')]
typename__: Annotated[Literal['Car'] | None, Field(alias='__typename')] = 'Car'


class FooEmployee(FooIResource):
first_name: Annotated[FooString | None, Field(alias='firstName')] = None
id: FooID
last_name: Annotated[FooString | None, Field(alias='lastName')] = None
typename__: Annotated[Literal['Employee'] | None, Field(alias='__typename')] = (
'Employee'
)


FooResource = TypeAliasType(
"FooResource",
Union[
'FooCar',
'FooEmployee',
],
)


FooTechnicalResource = TypeAliasType("FooTechnicalResource", FooCar)
31 changes: 31 additions & 0 deletions tests/data/expected/main/jsonschema/allof_mro.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# generated by datamodel-codegen:
# filename: allof_mro.json
# timestamp: 2019-07-26T00:00:00+00:00

from __future__ import annotations

from pydantic import BaseModel


class BaseInterface(BaseModel):
id: str | None = None


class LeftBranch(BaseInterface):
left_value: str | None = None


class RightBranch(BaseInterface):
right_value: str | None = None


class DiamondType(LeftBranch, RightBranch, BaseInterface):
"""
Diamond inheritance pattern - inherits from both LeftBranch and RightBranch which both inherit from BaseInterface
"""

diamond_value: str | None = None


class Model(DiamondType):
pass
11 changes: 11 additions & 0 deletions tests/data/expected/main/jsonschema/unknown_format.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# generated by datamodel-codegen:
# filename: unknown_format.json
# timestamp: 2019-07-26T00:00:00+00:00

from __future__ import annotations

from pydantic import BaseModel


class Model(BaseModel):
data: str | None = None
61 changes: 61 additions & 0 deletions tests/data/graphql/interface_mro.graphql
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
interface Entity {
id: ID!
}

interface Node {
id: ID!
}

interface Notification implements Node {
id: ID!
readAt: String
}

type CustomerNeedNotification implements Entity & Node & Notification {
id: ID!
readAt: String
customer: String
}

interface BaseInterface {
baseField: String
}

interface LeftBranch implements BaseInterface {
baseField: String
leftField: String
}

interface RightBranch implements BaseInterface {
baseField: String
rightField: String
}

type DiamondType implements BaseInterface & LeftBranch & RightBranch {
baseField: String
leftField: String
rightField: String
diamondField: String
}

interface GrandParent {
grandField: String
}

interface Parent implements GrandParent {
grandField: String
parentField: String
}

interface Child implements GrandParent & Parent {
grandField: String
parentField: String
childField: String
}

type LeafType implements GrandParent & Parent & Child {
grandField: String
parentField: String
childField: String
leafField: String
}
18 changes: 18 additions & 0 deletions tests/data/graphql/union_class_name_prefix.graphql
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
interface IResource {
id: ID!
}

type Employee implements IResource {
id: ID!
firstName: String
lastName: String
}

type Car implements IResource {
id: ID!
passengerCapacity: Int!
}

union Resource = Employee | Car

union TechnicalResource = Car
Loading
Loading