Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 2 additions & 2 deletions docs/llms-full.txt
Original file line number Diff line number Diff line change
Expand Up @@ -29212,7 +29212,7 @@ The tables below are generated from the input type enum, parser routing code, sc
| MCP tool schemas | `mcp-tools` | `JsonSchemaParser after MCP conversion` | Converted to JSON Schema; `draft-04`, `draft-06`, `draft-07`, `2019-09`, `2020-12`, `auto` | Explicit only |
| JSON data | `json` | `JsonSchemaParser after genson conversion` | Converted to JSON Schema; `draft-04`, `draft-06`, `draft-07`, `2019-09`, `2020-12`, `auto` | Mapping that is not a schema/OpenAPI/AsyncAPI/Avro document |
| YAML data | `yaml` | `JsonSchemaParser after genson conversion` | Converted to JSON Schema; `draft-04`, `draft-06`, `draft-07`, `2019-09`, `2020-12`, `auto` | Explicit for YAML sample data |
| CSV data | `csv` | `JsonSchemaParser after genson conversion` | Converted to JSON Schema; `draft-04`, `draft-06`, `draft-07`, `2019-09`, `2020-12`, `auto` | Fallback when text cannot parse as YAML |
| CSV data | `csv` | `JsonSchemaParser after genson conversion` | Converted to JSON Schema; `draft-04`, `draft-06`, `draft-07`, `2019-09`, `2020-12`, `auto` | Detected from CSV-like text with consistent comma counts across non-empty lines or explicit `csv` |
| Python dictionary data | `dict` | `JsonSchemaParser after genson conversion` | Converted to JSON Schema; `draft-04`, `draft-06`, `draft-07`, `2019-09`, `2020-12`, `auto` | Explicit for mapping input |
| Python input model | `--input-model` | `JsonSchemaParser` after Python schema conversion | JSON Schema after conversion; dict input can select another explicit schema type | Explicit only |

Expand Down Expand Up @@ -29542,7 +29542,7 @@ The tables below are generated from the input type enum, parser routing code, sc
| MCP tool schemas | `mcp-tools` | `JsonSchemaParser after MCP conversion` | Converted to JSON Schema; `draft-04`, `draft-06`, `draft-07`, `2019-09`, `2020-12`, `auto` | Explicit only |
| JSON data | `json` | `JsonSchemaParser after genson conversion` | Converted to JSON Schema; `draft-04`, `draft-06`, `draft-07`, `2019-09`, `2020-12`, `auto` | Mapping that is not a schema/OpenAPI/AsyncAPI/Avro document |
| YAML data | `yaml` | `JsonSchemaParser after genson conversion` | Converted to JSON Schema; `draft-04`, `draft-06`, `draft-07`, `2019-09`, `2020-12`, `auto` | Explicit for YAML sample data |
| CSV data | `csv` | `JsonSchemaParser after genson conversion` | Converted to JSON Schema; `draft-04`, `draft-06`, `draft-07`, `2019-09`, `2020-12`, `auto` | Fallback when text cannot parse as YAML |
| CSV data | `csv` | `JsonSchemaParser after genson conversion` | Converted to JSON Schema; `draft-04`, `draft-06`, `draft-07`, `2019-09`, `2020-12`, `auto` | Detected from CSV-like text with consistent comma counts across non-empty lines or explicit `csv` |
| Python dictionary data | `dict` | `JsonSchemaParser after genson conversion` | Converted to JSON Schema; `draft-04`, `draft-06`, `draft-07`, `2019-09`, `2020-12`, `auto` | Explicit for mapping input |
| Python input model | `--input-model` | `JsonSchemaParser` after Python schema conversion | JSON Schema after conversion; dict input can select another explicit schema type | Explicit only |

