Skip to content
Draft
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
ae7ba7a
Add fix and tests
kevin-paulson-mindbridge-ai Apr 10, 2026
dff13a4
Remove unused noqa
kevin-paulson-mindbridge-ai Apr 10, 2026
b4e675c
Add one more noqa
kevin-paulson-mindbridge-ai Apr 10, 2026
7eaf6ab
Add assertions to test
kevin-paulson-mindbridge-ai Apr 10, 2026
7786cf9
add some generation test for schemas with cyclic references in order …
kevin-paulson-mindbridge-ai Apr 11, 2026
8d3ee39
Add missing expected_file
kevin-paulson-mindbridge-ai Apr 11, 2026
bb037b0
Update the `test_main_cyclic_` tests so that before the change they a…
kevin-paulson-mindbridge-ai Apr 13, 2026
db5f26a
tmp add
kevin-paulson-mindbridge-ai Apr 13, 2026
6b51fbe
Merge remote-tracking branch 'origin/main' into slow_schema_generatio…
kevin-paulson-mindbridge-ai Apr 16, 2026
207dda3
Revert all changes
kevin-paulson-mindbridge-ai Apr 16, 2026
6cc7834
Add test that takes much more than 30 seconds (probably will never fi…
kevin-paulson-mindbridge-ai Apr 16, 2026
f9da08d
Add test timeout
kevin-paulson-mindbridge-ai Apr 16, 2026
670f35d
Code format mostly
kevin-paulson-mindbridge-ai Apr 16, 2026
7deb47d
whitespace changes to help view the code better
kevin-paulson-mindbridge-ai Apr 16, 2026
c380a72
Produce smaler openapi example (still fails)
kevin-paulson-mindbridge-ai Apr 16, 2026
582a495
Resolve test failure
kevin-paulson-mindbridge-ai Apr 16, 2026
45f0208
Merge branch 'main' into slow_schema_generation_fix
kevin-paulson-mindbridge-ai Apr 17, 2026
b3a3e3e
handle None model case in discriminator mapping validation
kevin-paulson-mindbridge-ai Apr 17, 2026
064c620
Rename and describe test
kevin-paulson-mindbridge-ai Apr 17, 2026
d10ed4a
No disable timestamp like the other tests
kevin-paulson-mindbridge-ai Apr 17, 2026
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
12 changes: 10 additions & 2 deletions src/datamodel_code_generator/parser/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -493,7 +493,7 @@ def add_model_path_to_list(
return paths


def sort_data_models( # noqa: PLR0912, PLR0915
def sort_data_models( # noqa: PLR0912, PLR0914, PLR0915
unsorted_data_models: list[DataModel],
sorted_data_models: SortedDataModels | None = None,
require_update_action_models: list[str] | None = None,
Expand Down Expand Up @@ -534,6 +534,7 @@ def sort_data_models( # noqa: PLR0912, PLR0915
pass

# sort on base_class dependency
seen_orders: set[tuple[str, ...]] = set()
while True:
ordered_models: list[tuple[int, DataModel]] = []
# Build lookup dict for O(1) index access instead of O(n) list.index()
Expand Down Expand Up @@ -565,6 +566,11 @@ def sort_data_models( # noqa: PLR0912, PLR0915
sorted_unresolved_models = [m[1] for m in sorted(ordered_models, key=operator.itemgetter(0))]
if sorted_unresolved_models == unresolved_references:
break
new_order = tuple(m.path for m in sorted_unresolved_models)
if new_order in seen_orders:
unresolved_references = sorted_unresolved_models
break
seen_orders.add(new_order)
unresolved_references = sorted_unresolved_models

# circular reference
Expand Down Expand Up @@ -1624,7 +1630,9 @@ def get_discriminator_field_value(

if len(discriminator_values) == 0:
for base_class in discriminator_model.base_classes:
check_paths(base_class.reference, mapping) # ty: ignore
if not base_class.reference:
continue
check_paths(base_class.reference, mapping)

if not discriminator_values:
discriminator_values = [discriminator_model.path.split("/")[-1]]
Expand Down
36 changes: 36 additions & 0 deletions tests/data/expected/main/graphql/cyclic_mutual_types.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# generated by datamodel-codegen:
# filename: cyclic-mutual-types.graphql
# timestamp: 2019-07-26T00:00:00+00:00

from __future__ import annotations

from typing import Literal

from pydantic import BaseModel, Field
from typing_extensions import TypeAliasType

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


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 Model(BaseModel):
name: String | None = None
typename__: Literal['Model'] | None = Field('Model', alias='__typename')


class Team(Person, Model):
name: String | None = None
typename__: Literal['Team'] | None = Field('Team', alias='__typename')


class Person(Team, Model):
name: String | None = None
typename__: Literal['Person'] | None = Field('Person', alias='__typename')
19 changes: 19 additions & 0 deletions tests/data/expected/main/jsonschema/cyclic_mutual_defs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# generated by datamodel-codegen:
# filename: cyclic_mutual_defs.json
# timestamp: 2019-07-26T00:00:00+00:00

from __future__ import annotations

from pydantic import BaseModel


class Model(BaseModel):
pass


class Person(Team, Model):
name: str | None = None


class Team(Person, Model):
name: str | None = None
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify current pattern fails at class creation time (read-only check).
python - <<'PY'
code = """
from __future__ import annotations
from pydantic import BaseModel

class Model(BaseModel):
    pass

class Person(Team, Model):
    name: str | None = None

class Team(Person, Model):
    name: str | None = None
"""
try:
    exec(code, {})
    print("UNEXPECTED: class graph executed successfully")
except Exception as e:
    print(f"Expected failure: {type(e).__name__}: {e}")
PY

Repository: koxudaxi/datamodel-code-generator

Length of output: 132


🏁 Script executed:

# Check the full content of the fixture file
cat -n tests/data/expected/main/jsonschema/cyclic_mutual_defs.py

Repository: koxudaxi/datamodel-code-generator

Length of output: 540


🏁 Script executed:

# Look for related test files that might explain the purpose
fd -i cyclic tests/

Repository: koxudaxi/datamodel-code-generator

Length of output: 495


🏁 Script executed:

# Check for input schema files
fd cyclic_mutual tests/ -type f

Repository: koxudaxi/datamodel-code-generator

Length of output: 247


🏁 Script executed:

# Read the input schema file
cat -n tests/data/jsonschema/cyclic_mutual_defs.json

Repository: koxudaxi/datamodel-code-generator

Length of output: 994


🏁 Script executed:

# Search for test code that validates this fixture
rg -l cyclic_mutual_defs tests/ -t py

Repository: koxudaxi/datamodel-code-generator

Length of output: 180


🏁 Script executed:

# Find and read the test file that uses this fixture
rg -B 5 -A 10 cyclic_mutual_defs tests/main/jsonschema/test_main_jsonschema.py

Repository: koxudaxi/datamodel-code-generator

Length of output: 714


🏁 Script executed:

# Check the other cyclic test case (OpenAPI)
cat -n tests/data/expected/main/openapi/cyclic_component_refs.py

Repository: koxudaxi/datamodel-code-generator

Length of output: 543


🏁 Script executed:

# Check the GraphQL cyclic test case
cat -n tests/data/expected/main/graphql/cyclic_mutual_types.py

Repository: koxudaxi/datamodel-code-generator

Length of output: 1337


🏁 Script executed:

# Search for any discussion or comments about cyclic references in the codebase
rg -i "cyclic|forward.*ref|mutual.*inherit" tests/ -A 2 -B 2 --max-count=20

Repository: koxudaxi/datamodel-code-generator

Length of output: 18508


🏁 Script executed:

# Search for any TODOs or comments about cyclic inheritance in the codebase
rg -i "cyclic|todo|fixme.*inherit|forward.*ref.*inherit" --max-count=20 datamodel_code_generator/

Repository: koxudaxi/datamodel-code-generator

Length of output: 142


🏁 Script executed:

# Check if there are any comments in the test file about cyclic_mutual_defs
rg -B 10 -A 5 "test_main_cyclic_mutual_defs" tests/main/jsonschema/test_main_jsonschema.py

Repository: koxudaxi/datamodel-code-generator

Length of output: 701


🏁 Script executed:

# Find the source directory
fd -t d "^datamodel" . | head -5

Repository: koxudaxi/datamodel-code-generator

Length of output: 108


🏁 Script executed:

# Search for any issue numbers or comments about cyclic handling
rg -i "cyclic|mutual.*def" . --max-count=30 -t py | grep -E "(TODO|FIXME|issue|#[0-9]+|NOTE)" | head -20

Repository: koxudaxi/datamodel-code-generator

Length of output: 59


🏁 Script executed:

# Search for code related to inheritance and base class generation
rg -i "base.*class|inherit" src/datamodel_code_generator/ -t py --max-count=20 | head -30

Repository: koxudaxi/datamodel-code-generator

Length of output: 3872


🏁 Script executed:

# Check if there's special handling for ordering classes based on dependencies
rg -l "sort|order|depend" src/datamodel_code_generator/ -t py | head -10

Repository: koxudaxi/datamodel-code-generator

Length of output: 526


🏁 Script executed:

# Look for any parser logic that handles allOf or schema references
rg "allOf|definitions" src/datamodel_code_generator/ -t py --max-count=15

Repository: koxudaxi/datamodel-code-generator

Length of output: 8419


🏁 Script executed:

# Examine the SCC (Strongly Connected Components) module for cycle handling
cat src/datamodel_code_generator/parser/_scc.py

Repository: koxudaxi/datamodel-code-generator

Length of output: 5865


🏁 Script executed:

# Search for where allOf is processed and how inheritance is generated
rg "allOf" src/datamodel_code_generator/parser/jsonschema.py -B 3 -A 10 | head -80

Repository: koxudaxi/datamodel-code-generator

Length of output: 3802


🏁 Script executed:

# Look for how base classes are generated from allOf
rg -i "base.*class|inherit" src/datamodel_code_generator/parser/jsonschema.py -B 2 -A 5 | head -100

Repository: koxudaxi/datamodel-code-generator

Length of output: 4456


🏁 Script executed:

# Check the inheritance_forward_ref fixture to see how it handles forward references
cat -n tests/data/expected/main/jsonschema/inheritance_forward_ref.py

Repository: koxudaxi/datamodel-code-generator

Length of output: 1616


🏁 Script executed:

# Also check the input schema for inheritance_forward_ref
cat tests/data/jsonschema/inheritance_forward_ref.json

Repository: koxudaxi/datamodel-code-generator

Length of output: 2386


🏁 Script executed:

# Check if cyclic_mutual_defs.py has model_rebuild calls
tail -20 tests/data/expected/main/jsonschema/cyclic_mutual_defs.py

Repository: koxudaxi/datamodel-code-generator

Length of output: 407


🏁 Script executed:

# Look for any comments or documentation about this limitation in the parser code
rg -i "mutual|cyclic.*class|inherit.*cycle" src/datamodel_code_generator/ -B 2 -A 2

Repository: koxudaxi/datamodel-code-generator

Length of output: 1180


🏁 Script executed:

# Check if there's a --keep-model-order option being used and how it affects output
rg "keep.*model.*order|--keep-model-order" tests/data/expected/main/jsonschema/ -l

Repository: koxudaxi/datamodel-code-generator

Length of output: 149


🏁 Script executed:

# Check if there are any GitHub issues or discussions about this in code comments
rg "1466|1467|1468|cyclic.*inherit" src/ tests/ --max-count=10

Repository: koxudaxi/datamodel-code-generator

Length of output: 197


🏁 Script executed:

# Final confirmation: does the current output actually fail to import?
python3 << 'EOF'
import sys
code_path = "tests/data/expected/main/jsonschema/cyclic_mutual_defs.py"
try:
    with open(code_path) as f:
        code = f.read()
    exec(compile(code, code_path, 'exec'), {})
    print("SUCCESS: Code executed without error")
except Exception as e:
    print(f"FAILURE: {type(e).__name__}: {e}")
    sys.exit(1)
EOF

Repository: koxudaxi/datamodel-code-generator

Length of output: 123


Invalid Python output: forward base reference and mutual inheritance cycle

Person(Team, Model) at Line 14 references Team before it exists, and Person/Team form an impossible direct inheritance cycle. The output fails at runtime with NameError: name 'Team' is not defined. Even with from __future__ import annotations, base class names are resolved eagerly at class definition time, so forward references do not help. The model_rebuild() pattern used in other forward-reference fixtures does not apply here because mutual class inheritance cannot be resolved post-hoc.

Consider generating valid output by removing the mutual inheritance:

Proposed fixture correction
-class Person(Team, Model):
+class Person(Model):
     name: str | None = None
 
 
-class Team(Person, Model):
+class Team(Model):
     name: str | None = None
🧰 Tools
🪛 Ruff (0.15.9)

[error] 14-14: Undefined name Team

(F821)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/data/expected/main/jsonschema/cyclic_mutual_defs.py` around lines 14 -
19, The generated classes Person and Team create an impossible mutual
inheritance cycle (Person(Team, Model) and Team(Person, Model)) causing a
runtime NameError; replace this mutual inheritance with a valid hierarchy by
removing the forward base reference—e.g., have both classes inherit from Model
only or make one inherit from the other (but not both) so that Person and Team
do not form a direct cycle; update the class headers for Person and Team (the
class definitions named Person and Team) accordingly and ensure any
tests/fixtures referencing Model still import/define Model as before.

19 changes: 19 additions & 0 deletions tests/data/expected/main/openapi/cyclic_component_refs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# generated by datamodel-codegen:
# filename: cyclic_component_refs.yaml
# timestamp: 2019-07-26T00:00:00+00:00

from __future__ import annotations

from pydantic import BaseModel


class Model(BaseModel):
pass


class Person(Team, Model):
name: str | None = None


class Team(Person, Model):
name: str | None = None
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
python - <<'PY'
src = """
from __future__ import annotations
class BaseModel: pass
class Model(BaseModel): pass
class Person(Team, Model): pass
class Team(Person, Model): pass
"""
try:
    exec(src, {})
except Exception as exc:
    print(f"{type(exc).__name__}: {exc}")
PY

Repository: koxudaxi/datamodel-code-generator

Length of output: 114


🏁 Script executed:

cat -n tests/data/expected/main/openapi/cyclic_component_refs.py

Repository: koxudaxi/datamodel-code-generator

Length of output: 543


Fix the cyclic inheritance in the expected fixture—this code cannot be imported.

The generated Person and Team classes form an unimportable cycle. Line 14 references Team before it is defined, which raises NameError at class definition time. The from __future__ import annotations pragma only defers annotations, not base-class lookup. Either reject this schema in the generator or adjust the expected output to produce valid Python.

🧰 Tools
🪛 Ruff (0.15.9)

[error] 14-14: Undefined name Team

(F821)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/data/expected/main/openapi/cyclic_component_refs.py` around lines 14 -
19, The expected fixture currently creates an unimportable cyclic base-class
relationship between Person and Team (Person(Team, Model) and Team(Person,
Model)); fix it by removing the cyclic inheritance and making both classes
inherit only from Model (e.g., class Person(Model): and class Team(Model):) and,
if a cross-reference is needed, add attributes that reference the other type as
a forward annotation (e.g., person has team: "Team" | None = None and team has
member: "Person" | None = None) so the file can be imported successfully while
preserving the relationship between Person and Team.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll see if I can create better test data.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kevin-paulson-mindbridge-ai, sounds good! Here are a few concrete directions you could take:


Option A — Cyclic field references instead of cyclic inheritance

If your input schema uses allOf to pull in referenced types as base classes, consider restructuring it so each type holds the other as an optional field (forward-referenced). This produces importable Python:

# generated by datamodel-codegen:
#   filename:  cyclic_component_refs.yaml
#   timestamp: 2019-07-26T00:00:00+00:00

from __future__ import annotations

from typing import Optional

from pydantic import BaseModel


class Person(BaseModel):
    name: str | None = None
    team: Optional[Team] = None   # forward ref resolved by `from __future__ import annotations`


class Team(BaseModel):
    name: str | None = None
    member: Optional[Person] = None

This is valid Python (the from __future__ import annotations does help for field annotations).


Option B — Non-cyclic inheritance with a shared base

If the test is specifically meant to exercise how the generator handles allOf with a common parent, restructure so the cycle is not in the base-class chain:

class Model(BaseModel):
    pass

class Person(Model):
    name: str | None = None
    team: Team | None = None   # field ref, not inheritance

class Team(Model):
    name: str | None = None

Option C — Adjust the input schema

The root cause is the input YAML/JSON/GraphQL schema itself encoding a pattern (mutual allOf inheritance) that has no valid Python mapping. You could adjust the input schema to use composition via properties instead of allOf-based inheritance, which would make the expected output naturally importable.


Which approach fits your schema best? Happy to help refine the expected fixture once you decide on a direction.


🧠 Learnings used
Learnt from: koxudaxi
Repo: koxudaxi/datamodel-code-generator PR: 2890
File: tests/data/expected/main/jsonschema/ref_nullable_with_constraint.py:14-15
Timestamp: 2026-01-02T08:25:22.111Z
Learning: The datamodel-code-generator currently generates RootModel subclasses with an explicit `root` field annotation (e.g., `class StringType(RootModel[str]): root: str`). This is existing behavior of the code generator and should not be flagged as an issue introduced by new changes.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kevin-paulson-mindbridge-ai since you said those examples won't generate any output before1, and now they generate this output, I think this may been introduced by your bugfix.

That's an interesting corner-case. I think we should look into that before merging.

@kevin-paulson-mindbridge-ai IMHO if you can't fix that, consider adding an error to communicate to the user this is an (yet) unsupported corner-case, in order to avoid generating broken code. We may track that in another issue then and fix later. Finding the edges of the problem is the first step to solve it, after all.

CC: @koxudaxi WDYT? 🙂

Footnotes

  1. https://github.com/koxudaxi/datamodel-code-generator/pull/3078#issuecomment-4238086331

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I no longer think that this test data is fully reproducing the original issue and I don't think the fix is quite it either.

What caused me to look at this is we have a somewhat large schema available at test.mindbridge.ai/api/docs/mindbridge-openapi.json which produces with this change an about 20,000 line (valid) python file. Although with this change it will now generate the python file instead of never terminating:

  1. The generated python file produces duplicate union members, this was not noticed as they were hidden by ruff check --fix
  2. I'm having a lot of difficulty determining a test that reproduces the issue and still generates the valid Python expected.

I will plan to continue reviewing this PR later this week to see what I can discover.

19 changes: 19 additions & 0 deletions tests/data/graphql/cyclic-mutual-types.graphql
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
type Query {
entry: Team
}

# GraphQL interfaces must declare fields; OpenAPI `Model` is an empty object, so `name`
# is the shared optional field carried by Team/Person (mirrors OpenAPI `name` on each).
interface Model {
name: String
}

# Mirrors allOf [Model, Person] + properties { name }.
interface Team implements Model & Person {
name: String
}

# Mirrors allOf [Model, Team] + properties { name }.
interface Person implements Model & Team {
name: String
}
33 changes: 33 additions & 0 deletions tests/data/jsonschema/cyclic_mutual_defs.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {},
"definitions": {
"Model": {
"title": "Model",
"type": "object"
},
"Team": {
"title": "Team",
"allOf": [
{ "$ref": "#/definitions/Model" },
{ "$ref": "#/definitions/Person" }
],
"type": "object",
"properties": {
"name": { "type": "string" }
}
},
"Person": {
"title": "Person",
"allOf": [
{ "$ref": "#/definitions/Model" },
{ "$ref": "#/definitions/Team" }
],
"type": "object",
"properties": {
"name": { "type": "string" }
}
}
}
}
25 changes: 25 additions & 0 deletions tests/data/openapi/cyclic_component_refs.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
openapi: 3.0.0
info:
title: Cyclic component $ref
version: "1.0"
paths: {}
components:
schemas:
Model:
type: object
Team:
allOf:
- $ref: "#/components/schemas/Model"
- $ref: "#/components/schemas/Person"
type: object
properties:
name:
type: string
Person:
allOf:
- $ref: "#/components/schemas/Model"
- $ref: "#/components/schemas/Team"
type: object
properties:
name:
type: string
11 changes: 11 additions & 0 deletions tests/main/graphql/test_main_graphql.py
Original file line number Diff line number Diff line change
Expand Up @@ -917,3 +917,14 @@ def test_main_graphql_no_typename(output_file: Path) -> None:
expected_file="no_typename.py",
extra_args=["--graphql-no-typename"],
)


def test_main_cyclic_mutual_types(output_file: Path) -> None:
"""Mutual object-type cycle (A <-> B) in GraphQL."""
run_main_and_assert(
input_path=GRAPHQL_DATA_PATH / "cyclic-mutual-types.graphql",
output_path=output_file,
input_file_type="graphql",
assert_func=assert_file_content,
expected_file="cyclic_mutual_types.py",
)
10 changes: 10 additions & 0 deletions tests/main/jsonschema/test_main_jsonschema.py
Original file line number Diff line number Diff line change
Expand Up @@ -1157,6 +1157,16 @@ def test_main_circular_reference(output_file: Path) -> None:
)


def test_main_cyclic_mutual_defs(output_file: Path) -> None:
"""Minimal mutual cycle between two JSON Schema definitions (A <-> B)."""
run_main_and_assert(
input_path=JSON_SCHEMA_DATA_PATH / "cyclic_mutual_defs.json",
output_path=output_file,
input_file_type="jsonschema",
assert_func=assert_file_content,
)


def test_main_invalid_enum_name(output_file: Path) -> None:
"""Test invalid enum name handling."""
run_main_and_assert(
Expand Down
10 changes: 10 additions & 0 deletions tests/main/openapi/test_main_openapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -4751,6 +4751,16 @@ def test_main_openapi_circular_imports_acyclic(output_dir: Path) -> None:
)


def test_main_cyclic_component_refs(output_file: Path) -> None:
"""Mutual $ref cycle in components/schemas (single-file OpenAPI)."""
run_main_and_assert(
input_path=OPEN_API_DATA_PATH / "cyclic_component_refs.yaml",
output_path=output_file,
input_file_type="openapi",
assert_func=assert_file_content,
)


def test_main_openapi_circular_imports_class_conflict(output_dir: Path) -> None:
"""Test that class name conflicts in merged _internal.py are resolved with sequential renaming."""
with freeze_time(TIMESTAMP):
Expand Down
74 changes: 74 additions & 0 deletions tests/parser/test_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
if TYPE_CHECKING:
from datamodel_code_generator.parser.schema_version import JsonSchemaFeatures

from datamodel_code_generator.imports import Imports
from datamodel_code_generator.model.base import BaseClassDataType
from datamodel_code_generator.model.pydantic_v2 import BaseModel, DataModelField
from datamodel_code_generator.model.type_alias import TypeAlias, TypeAliasTypeBackport, TypeStatement
from datamodel_code_generator.parser.base import (
Expand Down Expand Up @@ -242,6 +244,78 @@ def test_sort_data_models_unresolved_raise_recursion_error() -> None:
sort_data_models(reference, recursion_count=100000)


def test_sort_data_models_circular_base_classes_no_infinite_loop() -> None:
"""Mutual base-class references must not oscillate forever in the dependency sort."""
reference_a = Reference(path="A", original_name="A", name="A")
reference_b = Reference(path="B", original_name="B", name="B")
reference = [
BaseModel(
fields=[],
reference=reference_a,
base_classes=[reference_b],
),
BaseModel(
fields=[],
reference=reference_b,
base_classes=[reference_a],
),
]

_, resolved, require_update_action_models = sort_data_models(reference)

assert set(resolved) == {"A", "B"}
assert sorted(require_update_action_models) == ["A", "B"]


def test_apply_discriminator_type_skips_base_class_without_reference() -> None:
"""Base class slots without a Reference must not be passed to check_paths."""
ref_pet = Reference(path="#/components/schemas/Pet", original_name="Pet", name="Pet")
pet_model = BaseModel(fields=[], reference=ref_pet)
ref_pet.source = pet_model
pet_model.base_classes.append(BaseClassDataType())

ref_other = Reference(path="#/components/schemas/Other", original_name="Other", name="Other")
other_model = BaseModel(fields=[], reference=ref_other)
ref_other.source = other_model

union_inner = DataType(data_types=[DataType(reference=ref_pet), DataType(reference=ref_other)])
ref_root = Reference(path="#/components/schemas/Root", original_name="Root", name="Root")
field = DataModelField(
name="u",
data_type=union_inner,
extras={
"discriminator": {
"propertyName": "petType",
"mapping": {"dog": "#/components/schemas/Other"},
}
},
)
root = BaseModel(fields=[field], reference=ref_root)
ref_root.source = root

parser = C(
data_model_type=BaseModel,
data_model_root_type=BaseModel,
data_model_field_type=DataModelField,
base_class="BaseModel",
source="",
)
union_variant_types = tuple(union_inner.data_types)
assert len(union_variant_types) == 2
assert {dt.reference.path for dt in union_variant_types} == {ref_pet.path, ref_other.path}
assert {id(dt.reference) for dt in union_variant_types} == {id(ref_pet), id(ref_other)}
pet_base_classes = pet_model.base_classes
bare_base_slot = pet_model.base_classes[-1]

parser._Parser__apply_discriminator_type([root], Imports())

assert tuple(union_inner.data_types) == union_variant_types
assert {dt.reference.path for dt in union_inner.data_types} == {ref_pet.path, ref_other.path}
assert {id(dt.reference) for dt in union_inner.data_types} == {id(ref_pet), id(ref_other)}
assert pet_model.base_classes is pet_base_classes
assert pet_model.base_classes[-1] is bare_base_slot


@pytest.mark.parametrize(
("current_module", "reference", "val"),
[
Expand Down
Loading