Skip to content

Commit 3dd150e

Browse files
authored
Correct import generation for dot notation schema names with inheritance (#2607)
* Fix: Enhance ancestor package reference resolution for imports in dot notation schemas * Fix: Simplify ancestor package reference handling in import resolution
1 parent 4c116d5 commit 3dd150e

11 files changed

Lines changed: 195 additions & 1 deletion

File tree

src/datamodel_code_generator/parser/base.py

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,45 @@ def relative(current_module: str, reference: str) -> tuple[str, str]:
293293
return left, right
294294

295295

296+
def is_ancestor_package_reference(current_module: str, reference: str) -> bool:
297+
"""Check if reference is in an ancestor package (__init__.py).
298+
299+
When the reference's module path is an ancestor (prefix) of the current module,
300+
the reference is in an ancestor package's __init__.py file.
301+
302+
Args:
303+
current_module: The current module path (e.g., "v0.mammal.canine")
304+
reference: The full reference path (e.g., "v0.Animal")
305+
306+
Returns:
307+
True if the reference is in an ancestor package, False otherwise.
308+
309+
Examples:
310+
- current="v0.animal", ref="v0.Animal" -> True (immediate parent)
311+
- current="v0.mammal.canine", ref="v0.Animal" -> True (grandparent)
312+
- current="v0.animal", ref="v0.animal.Dog" -> False (same or child)
313+
- current="pets", ref="Animal" -> True (root package is immediate parent)
314+
"""
315+
current_path = current_module.split(".") if current_module else []
316+
*reference_path, _ = reference.split(".")
317+
318+
if not current_path:
319+
return False
320+
321+
# Case 1: Direct parent package (includes root package when reference_path is empty)
322+
# e.g., current="pets", ref="Animal" -> current_path[:-1]=[] == reference_path=[]
323+
if current_path[:-1] == reference_path:
324+
return True
325+
326+
# Case 2: Deeper ancestor package (reference_path must be non-empty proper prefix)
327+
# e.g., current="v0.mammal.canine", ref="v0.Animal" -> ["v0"] is prefix of ["v0","mammal","canine"]
328+
return (
329+
len(reference_path) > 0
330+
and len(reference_path) < len(current_path)
331+
and current_path[: len(reference_path)] == reference_path
332+
)
333+
334+
296335
def exact_import(from_: str, import_: str, short_name: str) -> tuple[str, str]:
297336
"""Create exact import path to avoid relative import issues."""
298337
if from_ == len(from_) * ".":
@@ -813,7 +852,8 @@ def __change_from_import(
813852

814853
if isinstance(data_type, BaseClassDataType):
815854
left, right = relative(model.module_name, data_type.full_name)
816-
from_ = f"{left}{right}" if left.endswith(".") else f"{left}.{right}"
855+
is_ancestor = is_ancestor_package_reference(model.module_name, data_type.full_name)
856+
from_ = left if is_ancestor else (f"{left}{right}" if left.endswith(".") else f"{left}.{right}")
817857
import_ = data_type.reference.short_name
818858
full_path = from_, import_
819859
else:
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# generated by datamodel-codegen:
2+
# filename: dot_notation_deep_inheritance.yaml
3+
# timestamp: 2019-07-26T00:00:00+00:00
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# generated by datamodel-codegen:
2+
# filename: dot_notation_deep_inheritance.yaml
3+
# timestamp: 2019-07-26T00:00:00+00:00
4+
5+
from __future__ import annotations
6+
7+
from typing import Optional
8+
9+
from pydantic import BaseModel
10+
11+
12+
class Animal(BaseModel):
13+
species: Optional[str] = None
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# generated by datamodel-codegen:
2+
# filename: dot_notation_deep_inheritance.yaml
3+
# timestamp: 2019-07-26T00:00:00+00:00
4+
5+
from __future__ import annotations
6+
7+
from typing import Optional
8+
9+
from .. import Animal
10+
11+
12+
class Dog(Animal):
13+
breed: Optional[str] = None
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# generated by datamodel-codegen:
2+
# filename: dot_notation_deep_inheritance.yaml
3+
# timestamp: 2019-07-26T00:00:00+00:00
4+
5+
from __future__ import annotations
6+
7+
from typing import Optional
8+
9+
from .. import Animal
10+
11+
12+
class Puppy(Animal):
13+
age_weeks: Optional[int] = None
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# generated by datamodel-codegen:
2+
# filename: dot_notation_inheritance.yaml
3+
# timestamp: 2019-07-26T00:00:00+00:00
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# generated by datamodel-codegen:
2+
# filename: dot_notation_inheritance.yaml
3+
# timestamp: 2019-07-26T00:00:00+00:00
4+
5+
from __future__ import annotations
6+
7+
from typing import Optional
8+
9+
from pydantic import BaseModel
10+
11+
12+
class Properties(BaseModel):
13+
name: Optional[str] = None
14+
15+
16+
class Animal(BaseModel):
17+
species: Optional[str] = None
18+
props: Optional[Properties] = None
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# generated by datamodel-codegen:
2+
# filename: dot_notation_inheritance.yaml
3+
# timestamp: 2019-07-26T00:00:00+00:00
4+
5+
from __future__ import annotations
6+
7+
from typing import Optional
8+
9+
from . import Animal
10+
11+
12+
class Dog(Animal):
13+
breed: Optional[str] = None
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
openapi: "3.0.0"
2+
info:
3+
title: Test API with deep inheritance from ancestor packages
4+
version: "1.0.0"
5+
paths: {}
6+
components:
7+
schemas:
8+
v0.Animal:
9+
type: object
10+
description: Base animal in root package
11+
properties:
12+
species:
13+
type: string
14+
v0.mammal.Dog:
15+
description: Dog inheriting from Animal (2 levels up)
16+
allOf:
17+
- $ref: '#/components/schemas/v0.Animal'
18+
- type: object
19+
properties:
20+
breed:
21+
type: string
22+
v0.mammal.canine.Puppy:
23+
description: Puppy inheriting from Animal (3 levels up)
24+
allOf:
25+
- $ref: '#/components/schemas/v0.Animal'
26+
- type: object
27+
properties:
28+
age_weeks:
29+
type: integer
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
openapi: "3.0.0"
2+
info:
3+
title: Test API with dot notation schema names and inheritance
4+
version: "1.0.0"
5+
paths: {}
6+
components:
7+
schemas:
8+
v0.properties:
9+
type: object
10+
description: Base properties schema
11+
properties:
12+
name:
13+
type: string
14+
v0.animal:
15+
type: object
16+
description: Animal schema that references v0.properties
17+
properties:
18+
species:
19+
type: string
20+
props:
21+
$ref: '#/components/schemas/v0.properties'
22+
v0.animal.dog:
23+
description: Dog schema that extends v0.animal
24+
allOf:
25+
- $ref: '#/components/schemas/v0.animal'
26+
- type: object
27+
properties:
28+
breed:
29+
type: string

0 commit comments

Comments
 (0)