Skip to content

Commit e771ada

Browse files
authored
feat: add new option --parent-scoped-naming to avoid name collisions (#2373)
1 parent b0d9109 commit e771ada

File tree

13 files changed

+174
-0
lines changed

13 files changed

+174
-0
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -462,6 +462,8 @@ Model customization:
462462
Choose Datetime class between AwareDatetime, NaiveDatetime or
463463
datetime. Each output model has its default mapping (for example
464464
pydantic: datetime, dataclass: str, ...)
465+
--parent-scoped-naming
466+
Set name of models defined inline from the parent model
465467
--reuse-model Reuse models on the field when a module has the model with the same
466468
content
467469
--target-python-version {3.9,3.10,3.11,3.12,3.13}

docs/index.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -454,6 +454,8 @@ Model customization:
454454
Choose Datetime class between AwareDatetime, NaiveDatetime or
455455
datetime. Each output model has its default mapping (for example
456456
pydantic: datetime, dataclass: str, ...)
457+
--parent-scoped-naming
458+
Set name of models defined inline from the parent model
457459
--reuse-model Reuse models on the field when a module has the model with the same
458460
content
459461
--target-python-version {3.9,3.10,3.11,3.12,3.13}

src/datamodel_code_generator/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,7 @@ def generate( # noqa: PLR0912, PLR0913, PLR0914, PLR0915
287287
keyword_only: bool = False,
288288
no_alias: bool = False,
289289
formatters: list[Formatter] = DEFAULT_FORMATTERS,
290+
parent_scoped_naming: bool = False,
290291
) -> None:
291292
remote_text_cache: DefaultPutDict[str, str] = DefaultPutDict()
292293
if isinstance(input_, str):
@@ -483,6 +484,7 @@ def get_header_and_first_line(csv_file: IO[str]) -> dict[str, Any]:
483484
no_alias=no_alias,
484485
formatters=formatters,
485486
encoding=encoding,
487+
parent_scoped_naming=parent_scoped_naming,
486488
**kwargs,
487489
)
488490

src/datamodel_code_generator/__main__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -314,6 +314,7 @@ def validate_root(cls, values: Any) -> Any: # noqa: N805
314314
keyword_only: bool = False
315315
no_alias: bool = False
316316
formatters: list[Formatter] = DEFAULT_FORMATTERS
317+
parent_scoped_naming: bool = False
317318

318319
def merge_args(self, args: Namespace) -> None:
319320
set_args = {f: getattr(args, f) for f in self.get_fields() if getattr(args, f) is not None}
@@ -525,6 +526,7 @@ def main(args: Sequence[str] | None = None) -> Exit: # noqa: PLR0911, PLR0912,
525526
keyword_only=config.keyword_only,
526527
no_alias=config.no_alias,
527528
formatters=config.formatters,
529+
parent_scoped_naming=config.parent_scoped_naming,
528530
)
529531
except InvalidClassNameError as e:
530532
print(f"{e} You have to set `--class-name` option", file=sys.stderr) # noqa: T201

src/datamodel_code_generator/arguments.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,12 @@ def start_section(self, heading: str | None) -> None:
202202
choices=[i.value for i in DatetimeClassType],
203203
default=None,
204204
)
205+
model_options.add_argument(
206+
"--parent-scoped-naming",
207+
help="Set name of models defined inline from the parent model",
208+
action="store_true",
209+
default=None,
210+
)
205211

206212
# ======================================================================================
207213
# Typing options for generated models

