Skip to content

Commit 70bf516

Browse files
authored
Add named profile support and --ignore-pyproject option for pyproject.toml configuration (#2619)
* Add support for --ignore-pyproject and --profile options in CLI * Add skipif decorators for black version compatibility in tests
1 parent c7f65e8 commit 70bf516

11 files changed

Lines changed: 538 additions & 13 deletions

File tree

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -566,7 +566,10 @@ General options:
566566
--generate-pyproject-config
567567
Generate pyproject.toml configuration from the provided CLI
568568
arguments and exit
569+
--ignore-pyproject Ignore pyproject.toml configuration
569570
--no-color disable colorized output
571+
--profile PROFILE Use a named profile from pyproject.toml [tool.datamodel-
572+
codegen.profiles.<name>]
570573
--version show version
571574
-h, --help show this help message and exit
572575
```

docs/index.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -558,7 +558,10 @@ General options:
558558
--generate-pyproject-config
559559
Generate pyproject.toml configuration from the provided CLI
560560
arguments and exit
561+
--ignore-pyproject Ignore pyproject.toml configuration
561562
--no-color disable colorized output
563+
--profile PROFILE Use a named profile from pyproject.toml [tool.datamodel-
564+
codegen.profiles.<name>]
562565
--version show version
563566
-h, --help show this help message and exit
564567
```

docs/pyproject_toml.md

Lines changed: 109 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,115 @@
1-
datamodel-code-generator has a lot of command-line options.
1+
# pyproject.toml Configuration
22

3-
The options are supported on `pyproject.toml`.
3+
datamodel-code-generator can be configured using `pyproject.toml`. The tool automatically searches for `pyproject.toml` in the current directory and parent directories (stopping at the git repository root).
44

5-
Example `pyproject.toml`:
6-
```toml
5+
## Basic Usage
6+
7+
```toml
78
[tool.datamodel-codegen]
9+
input = "schema.yaml"
10+
output = "models.py"
11+
target-python-version = "3.11"
12+
snake-case-field = true
813
field-constraints = true
14+
```
15+
16+
All CLI options can be used in `pyproject.toml` by converting them to kebab-case (e.g., `--snake-case-field` becomes `snake-case-field`).
17+
18+
## Named Profiles
19+
20+
You can define multiple named profiles for different use cases within a single project:
21+
22+
```toml
23+
[tool.datamodel-codegen]
24+
target-python-version = "3.9"
25+
snake-case-field = true
26+
27+
[tool.datamodel-codegen.profiles.api]
28+
input = "schemas/api.yaml"
29+
output = "src/models/api.py"
30+
target-python-version = "3.11"
31+
32+
[tool.datamodel-codegen.profiles.database]
33+
input = "schemas/db.json"
34+
output = "src/models/db.py"
35+
input-file-type = "jsonschema"
36+
```
37+
38+
Base settings in `[tool.datamodel-codegen]` are used when no profile is specified, and also serve as defaults for profiles.
39+
40+
Use a profile with the `--profile` option:
41+
42+
```bash
43+
datamodel-codegen --profile api
44+
datamodel-codegen --profile database
45+
```
46+
47+
## Configuration Priority
48+
49+
Settings are applied in the following priority order (highest to lowest):
50+
51+
1. **CLI arguments** - Always take precedence
52+
2. **Profile settings** - From `[tool.datamodel-codegen.profiles.<name>]`
53+
3. **Base settings** - From `[tool.datamodel-codegen]`
54+
4. **Default values** - Built-in defaults
55+
56+
## Merge Rules
57+
58+
When using profiles, settings are merged using **shallow merge**:
59+
60+
- Profile values **completely replace** base values (no deep merging)
61+
- Settings not specified in the profile are inherited from the base configuration
62+
- Lists and dictionaries are replaced entirely, not merged
63+
64+
### Example
65+
66+
```toml
67+
[tool.datamodel-codegen]
68+
strict-types = ["str", "int"]
69+
http-headers = ["Authorization: Bearer token"]
70+
71+
[tool.datamodel-codegen.profiles.api]
72+
strict-types = ["bytes"]
73+
```
74+
75+
When using `--profile api`:
76+
77+
- `strict-types` becomes `["bytes"]` (completely replaces base, not merged)
78+
- `http-headers` is inherited from base as `["Authorization: Bearer token"]`
79+
80+
## Ignoring pyproject.toml
81+
82+
To ignore all `pyproject.toml` configuration and use only CLI arguments:
83+
84+
```bash
85+
datamodel-codegen --ignore-pyproject --input schema.yaml --output models.py
86+
```
87+
88+
## Generating Configuration
89+
90+
Generate a `pyproject.toml` configuration section from CLI arguments:
91+
92+
```bash
93+
datamodel-codegen --input schema.yaml --output models.py --snake-case-field --generate-pyproject-config
94+
```
95+
96+
Output:
97+
98+
```toml
99+
[tool.datamodel-codegen]
100+
input = "schema.yaml"
101+
output = "models.py"
9102
snake-case-field = true
10-
strip-default-none = false
11-
target-python-version = "3.7"
103+
```
104+
105+
Generate CLI command from existing `pyproject.toml`:
106+
107+
```bash
108+
datamodel-codegen --generate-cli-command
109+
```
110+
111+
With a specific profile:
112+
113+
```bash
114+
datamodel-codegen --profile api --generate-cli-command
12115
```

src/datamodel_code_generator/__main__.py

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,8 @@
6767
"check",
6868
"generate_pyproject_config",
6969
"generate_cli_command",
70+
"ignore_pyproject",
71+
"profile",
7072
"version",
7173
"help",
7274
"debug",
@@ -469,16 +471,27 @@ def merge_args(self, args: Namespace) -> None:
469471
setattr(self, field_name, getattr(parsed_args, field_name))
470472

471473

472-
def _get_pyproject_toml_config(source: Path) -> dict[str, Any]:
473-
"""Find and return the [tool.datamodel-codgen] section of the closest pyproject.toml if it exists."""
474+
def _get_pyproject_toml_config(source: Path, profile: str | None = None) -> dict[str, Any]:
475+
"""Find and return the [tool.datamodel-codegen] section of the closest pyproject.toml if it exists."""
474476
current_path = source
475477
while current_path != current_path.parent:
476478
if (current_path / "pyproject.toml").is_file():
477479
pyproject_toml = load_toml(current_path / "pyproject.toml")
478480
if "datamodel-codegen" in pyproject_toml.get("tool", {}):
479-
pyproject_config = pyproject_toml["tool"]["datamodel-codegen"]
480-
# Convert options from kebap- to snake-case
481-
pyproject_config = {k.replace("-", "_"): v for k, v in pyproject_config.items()}
481+
tool_config = pyproject_toml["tool"]["datamodel-codegen"]
482+
483+
base_config: dict[str, Any] = {k: v for k, v in tool_config.items() if k != "profiles"}
484+
485+
if profile:
486+
profiles = tool_config.get("profiles", {})
487+
if profile not in profiles:
488+
available = list(profiles.keys()) if profiles else "none"
489+
msg = f"Profile '{profile}' not found in pyproject.toml. Available profiles: {available}"
490+
raise Error(msg)
491+
profile_config = profiles[profile]
492+
base_config.update(profile_config)
493+
494+
pyproject_config = {k.replace("-", "_"): v for k, v in base_config.items()}
482495
# Replace US-american spelling if present (ignore if british spelling is present)
483496
if (
484497
"capitalize_enum_members" in pyproject_config and "capitalise_enum_members" not in pyproject_config
@@ -488,9 +501,15 @@ def _get_pyproject_toml_config(source: Path) -> dict[str, Any]:
488501

489502
if (current_path / ".git").exists():
490503
# Stop early if we see a git repository root.
491-
return {}
504+
break
492505

493506
current_path = current_path.parent
507+
508+
# If profile was requested but no pyproject.toml config was found, raise an error
509+
if profile:
510+
msg = f"Profile '{profile}' requested but no [tool.datamodel-codegen] section found in pyproject.toml"
511+
raise Error(msg)
512+
494513
return {}
495514

496515

@@ -648,7 +667,15 @@ def main(args: Sequence[str] | None = None) -> Exit: # noqa: PLR0911, PLR0912,
648667
print(config_output) # noqa: T201
649668
return Exit.OK
650669

651-
pyproject_config = _get_pyproject_toml_config(Path.cwd())
670+
# Handle --ignore-pyproject and --profile options
671+
if namespace.ignore_pyproject:
672+
pyproject_config: dict[str, Any] = {}
673+
else:
674+
try:
675+
pyproject_config = _get_pyproject_toml_config(Path.cwd(), profile=namespace.profile)
676+
except Error as e:
677+
print(e.message, file=sys.stderr) # noqa: T201
678+
return Exit.ERROR
652679

653680
if namespace.generate_cli_command:
654681
if not pyproject_config:

src/datamodel_code_generator/arguments.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -717,6 +717,17 @@ def start_section(self, heading: str | None) -> None:
717717
default=None,
718718
help="Generate CLI command from pyproject.toml configuration and exit",
719719
)
720+
general_options.add_argument(
721+
"--ignore-pyproject",
722+
action="store_true",
723+
default=False,
724+
help="Ignore pyproject.toml configuration",
725+
)
726+
general_options.add_argument(
727+
"--profile",
728+
help="Use a named profile from pyproject.toml [tool.datamodel-codegen.profiles.<name>]",
729+
default=None,
730+
)
720731
general_options.add_argument(
721732
"--version",
722733
action="store_true",
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# generated by datamodel-codegen:
2+
# filename: schema.json
3+
4+
from __future__ import annotations
5+
6+
from pydantic import BaseModel
7+
8+
9+
class Model(BaseModel):
10+
firstName: str | None = 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: schema.json
3+
4+
from __future__ import annotations
5+
6+
from typing import Optional
7+
8+
from pydantic import BaseModel
9+
10+
11+
class Model(BaseModel):
12+
firstName: Optional[str] = None
13+
lastName: Optional[str] = None
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# generated by datamodel-codegen:
2+
# filename: schema.json
3+
4+
from __future__ import annotations
5+
6+
from typing import Optional
7+
8+
from pydantic import BaseModel, Field
9+
10+
11+
class Model(BaseModel):
12+
first_name: Optional[str] = Field(None, alias='firstName')
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# generated by datamodel-codegen:
2+
# filename: schema.json
3+
4+
from __future__ import annotations
5+
6+
from typing import Optional
7+
8+
from pydantic import BaseModel, StrictBytes
9+
10+
11+
class Model(BaseModel):
12+
data: Optional[StrictBytes] = 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: schema.json
3+
4+
from __future__ import annotations
5+
6+
from typing import Optional
7+
8+
from pydantic import BaseModel, Field
9+
10+
11+
class Model(BaseModel):
12+
first_name: Optional[str] = Field(None, alias='firstName')
13+
last_name: Optional[str] = Field(None, alias='lastName')

0 commit comments

Comments
 (0)