Skip to content

Commit 430582a

Browse files
authored
Keep generate string input inline (#3573)
* Keep generate string input inline * Address generate string input review
1 parent 6147628 commit 430582a

7 files changed

Lines changed: 193 additions & 76 deletions

File tree

docs/llms-full.txt

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35736,14 +35736,32 @@ result = generate(json_schema, config=config)
3573635736
print(result)
3573735737
```
3573835738

35739+
### 📝 Reading Input From a File
35740+
35741+
Pass a `Path` to `input_` for file input. Plain strings are treated as schema
35742+
text; if a string points to an existing file and parsing fails, `generate()`
35743+
warns and recommends using `Path`.
35744+
35745+
```python
35746+
from pathlib import Path
35747+
35748+
from datamodel_code_generator import InputFileType, generate
35749+
35750+
result = generate(
35751+
input_=Path("example.json"),
35752+
input_file_type=InputFileType.JsonSchema,
35753+
)
35754+
print(result)
35755+
```
35756+
3573935757
### 📝 Multiple Module Output
3574035758

3574135759
When the schema generates multiple modules, `generate()` returns a `GeneratedModules` dictionary mapping module path tuples to generated code:
3574235760

3574335761
```python
3574435762
from datamodel_code_generator import InputFileType, generate, GenerateConfig, GeneratedModules
3574535763

35746-
# Your OpenAPI specification (string, Path, or dict)
35764+
# Your OpenAPI specification (string schema text, Path, or dict)
3574735765
openapi_spec: str = "..." # Replace with your actual OpenAPI spec
3574835766

3574935767
# Schema that generates multiple modules (e.g., with $ref to other files)

docs/using_as_module.md

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,14 +42,32 @@ result = generate(json_schema, config=config)
4242
print(result)
4343
```
4444

45+
### 📝 Reading Input From a File
46+
47+
Pass a `Path` to `input_` for file input. Plain strings are treated as schema
48+
text; if a string points to an existing file and parsing fails, `generate()`
49+
warns and recommends using `Path`.
50+
51+
```python
52+
from pathlib import Path
53+
54+
from datamodel_code_generator import InputFileType, generate
55+
56+
result = generate(
57+
input_=Path("example.json"),
58+
input_file_type=InputFileType.JsonSchema,
59+
)
60+
print(result)
61+
```
62+
4563
### 📝 Multiple Module Output
4664

4765
When the schema generates multiple modules, `generate()` returns a `GeneratedModules` dictionary mapping module path tuples to generated code:
4866

4967
```python
5068
from datamodel_code_generator import InputFileType, generate, GenerateConfig, GeneratedModules
5169

52-
# Your OpenAPI specification (string, Path, or dict)
70+
# Your OpenAPI specification (string schema text, Path, or dict)
5371
openapi_spec: str = "..." # Replace with your actual OpenAPI spec
5472

5573
# Schema that generates multiple modules (e.g., with $ref to other files)

src/datamodel_code_generator/__init__.py

Lines changed: 71 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -106,8 +106,6 @@
106106
Returned by generate() when output=None and multiple modules are generated.
107107
"""
108108

109-
_SCHEMA_TEXT_START_CHARS = ("{", "[", "<")
110-
111109

112110
def _apply_generate_config_preset(config: GenerateConfig) -> GenerateConfig:
113111
"""Return a generate config with preset defaults applied."""
@@ -761,21 +759,41 @@ def _generate_config_values(generate_config: GenerateConfig) -> dict[str, Any]:
761759
return values
762760

763761

764-
def _coerce_existing_path_input(
762+
def _warn_if_input_string_points_to_existing_path(
765763
input_: Path | str | ParseResult | Mapping[str, Any] | list[Any],
766-
) -> Path | str | ParseResult | Mapping[str, Any] | list[Any]:
767-
if not isinstance(input_, str):
768-
return input_
769-
if not input_ or "\n" in input_ or "\r" in input_:
770-
return input_
771-
if (stripped := input_.lstrip()).startswith(_SCHEMA_TEXT_START_CHARS) or "://" in stripped:
772-
return input_
764+
) -> None:
765+
match input_:
766+
case str() as input_text if input_text and "\n" not in input_text and "\r" not in input_text:
767+
pass
768+
case _:
769+
return
773770
try:
774-
path = Path(input_).expanduser()
771+
path = Path(input_text).expanduser()
775772
path_exists = path.exists()
776-
except (OSError, RuntimeError, ValueError): # pragma: no cover
777-
return input_
778-
return path if path_exists else input_
773+
except (OSError, RuntimeError, ValueError):
774+
return
775+
if not path_exists:
776+
return
777+
778+
import warnings # noqa: PLC0415
779+
780+
with contextlib.suppress(Warning):
781+
warnings.warn(
782+
"`input_` strings are treated as schema text. "
783+
"The value also resolves to an existing path; pass a `Path` object to read it as a file.",
784+
stacklevel=3,
785+
)
786+
787+
788+
@contextlib.contextmanager
789+
def _warn_on_input_string_path_failure(
790+
input_: Path | str | ParseResult | Mapping[str, Any] | list[Any],
791+
) -> Iterator[None]:
792+
try:
793+
yield
794+
except Exception:
795+
_warn_if_input_string_points_to_existing_path(input_)
796+
raise
779797

780798

781799
def _create_parser_config(
@@ -1320,7 +1338,7 @@ def generate( # noqa: PLR0912, PLR0914, PLR0915
13201338
(JSON, YAML, Dict, CSV) as input.
13211339
13221340
Args:
1323-
input_: The input source (file path, string content, URL, dict,
1341+
input_: The input source (Path file input, string content, URL, dict,
13241342
list of file paths, or MCP tools list when input_file_type is
13251343
InputFileType.MCPTools).
13261344
config: A GenerateConfig object with all options. Cannot be used together with **options.
@@ -1361,8 +1379,6 @@ def generate( # noqa: PLR0912, PLR0914, PLR0915
13611379
skip_root_model = config.skip_root_model
13621380
source_override: Mapping[str, Any] | None = None
13631381

1364-
input_ = _coerce_existing_path_input(input_)
1365-
13661382
if (
13671383
isinstance(input_, list)
13681384
and input_file_type != InputFileType.MCPTools
@@ -1446,8 +1462,9 @@ def generate( # noqa: PLR0912, PLR0914, PLR0915
14461462
raise Error(msg) from exc
14471463

14481464
try:
1449-
assert isinstance(input_text_, str)
1450-
input_file_type = infer_input_type(input_text_)
1465+
with _warn_on_input_string_path_failure(input_):
1466+
assert isinstance(input_text_, str)
1467+
input_file_type = infer_input_type(input_text_)
14511468
except Exception as exc:
14521469
raise InvalidFileFormatError(exc) from exc
14531470
else:
@@ -1460,15 +1477,17 @@ def generate( # noqa: PLR0912, PLR0914, PLR0915
14601477
if isinstance(input_, Path) and input_.is_file() and input_file_type not in RAW_DATA_TYPES:
14611478
input_text = input_text_
14621479

1463-
input_text = _normalize_raw_input(input_, input_text, input_file_type, config)
1480+
with _warn_on_input_string_path_failure(input_):
1481+
input_text = _normalize_raw_input(input_, input_text, input_file_type, config)
14641482

14651483
if input_file_type == InputFileType.MCPTools:
1466-
source_override, input_file_type, skip_root_model = _convert_mcp_tools(
1467-
input_,
1468-
input_text,
1469-
config,
1470-
remote_text_cache,
1471-
)
1484+
with _warn_on_input_string_path_failure(input_):
1485+
source_override, input_file_type, skip_root_model = _convert_mcp_tools(
1486+
input_,
1487+
input_text,
1488+
config,
1489+
remote_text_cache,
1490+
)
14721491

14731492
if isinstance(input_, ParseResult) and input_file_type not in RAW_DATA_TYPES:
14741493
input_text = None
@@ -1489,29 +1508,35 @@ def generate( # noqa: PLR0912, PLR0914, PLR0915
14891508
_resolve_schema_versions(input_file_type, config.schema_version)
14901509
)
14911510

1492-
parser = _build_parser(
1493-
input_file_type,
1494-
source,
1495-
config,
1496-
additional_options,
1497-
data_model_types,
1498-
jsonschema_version=jsonschema_version,
1499-
openapi_version=openapi_version,
1500-
asyncapi_version=asyncapi_version,
1501-
xmlschema_version=xmlschema_version,
1502-
protobuf_version=protobuf_version,
1503-
)
1511+
with _warn_on_input_string_path_failure(input_):
1512+
parser = _build_parser(
1513+
input_file_type,
1514+
source,
1515+
config,
1516+
additional_options,
1517+
data_model_types,
1518+
jsonschema_version=jsonschema_version,
1519+
openapi_version=openapi_version,
1520+
asyncapi_version=asyncapi_version,
1521+
xmlschema_version=xmlschema_version,
1522+
protobuf_version=protobuf_version,
1523+
)
15041524

15051525
with chdir(config.output):
15061526
try:
1507-
results = parser.parse(
1508-
settings_path=config.settings_path,
1509-
disable_future_imports=config.disable_future_imports,
1510-
all_exports_scope=config.all_exports_scope,
1511-
all_exports_collision_strategy=config.all_exports_collision_strategy,
1512-
module_split_mode=config.module_split_mode,
1513-
collect_model_metadata=config.emit_model_metadata is not None,
1514-
)
1527+
with _warn_on_input_string_path_failure(input_):
1528+
results = parser.parse(
1529+
settings_path=config.settings_path,
1530+
disable_future_imports=config.disable_future_imports,
1531+
all_exports_scope=config.all_exports_scope,
1532+
all_exports_collision_strategy=config.all_exports_collision_strategy,
1533+
module_split_mode=config.module_split_mode,
1534+
collect_model_metadata=config.emit_model_metadata is not None,
1535+
)
1536+
except Exception:
1537+
with contextlib.suppress(BaseException):
1538+
parser._dispose() # noqa: SLF001
1539+
raise
15151540
except BaseException:
15161541
with contextlib.suppress(BaseException):
15171542
parser._dispose() # noqa: SLF001

tests/data/expected/main/generate_accepts_string_path.py renamed to tests/data/expected/main/generate_accepts_path_input.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,17 @@
22
# filename: person.json
33

44
from __future__ import annotations
5-
from pydantic import BaseModel, Field, conint
5+
66
from typing import Any
77

8+
from pydantic import BaseModel, Field, conint
9+
810

911
class Person(BaseModel):
1012
firstName: str | None = Field(None, description="The person's first name.")
1113
lastName: str | None = Field(None, description="The person's last name.")
12-
age: conint(ge=0) | None = Field(None, description='Age in years which must be equal to or greater than zero.')
14+
age: conint(ge=0) | None = Field(
15+
None, description='Age in years which must be equal to or greater than zero.'
16+
)
1317
friends: list[Any] | None = None
1418
comment: None = Field(None)

tests/data/expected/main/generate_keeps_unresolved_user_path_string_input.py renamed to tests/data/expected/main/generate_keeps_existing_path_string_input.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@
22
# filename: inline.yaml
33

44
from __future__ import annotations
5+
56
from pydantic import RootModel
67

78

89
class Model(RootModel[str]):
9-
root: str
10+
root: str

tests/data/expected/main/generate_keeps_non_path_string_input.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@
22
# filename: inline.yaml
33

44
from __future__ import annotations
5+
56
from pydantic import BaseModel
67

78

89
class Model(BaseModel):
9-
name: str
10+
name: str

0 commit comments

Comments
 (0)