src/datamodel_code_generator/parser/base.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -377,6 +377,7 @@ def __init__( # noqa: PLR0913, PLR0915
377377
keyword_only: bool = False,
378378
no_alias: bool = False,
379379
formatters: list[Formatter] = DEFAULT_FORMATTERS,
380+
parent_scoped_naming: bool = False,
380381
) -> None:
381382
self.keyword_only = keyword_only
382383
self.data_type_manager: DataTypeManager = data_type_manager_type(
@@ -463,6 +464,7 @@ def __init__( # noqa: PLR0913, PLR0915
463464
remove_special_field_name_prefix=remove_special_field_name_prefix,
464465
capitalise_enum_members=capitalise_enum_members,
465466
no_alias=no_alias,
467+
parent_scoped_naming=parent_scoped_naming,
466468
)
467469
self.class_name: str | None = class_name
468470
self.wrap_string_literal: bool | None = wrap_string_literal

src/datamodel_code_generator/parser/graphql.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,7 @@ def __init__( # noqa: PLR0913
154154
keyword_only: bool = False,
155155
no_alias: bool = False,
156156
formatters: list[Formatter] = DEFAULT_FORMATTERS,
157+
parent_scoped_naming: bool = False,
157158
) -> None:
158159
super().__init__(
159160
source=source,
@@ -228,6 +229,7 @@ def __init__( # noqa: PLR0913
228229
keyword_only=keyword_only,
229230
no_alias=no_alias,
230231
formatters=formatters,
232+
parent_scoped_naming=parent_scoped_naming,
231233
)
232234

233235
self.data_model_scalar_type = data_model_scalar_type

src/datamodel_code_generator/parser/jsonschema.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -423,6 +423,7 @@ def __init__( # noqa: PLR0913
423423
keyword_only: bool = False,
424424
no_alias: bool = False,
425425
formatters: list[Formatter] = DEFAULT_FORMATTERS,
426+
parent_scoped_naming: bool = False,
426427
) -> None:
427428
super().__init__(
428429
source=source,
@@ -497,6 +498,7 @@ def __init__( # noqa: PLR0913
497498
keyword_only=keyword_only,
498499
no_alias=no_alias,
499500
formatters=formatters,
501+
parent_scoped_naming=parent_scoped_naming,
500502
)
501503

502504
self.remote_object_cache: DefaultPutDict[str, dict[str, Any]] = DefaultPutDict()

src/datamodel_code_generator/parser/openapi.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,7 @@ def __init__( # noqa: PLR0913
216216
keyword_only: bool = False,
217217
no_alias: bool = False,
218218
formatters: list[Formatter] = DEFAULT_FORMATTERS,
219+
parent_scoped_naming: bool = False,
219220
) -> None:
220221
super().__init__(
221222
source=source,
@@ -290,6 +291,7 @@ def __init__( # noqa: PLR0913
290291
keyword_only=keyword_only,
291292
no_alias=no_alias,
292293
formatters=formatters,
294+
parent_scoped_naming=parent_scoped_naming,
293295
)
294296
self.open_api_scopes: list[OpenAPIScope] = openapi_scopes or [OpenAPIScope.Schemas]
295297

src/datamodel_code_generator/reference.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -318,6 +318,7 @@ def __init__( # noqa: PLR0913, PLR0917
318318
capitalise_enum_members: bool = False, # noqa: FBT001, FBT002
319319
no_alias: bool = False, # noqa: FBT001, FBT002
320320
remove_suffix_number: bool = False, # noqa: FBT001, FBT002
321+
parent_scoped_naming: bool = False, # noqa: FBT001, FBT002
321322
) -> None:
322323
self.references: dict[str, Reference] = {}
323324
self._current_root: Sequence[str] = []
@@ -351,6 +352,7 @@ def __init__( # noqa: PLR0913, PLR0917
351352
self._base_path: Path = base_path or Path.cwd()
352353
self._current_base_path: Path | None = self._base_path
353354
self.remove_suffix_number: bool = remove_suffix_number
355+
self.parent_scoped_naming = parent_scoped_naming
354356

355357
@property
356358
def current_base_path(self) -> Path | None:
@@ -512,6 +514,19 @@ def add_ref(self, ref: str, resolved: bool = False) -> Reference: # noqa: FBT00
512514
self.references[path] = reference
513515
return reference
514516

517+
def _check_parent_scope_option(self, name: str, path: Sequence[str]) -> str:
518+
if self.parent_scoped_naming:
519+
parent_reference = None
520+
parent_path = path[:-1]
521+
while parent_path:
522+
parent_reference = self.references.get(self.join_path(parent_path))
523+
if parent_reference is not None:
524+
break
525+
parent_path = parent_path[:-1]
526+
if parent_reference:
527+
name = f"{parent_reference.name}_{name}"
528+
return name
529+
515530
def add( # noqa: PLR0913
516531
self,
517532
path: Sequence[str],
@@ -533,6 +548,7 @@ def add( # noqa: PLR0913
533548
name = original_name
534549
duplicate_name: str | None = None
535550
if class_name:
551+
name = self._check_parent_scope_option(name, path)
536552
name, duplicate_name = self.get_class_name(
537553
name=name,
538554
unique=unique,
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# generated by datamodel-codegen:
2+
# filename: duplicate_models2.yaml
3+
# timestamp: 2019-07-26T00:00:00+00:00
4+
5+
from __future__ import annotations
6+
7+
from enum import Enum
8+
from typing import Optional
9+
10+
from pydantic import BaseModel
11+
12+
13+
class PetType(Enum):
14+
pet = 'pet'
15+
16+
17+
class PetDetails(BaseModel):
18+
race: Optional[str] = None
19+
20+
21+
class Pet(BaseModel):
22+
id: int
23+
name: str
24+
tag: Optional[str] = None
25+
type: PetType
26+
details: Optional[PetDetails] = None
27+
28+
29+
class CarType(Enum):
30+
car = 'car'
31+
32+
33+
class CarDetails(BaseModel):
34+
brand: Optional[str] = None
35+
36+
37+
class Car(BaseModel):
38+
id: int
39+
name: str
40+
tag: Optional[str] = None
41+
type: CarType
42+
details: Optional[CarDetails] = None
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
openapi: "3.0.0"
2+
info:
3+
version: 1.0.0
4+
title: Swagger Petstore
5+
license:
6+
name: MIT
7+
servers:
8+
- url: http://petstore.swagger.io/v1
9+
paths:
10+
/pets:
11+
get:
12+
summary: Get pet
13+
operationId: getPets
14+
responses:
15+
'200':
16+
content:
17+
application/json:
18+
schema:
19+
$ref: "#/components/schemas/Pet"
20+
/cars:
21+
get:
22+
summary: Get car
23+
operationId: getCar
24+
responses:
25+
'200':
26+
content:
27+
application/json:
28+
schema:
29+
$ref: "#/components/schemas/Cars"
30+
31+
components:
32+
schemas:
33+
Pet:
34+
required:
35+
- id
36+
- name
37+
- type
38+
properties:
39+
id:
40+
type: integer
41+
format: int64
42+
name:
43+
type: string
44+
tag:
45+
type: string
46+
type:
47+
type: string
48+
enum: [ 'pet' ]
49+
details:
50+
type: object
51+
properties:
52+
race: { type: string }
53+
Car:
54+
required:
55+
- id
56+
- name
57+
- type
58+
properties:
59+
id:
60+
type: integer
61+
format: int64
62+
name:
63+
type: string
64+
tag:
65+
type: string
66+
type:
67+
type: string
68+
enum: [ 'car' ]
69+
details:
70+
type: object
71+
properties:
72+
brand: { type: string }

tests/main/openapi/test_main_openapi.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2570,6 +2570,28 @@ def test_main_openapi_referenced_default() -> None:
25702570
assert output_file.read_text() == (EXPECTED_OPENAPI_PATH / "referenced_default.py").read_text()
25712571

25722572

2573+
@freeze_time("2019-07-26")
2574+
def test_duplicate_models() -> None:
2575+
with TemporaryDirectory() as output_dir:
2576+
output_file: Path = Path(output_dir) / "output.py"
2577+
return_code: Exit = main([
2578+
"--input",
2579+
str(OPEN_API_DATA_PATH / "duplicate_models2.yaml"),
2580+
"--output",
2581+
str(output_file),
2582+
"--use-operation-id-as-name",
2583+
"--openapi-scopes",
2584+
"paths",
2585+
"schemas",
2586+
"parameters",
2587+
"--output-model-type",
2588+
"pydantic_v2.BaseModel",
2589+
"--parent-scoped-naming",
2590+
])
2591+
assert return_code == Exit.OK
2592+
assert output_file.read_text() == (EXPECTED_OPENAPI_PATH / "duplicate_models2.py").read_text()
2593+
2594+
25732595
@freeze_time("2019-07-26")
25742596
def test_main_openapi_shadowed_imports() -> None:
25752597
with TemporaryDirectory() as output_dir:

0 commit comments

Comments
 (0)