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
14 changes: 14 additions & 0 deletions docs/formatting.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,14 @@ Generated code is automatically formatted using code formatters. By default, `bl

## 🎯 Default Behavior

!!! warning "Future Change"
In a future version, the default formatters will change from `black` and `isort` to `ruff`.
To prepare for this change, consider switching to ruff now using `--formatters ruff-format ruff-check`.

**CLI users**: To suppress this warning, use `--disable-warnings` or explicitly specify `--formatters black isort`.

**Library users**: Explicitly pass `formatters=[Formatter.BLACK, Formatter.ISORT]` to suppress this warning.

```bash
datamodel-codegen --input schema.yaml --output model.py
```
Expand All @@ -28,6 +36,12 @@ This runs the following formatters in order:

[Ruff](https://github.com/astral-sh/ruff) is a fast Python linter and formatter. To use it:

!!! note "Installation Required"
ruff is an optional dependency. Install it with:
```bash
pip install 'datamodel-code-generator[ruff]'
```

```bash
# Use ruff for both linting and formatting
datamodel-codegen --formatters ruff-check ruff-format --input schema.yaml --output model.py
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,7 @@ filterwarnings = [
"ignore:^.*No schemas found in components/schemas.*",
"ignore:^.*Dataclass .* has a field ordering conflict due to inheritance.*:UserWarning",
"ignore:^.*is empty or not a dict. Skipping this file.*:UserWarning",
"ignore:^.*The default formatters.*:FutureWarning",
]
norecursedirs = [ "tests/data/*", ".tox" ]
verbosity_assertions = 2
Expand Down
6 changes: 5 additions & 1 deletion src/datamodel_code_generator/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -879,7 +879,11 @@ def get_header_and_first_line(csv_file: IO[str]) -> dict[str, Any]:

file.close()