Expand Down
2 changes: 1 addition & 1 deletion docs/supported-data-types.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ The tables below are generated from the input type enum, parser routing code, sc
| MCP tool schemas | `mcp-tools` | `JsonSchemaParser after MCP conversion` | Converted to JSON Schema; `draft-04`, `draft-06`, `draft-07`, `2019-09`, `2020-12`, `auto` | Explicit only |
| JSON data | `json` | `JsonSchemaParser after genson conversion` | Converted to JSON Schema; `draft-04`, `draft-06`, `draft-07`, `2019-09`, `2020-12`, `auto` | Mapping that is not a schema/OpenAPI/AsyncAPI/Avro document |
| YAML data | `yaml` | `JsonSchemaParser after genson conversion` | Converted to JSON Schema; `draft-04`, `draft-06`, `draft-07`, `2019-09`, `2020-12`, `auto` | Explicit for YAML sample data |
| CSV data | `csv` | `JsonSchemaParser after genson conversion` | Converted to JSON Schema; `draft-04`, `draft-06`, `draft-07`, `2019-09`, `2020-12`, `auto` | Fallback when text cannot parse as YAML |
| CSV data | `csv` | `JsonSchemaParser after genson conversion` | Converted to JSON Schema; `draft-04`, `draft-06`, `draft-07`, `2019-09`, `2020-12`, `auto` | Detected from CSV-like text with consistent comma counts across non-empty lines or explicit `csv` |
| Python dictionary data | `dict` | `JsonSchemaParser after genson conversion` | Converted to JSON Schema; `draft-04`, `draft-06`, `draft-07`, `2019-09`, `2020-12`, `auto` | Explicit for mapping input |
| Python input model | `--input-model` | `JsonSchemaParser` after Python schema conversion | JSON Schema after conversion; dict input can select another explicit schema type | Explicit only |

Expand Down
2 changes: 1 addition & 1 deletion docs/supported_formats.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ The tables below are generated from the input type enum, parser routing code, sc
| MCP tool schemas | `mcp-tools` | `JsonSchemaParser after MCP conversion` | Converted to JSON Schema; `draft-04`, `draft-06`, `draft-07`, `2019-09`, `2020-12`, `auto` | Explicit only |
| JSON data | `json` | `JsonSchemaParser after genson conversion` | Converted to JSON Schema; `draft-04`, `draft-06`, `draft-07`, `2019-09`, `2020-12`, `auto` | Mapping that is not a schema/OpenAPI/AsyncAPI/Avro document |
| YAML data | `yaml` | `JsonSchemaParser after genson conversion` | Converted to JSON Schema; `draft-04`, `draft-06`, `draft-07`, `2019-09`, `2020-12`, `auto` | Explicit for YAML sample data |
| CSV data | `csv` | `JsonSchemaParser after genson conversion` | Converted to JSON Schema; `draft-04`, `draft-06`, `draft-07`, `2019-09`, `2020-12`, `auto` | Fallback when text cannot parse as YAML |
| CSV data | `csv` | `JsonSchemaParser after genson conversion` | Converted to JSON Schema; `draft-04`, `draft-06`, `draft-07`, `2019-09`, `2020-12`, `auto` | Detected from CSV-like text with consistent comma counts across non-empty lines or explicit `csv` |
| Python dictionary data | `dict` | `JsonSchemaParser after genson conversion` | Converted to JSON Schema; `draft-04`, `draft-06`, `draft-07`, `2019-09`, `2020-12`, `auto` | Explicit for mapping input |
| Python input model | `--input-model` | `JsonSchemaParser` after Python schema conversion | JSON Schema after conversion; dict input can select another explicit schema type | Explicit only |

Expand Down
4 changes: 3 additions & 1 deletion scripts/build_schema_docs.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,9 @@
InputFileType.Json: "Mapping that is not a schema/OpenAPI/AsyncAPI/Avro document",
InputFileType.Yaml: "Explicit for YAML sample data",
InputFileType.Dict: "Explicit for mapping input",
InputFileType.CSV: "Fallback when text cannot parse as YAML",
InputFileType.CSV: (
"Detected from CSV-like text with consistent comma counts across non-empty lines or explicit `csv`"
),
InputFileType.GraphQL: "Explicit only",
}

Expand Down
42 changes: 36 additions & 6 deletions src/datamodel_code_generator/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -1524,8 +1524,11 @@ def infer_input_type(text: str) -> InputFileType: # noqa: PLR0911, PLR0912

try:
data = load_yaml(text)
except get_yaml_parse_errors():
return InputFileType.CSV
except get_yaml_parse_errors() as exc:
if not _is_json_text(text) and _looks_like_csv_text(text):
return InputFileType.CSV
msg = _infer_input_type_error_message(parse_error=exc)
raise Error(msg) from exc
if isinstance(data, dict):
if is_asyncapi(data):
return InputFileType.AsyncAPI
Expand All @@ -1546,17 +1549,44 @@ def infer_input_type(text: str) -> InputFileType: # noqa: PLR0911, PLR0912
if is_avro_schema_data(data):
return InputFileType.Avro
if isinstance(data, str):
if _looks_like_csv_text(text):
return InputFileType.CSV
from datamodel_code_generator.parser._avro_detection import is_avro_schema_data # noqa: PLC0415

if is_avro_schema_data(data):
return InputFileType.Avro
msg = (
"Can't infer input file type from the input data. "
"Please specify the input file type explicitly with --input-file-type option."
)
msg = _infer_input_type_error_message()
raise Error(msg)