if defer_formatting and (Formatter.RUFF_CHECK in config.formatters or Formatter.RUFF_FORMAT in config.formatters):
if (
defer_formatting
and config.formatters
and (Formatter.RUFF_CHECK in config.formatters or Formatter.RUFF_FORMAT in config.formatters)
):
code_formatter = CodeFormatter(
config.target_python_version,
config.settings_path,
Expand Down
3 changes: 1 addition & 2 deletions src/datamodel_code_generator/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,6 @@
)
from datamodel_code_generator.arguments import DEFAULT_ENCODING, arg_parser, namespace
from datamodel_code_generator.format import (
DEFAULT_FORMATTERS,
DateClassType,
DatetimeClassType,
Formatter,
Expand Down Expand Up @@ -595,7 +594,7 @@ def validate_class_name_affix_scope(cls, v: str | ClassNameAffixScope | None) ->
no_alias: bool = False
use_frozen_field: bool = False
use_default_factory_for_optional_nested_models: bool = False
formatters: list[Formatter] = DEFAULT_FORMATTERS
formatters: list[Formatter] | None = None
parent_scoped_naming: bool = False
naming_strategy: Optional[NamingStrategy] = None # noqa: UP045
duplicate_name_suffix: Optional[dict[str, str]] = None # noqa: UP045
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ class GenerateConfigDict(TypedDict):
no_alias: NotRequired[bool]
use_frozen_field: NotRequired[bool]
use_default_factory_for_optional_nested_models: NotRequired[bool]
formatters: NotRequired[list[Formatter]]
formatters: NotRequired[list[Formatter] | None]
settings_path: NotRequired[Path | None]
parent_scoped_naming: NotRequired[bool]
naming_strategy: NotRequired[NamingStrategy | None]
Expand Down
2 changes: 1 addition & 1 deletion src/datamodel_code_generator/_types/parser_config_dicts.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ class ParserConfigDict(TypedDict):
no_alias: NotRequired[bool]
use_frozen_field: NotRequired[bool]
use_default_factory_for_optional_nested_models: NotRequired[bool]
formatters: NotRequired[list[Formatter]]
formatters: NotRequired[list[Formatter] | None]
defer_formatting: NotRequired[bool]
parent_scoped_naming: NotRequired[bool]
naming_strategy: NotRequired[NamingStrategy | None]
Expand Down
5 changes: 2 additions & 3 deletions src/datamodel_code_generator/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@
TargetPydanticVersion,
)
from datamodel_code_generator.format import (
DEFAULT_FORMATTERS,
DateClassType,
DatetimeClassType,
Formatter,
Expand Down Expand Up @@ -185,7 +184,7 @@ class Config:
no_alias: bool = False
use_frozen_field: bool = False
use_default_factory_for_optional_nested_models: bool = False
formatters: list[Formatter] = DEFAULT_FORMATTERS
formatters: list[Formatter] | None = None
settings_path: Path | None = None
parent_scoped_naming: bool = False
naming_strategy: NamingStrategy | None = None
Expand Down Expand Up @@ -318,7 +317,7 @@ class Config:
no_alias: bool = False
use_frozen_field: bool = False
use_default_factory_for_optional_nested_models: bool = False
formatters: list[Formatter] = DEFAULT_FORMATTERS
formatters: list[Formatter] | None = None
defer_formatting: bool = False
parent_scoped_naming: bool = False
naming_strategy: NamingStrategy | None = None
Expand Down
13 changes: 12 additions & 1 deletion src/datamodel_code_generator/format.py
Original file line number Diff line number Diff line change
Expand Up @@ -193,10 +193,21 @@ def __init__( # noqa: PLR0912, PLR0913, PLR0915, PLR0917
custom_formatters: list[str] | None = None,
custom_formatters_kwargs: dict[str, Any] | None = None,
encoding: str = "utf-8",
formatters: list[Formatter] = DEFAULT_FORMATTERS,
formatters: list[Formatter] | None = None,
defer_formatting: bool = False, # noqa: FBT001, FBT002
) -> None:
"""Initialize code formatter with configuration for black, isort, ruff, and custom formatters."""
if formatters is None:
warn(
"The default formatters (black, isort) will be replaced by ruff in a future version. "
"To prepare for this change, consider using: formatters=[Formatter.RUFF_FORMAT, Formatter.RUFF_CHECK]. "
"Install ruff with: pip install 'datamodel-code-generator[ruff]'. "
"To suppress this warning, specify formatters explicitly.",
FutureWarning,
stacklevel=2,
)
formatters = list(DEFAULT_FORMATTERS)

if not settings_path:
settings_path = Path.cwd()
elif settings_path.is_file():
Expand Down
2 changes: 1 addition & 1 deletion src/datamodel_code_generator/parser/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -928,7 +928,7 @@ def __init__( # noqa: PLR0912, PLR0915
self.custom_formatters_kwargs = config.custom_formatters_kwargs
self.treat_dot_as_module = config.treat_dot_as_module
self.default_field_extras: dict[str, Any] | None = config.default_field_extras
self.formatters: list[Formatter] = config.formatters
self.formatters: list[Formatter] | None = config.formatters
self.defer_formatting: bool = config.defer_formatting
self.type_mappings: dict[tuple[str, str], str] = Parser._parse_type_mappings(config.type_mappings)
self.type_overrides: dict[str, str] = config.type_overrides or {}
Expand Down
2 changes: 1 addition & 1 deletion tests/data/expected/main/input_model/config_class.py
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,7 @@ class GenerateConfig(TypedDict):
no_alias: NotRequired[bool]
use_frozen_field: NotRequired[bool]
use_default_factory_for_optional_nested_models: NotRequired[bool]
formatters: NotRequired[list[Formatter]]
formatters: NotRequired[list[Formatter] | None]
settings_path: NotRequired[str | None]
parent_scoped_naming: NotRequired[bool]
naming_strategy: NotRequired[NamingStrategy | None]
Expand Down
6 changes: 3 additions & 3 deletions tests/main/test_public_api_signature_baseline.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
import pytest
from typing_extensions import NotRequired

from datamodel_code_generator import DEFAULT_FORMATTERS, DEFAULT_SHARED_MODULE_NAME, generate
from datamodel_code_generator import DEFAULT_SHARED_MODULE_NAME, generate
from datamodel_code_generator.enums import (
AllExportsCollisionStrategy,
AllExportsScope,
Expand Down Expand Up @@ -166,7 +166,7 @@ def _baseline_generate(
no_alias: bool = False,
use_frozen_field: bool = False,
use_default_factory_for_optional_nested_models: bool = False,
formatters: list[Formatter] = DEFAULT_FORMATTERS,
formatters: list[Formatter] | None = None,
settings_path: Path | None = None,
parent_scoped_naming: bool = False,
naming_strategy: NamingStrategy | None = None,
Expand Down Expand Up @@ -293,7 +293,7 @@ def __init__(
no_alias: bool = False,
use_frozen_field: bool = False,
use_default_factory_for_optional_nested_models: bool = False,
formatters: list[Formatter] = DEFAULT_FORMATTERS,
formatters: list[Formatter] | None = None,
defer_formatting: bool = False,
parent_scoped_naming: bool = False,
naming_strategy: NamingStrategy | None = None,
Expand Down
48 changes: 44 additions & 4 deletions tests/test_format.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from __future__ import annotations

import sys
import warnings
from pathlib import Path
from unittest import mock

Expand Down Expand Up @@ -48,7 +49,11 @@ def test_format_code_with_skip_string_normalization(
) -> None:
"""Test code formatting with skip string normalization option."""
monkeypatch.chdir(tmp_path)
formatter = CodeFormatter(PythonVersionMin, skip_string_normalization=skip_string_normalization)
formatter = CodeFormatter(
PythonVersionMin,
skip_string_normalization=skip_string_normalization,
formatters=[Formatter.BLACK, Formatter.ISORT],
)

formatted_code = formatter.format_code("a = 'b'")

Expand All @@ -61,6 +66,7 @@ def test_format_code_un_exist_custom_formatter() -> None:
_ = CodeFormatter(
PythonVersionMin,
custom_formatters=[UN_EXIST_FORMATTER],
formatters=[Formatter.BLACK, Formatter.ISORT],
)


Expand All @@ -70,6 +76,7 @@ def test_format_code_invalid_formatter_name() -> None:
_ = CodeFormatter(
PythonVersionMin,
custom_formatters=[WRONG_FORMATTER],
formatters=[Formatter.BLACK, Formatter.ISORT],
)


Expand All @@ -79,6 +86,7 @@ def test_format_code_is_not_subclass() -> None:
_ = CodeFormatter(
PythonVersionMin,
custom_formatters=[NOT_SUBCLASS_FORMATTER],
formatters=[Formatter.BLACK, Formatter.ISORT],
)


Expand All @@ -88,6 +96,7 @@ def test_format_code_with_custom_formatter_without_kwargs(tmp_path: Path, monkey
formatter = CodeFormatter(
PythonVersionMin,
custom_formatters=[ADD_COMMENT_FORMATTER],
formatters=[Formatter.BLACK, Formatter.ISORT],
)

formatted_code = formatter.format_code("x = 1\ny = 2")
Expand All @@ -102,6 +111,7 @@ def test_format_code_with_custom_formatter_with_kwargs(tmp_path: Path, monkeypat
PythonVersionMin,
custom_formatters=[ADD_LICENSE_FORMATTER],
custom_formatters_kwargs={"license_file": EXAMPLE_LICENSE_FILE},
formatters=[Formatter.BLACK, Formatter.ISORT],
)

formatted_code = formatter.format_code("x = 1\ny = 2")
Expand All @@ -128,6 +138,7 @@ def test_format_code_with_two_custom_formatters(tmp_path: Path, monkeypatch: pyt
ADD_LICENSE_FORMATTER,
],
custom_formatters_kwargs={"license_file": EXAMPLE_LICENSE_FILE},
formatters=[Formatter.BLACK, Formatter.ISORT],
)

formatted_code = formatter.format_code("x = 1\ny = 2")
Expand Down Expand Up @@ -190,7 +201,9 @@ def test_settings_path_with_existing_file(tmp_path: Path) -> None:
existing_file = tmp_path / "existing.py"
existing_file.write_text("", encoding="utf-8")

formatter = CodeFormatter(PythonVersionMin, settings_path=existing_file)
formatter = CodeFormatter(
PythonVersionMin, settings_path=existing_file, formatters=[Formatter.BLACK, Formatter.ISORT]
)

assert formatter.settings_path == str(tmp_path)

Expand All @@ -201,7 +214,9 @@ def test_settings_path_with_nonexistent_file(tmp_path: Path) -> None:
pyproject.write_text("[tool.black]\nline-length = 60\n", encoding="utf-8")
nonexistent_file = tmp_path / "nonexistent.py"

formatter = CodeFormatter(PythonVersionMin, settings_path=nonexistent_file)
formatter = CodeFormatter(
PythonVersionMin, settings_path=nonexistent_file, formatters=[Formatter.BLACK, Formatter.ISORT]
)

assert formatter.settings_path == str(tmp_path)

Expand All @@ -212,7 +227,9 @@ def test_settings_path_with_deeply_nested_nonexistent_path(tmp_path: Path) -> No
pyproject.write_text("[tool.black]\nline-length = 60\n", encoding="utf-8")
nested_path = tmp_path / "a" / "b" / "c" / "nonexistent.py"

formatter = CodeFormatter(PythonVersionMin, settings_path=nested_path)
formatter = CodeFormatter(
PythonVersionMin, settings_path=nested_path, formatters=[Formatter.BLACK, Formatter.ISORT]
)

assert formatter.settings_path == str(tmp_path)

Expand Down Expand Up @@ -338,3 +355,26 @@ def test_generate_with_ruff_batch_formatting(tmp_path: Path) -> None:
check=False,
cwd=mock.ANY,
)


def test_code_formatter_warns_when_formatters_is_none(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
"""Test that FutureWarning is emitted when formatters is None (default)."""
monkeypatch.chdir(tmp_path)
with pytest.warns(FutureWarning, match="default formatters"):
CodeFormatter(PythonVersionMin)


def test_code_formatter_no_warning_when_formatters_explicit(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
"""Test that no warning is emitted when formatters is explicitly specified."""
monkeypatch.chdir(tmp_path)
with warnings.catch_warnings():
warnings.simplefilter("error")
CodeFormatter(PythonVersionMin, formatters=[Formatter.BLACK, Formatter.ISORT])


def test_code_formatter_no_warning_when_formatters_empty(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
"""Test that no warning is emitted when formatters is empty list."""
monkeypatch.chdir(tmp_path)
with warnings.catch_warnings():
warnings.simplefilter("error")
CodeFormatter(PythonVersionMin, formatters=[])
Loading