def _infer_input_type_error_message(*, parse_error: Exception | None = None) -> str:
message = "Can't infer input file type from the input data."
hint = "Please specify the input file type explicitly with --input-file-type option."
if parse_error is None:
return f"{message} {hint}"
return f"{message} YAML parser error: {type(parse_error).__name__}: {parse_error}. {hint}"


_MIN_CSV_NON_EMPTY_LINES = 2


def _looks_like_csv_text(text: str) -> bool:
comma_count: int | None = None
matched_lines = 0
for raw_line in text.splitlines():
if not (line := raw_line.strip()):
continue
if (current_comma_count := line.count(",")) == 0:
return False
match comma_count:
case None:
comma_count = current_comma_count
case _ if current_comma_count != comma_count:
return False
matched_lines += 1
return matched_lines >= _MIN_CSV_NON_EMPTY_LINES


inferred_message = (
"The input file type was determined to be: {}\nThis can be specified explicitly with the "
"`--input-file-type` option."
Expand Down
2 changes: 1 addition & 1 deletion tests/data/expected/schema_docs/input_format_guide.txt
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ The tables below are generated from the input type enum, parser routing code, sc
| MCP tool schemas | `mcp-tools` | `JsonSchemaParser after MCP conversion` | Converted to JSON Schema; `draft-04`, `draft-06`, `draft-07`, `2019-09`, `2020-12`, `auto` | Explicit only |
| JSON data | `json` | `JsonSchemaParser after genson conversion` | Converted to JSON Schema; `draft-04`, `draft-06`, `draft-07`, `2019-09`, `2020-12`, `auto` | Mapping that is not a schema/OpenAPI/AsyncAPI/Avro document |
| YAML data | `yaml` | `JsonSchemaParser after genson conversion` | Converted to JSON Schema; `draft-04`, `draft-06`, `draft-07`, `2019-09`, `2020-12`, `auto` | Explicit for YAML sample data |
| CSV data | `csv` | `JsonSchemaParser after genson conversion` | Converted to JSON Schema; `draft-04`, `draft-06`, `draft-07`, `2019-09`, `2020-12`, `auto` | Fallback when text cannot parse as YAML |
| CSV data | `csv` | `JsonSchemaParser after genson conversion` | Converted to JSON Schema; `draft-04`, `draft-06`, `draft-07`, `2019-09`, `2020-12`, `auto` | Detected from CSV-like text with consistent comma counts across non-empty lines or explicit `csv` |
| Python dictionary data | `dict` | `JsonSchemaParser after genson conversion` | Converted to JSON Schema; `draft-04`, `draft-06`, `draft-07`, `2019-09`, `2020-12`, `auto` | Explicit for mapping input |
| Python input model | `--input-model` | `JsonSchemaParser` after Python schema conversion | JSON Schema after conversion; dict input can select another explicit schema type | Explicit only |

Expand Down
2 changes: 2 additions & 0 deletions tests/data/yaml/broken.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
root:
child: [
37 changes: 37 additions & 0 deletions tests/main/test_main_csv.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
from tests.main.conftest import (
CSV_DATA_PATH,
EXPECTED_CSV_PATH,
JSON_DATA_PATH,
YAML_DATA_PATH,
run_main_and_assert,
)

Expand All @@ -33,6 +35,41 @@ def test_csv_file(output_file: Path) -> None:
)


def test_csv_file_auto_detection(output_file: Path, capsys: pytest.CaptureFixture[str]) -> None:
"""Test CSV file input is detected by auto input type."""
run_main_and_assert(
input_path=CSV_DATA_PATH / "simple.csv",
output_path=output_file,
assert_func=assert_file_content,
expected_file="csv_file_simple.py",
capsys=capsys,
expected_stderr_contains="The input file type was determined to be: csv",
)


@pytest.mark.parametrize(
"input_path",
[
JSON_DATA_PATH / "broken.json",
YAML_DATA_PATH / "broken.yaml",
],
)
def test_auto_detection_rejects_malformed_non_csv_input(
input_path: Path,
output_file: Path,
capsys: pytest.CaptureFixture[str],
) -> None:
"""Test auto input type does not treat malformed JSON/YAML as CSV data."""
run_main_and_assert(
input_path=input_path,
output_path=output_file,
expected_exit=Exit.ERROR,
capsys=capsys,
expected_stderr_contains="Please specify the input file type explicitly with --input-file-type option.",
output_should_not_exist=True,
)


def test_csv_stdin(monkeypatch: pytest.MonkeyPatch, output_file: Path) -> None:
"""Test CSV stdin input code generation."""
run_main_and_assert(
Expand Down
59 changes: 58 additions & 1 deletion tests/test_infer_input_type.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,14 @@

import pytest

from datamodel_code_generator import Error, InputFileType, _is_json_text, _is_xml_text, infer_input_type
from datamodel_code_generator import (
Error,
InputFileType,
_is_json_text,
_is_xml_text,
_looks_like_csv_text,
infer_input_type,
)

DATA_PATH: Path = Path(__file__).parent / "data"
HEAVY_INFERENCE_MODULES = (
Expand Down Expand Up @@ -166,6 +173,56 @@ def test_is_json_text() -> None:
assert not _is_json_text("name: value")


@pytest.mark.parametrize(
("text", "expected"),
[
("id,name\n1,taro\n", True),
("\n id, name \n 1, taro \n", True),
("id,name\n", False),
("name\nvalue\n", False),
("id,name,tel\n1,taro\n", False),
("id,name\n1,taro\n2,jiro,extra\n", False),
("", False),
],
)
def test_looks_like_csv_text(text: str, expected: bool) -> None:
"""Test bounded CSV text detection for auto inference fallback."""
assert _looks_like_csv_text(text) is expected


@pytest.mark.parametrize(
("text", "expected"),
[
("id,name\n1,taro\n", InputFileType.CSV),
("\n id, name \n 1, taro \n", InputFileType.CSV),
],
)
def test_infer_input_type_detects_csv_text(text: str, expected: InputFileType) -> None:
"""Test auto inference detects CSV data without treating all YAML errors as CSV."""
assert infer_input_type(text) == expected


@pytest.mark.parametrize(
"text",
[
'{"Pet": {',
"root:\n child: [",
"id,name,tel\n1,taro\n",
"id,name\n1,taro\n2,jiro,extra\n",
],
)
def test_infer_input_type_rejects_non_csv_parse_errors(text: str) -> None:
"""Test malformed JSON/YAML and uneven comma data do not infer as CSV."""
with pytest.raises(
Error,
match=(
r"(?s)Can't infer input file type from the input data\..*"
r"Please specify the input file type explicitly with --input-file-type option\."
),
):
infer_input_type(text)


def test_infer_input_type_non_schema_xml() -> None:
"""Test non-schema XML does not infer as XML Schema."""
with pytest.raises(
Expand Down
20 changes: 16 additions & 4 deletions tests/test_yaml_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
import pytest
import yaml

from datamodel_code_generator import InputFileType, infer_input_type, load_yaml, load_yaml_dict_from_path
from datamodel_code_generator import Error, InputFileType, infer_input_type, load_yaml, load_yaml_dict_from_path
from datamodel_code_generator.util import (
_is_yaml_deprecated_bool_warning_enabled,
get_yaml_backend,
Expand Down Expand Up @@ -210,19 +210,31 @@ class TestInferInputType:
def test_csv_with_pyyaml_error(self) -> None:
"""YAML parse error from PyYAML returns CSV type."""
with patch.dict("sys.modules", {"ryaml": None}):
result = infer_input_type("a,b,c\n1,2,3\n::")
result = infer_input_type("a,b:\n1,2\n")
assert result == InputFileType.CSV

def test_csv_with_ryaml_error(self) -> None:
"""YAML parse error from ryaml returns CSV type."""
"""CSV-looking YAML parse error from ryaml returns CSV type."""
mock_invalid_yaml_error = type("InvalidYamlError", (Exception,), {})
mock_ryaml = MagicMock()
mock_ryaml.InvalidYamlError = mock_invalid_yaml_error
mock_ryaml.loads.side_effect = mock_invalid_yaml_error("parse error")
with patch.dict("sys.modules", {"ryaml": mock_ryaml}):
result = infer_input_type(":::invalid yaml:::")
result = infer_input_type("a,b,c\n1,2,3\n")
assert result == InputFileType.CSV

def test_non_csv_ryaml_error_raises_error(self) -> None:
"""Non-CSV YAML parse error from ryaml raises inference error."""
mock_invalid_yaml_error = type("InvalidYamlError", (Exception,), {})
mock_ryaml = MagicMock()
mock_ryaml.InvalidYamlError = mock_invalid_yaml_error
mock_ryaml.loads.side_effect = mock_invalid_yaml_error("parse error")
with (
patch.dict("sys.modules", {"ryaml": mock_ryaml}),
pytest.raises(Error, match=r"YAML parser error: InvalidYamlError: parse error"),
):
infer_input_type(":::invalid yaml:::")

def test_openapi_detection(self) -> None:
"""OpenAPI input is detected correctly regardless of backend."""
with patch.dict("sys.modules", {"ryaml": None}):
Expand Down
Loading