Skip to content

Commit d1d6f4f

Browse files
ubaumannkoxudaxi
andauthored
Support ClassVar for Pydantic v2 (#2920)
* Support ClassVar for Pydantic v2 * Implement ClassVar feature in code without changing template * Remove commented-out debug code * Fix unused Field import for ClassVar fields * Add UNDEFINED check for ClassVar default and additional tests * Fix import order --------- Co-authored-by: Koudai Aono <koxudaxi@gmail.com>
1 parent 4b29263 commit d1d6f4f

9 files changed

Lines changed: 144 additions & 4 deletions

File tree

src/datamodel_code_generator/model/pydantic/base_model.py

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
DataModelFieldBase,
2020
)
2121
from datamodel_code_generator.model._types import WrappedDefault
22-
from datamodel_code_generator.model.base import UNDEFINED
22+
from datamodel_code_generator.model.base import UNDEFINED, repr_set_sorted
2323
from datamodel_code_generator.model.pydantic.imports import (
2424
IMPORT_ANYURL,
2525
IMPORT_EXTRA,
@@ -91,6 +91,8 @@ def validator(self) -> str | None:
9191
@property
9292
def field(self) -> str | None:
9393
"""For backwards compatibility."""
94+
if self.is_class_var:
95+
return None
9496
result = str(self)
9597
if (
9698
self.use_default_kwarg
@@ -250,17 +252,32 @@ def __str__(self) -> str: # noqa: PLR0912
250252
elif self.required:
251253
field_arguments = ["...", *field_arguments]
252254
elif not default_factory:
253-
from datamodel_code_generator.model.base import repr_set_sorted # noqa: PLC0415
254-
255255
default_repr = repr_set_sorted(self.default) if isinstance(self.default, set) else repr(self.default)
256256
field_arguments = [default_repr, *field_arguments]
257257

258+
if self.is_class_var:
259+
if self.default is UNDEFINED: # pragma: no cover
260+
return ""
261+
return repr_set_sorted(self.default) if isinstance(self.default, set) else repr(self.default)
262+
258263
return f"Field({', '.join(field_arguments)})"
259264

265+
@property
266+
def is_class_var(self) -> bool:
267+
"""Check if this field is a ClassVar."""
268+
return self.extras.get("x-is-classvar") is True
269+
270+
@property
271+
def type_hint(self) -> str:
272+
"""Get the type hint including ClassVar if applicable."""
273+
if self.is_class_var:
274+
return f"ClassVar[{super().type_hint}]"
275+
return super().type_hint
276+
260277
@property
261278
def annotated(self) -> str | None:
262279
"""Get the Annotated type hint if use_annotated is enabled."""
263-
if not self.use_annotated or not str(self):
280+
if not self.use_annotated or not str(self) or self.is_class_var:
264281
return None
265282
return f"Annotated[{self.type_hint}, {self!s}]"
266283

src/datamodel_code_generator/model/pydantic_v2/base_model.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
from datamodel_code_generator.imports import IMPORT_ANY, Import
1616
from datamodel_code_generator.model.base import ALL_MODEL, UNDEFINED, BaseClassDataType, DataModelFieldBase
17+
from datamodel_code_generator.model.imports import IMPORT_CLASSVAR
1718
from datamodel_code_generator.model.pydantic.base_model import (
1819
BaseModelBase,
1920
)
@@ -192,6 +193,8 @@ def imports(self) -> tuple[Import, ...]:
192193
"""Get all required imports including AliasChoices and Field for discriminator."""
193194
base_imports = super().imports
194195
extra_imports: list[Import] = []
196+
if self.is_class_var:
197+
extra_imports.append(IMPORT_CLASSVAR)
195198
if self.validation_aliases:
196199
from datamodel_code_generator.model.pydantic_v2.imports import IMPORT_ALIAS_CHOICES # noqa: PLC0415
197200

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: has_classvar_extra.json
3+
# timestamp: 2019-07-26T00:00:00+00:00
4+
5+
from __future__ import annotations
6+
7+
from typing import ClassVar
8+
9+
from pydantic import BaseModel
10+
11+
12+
class Model(BaseModel):
13+
namespace: ClassVar[str | None] = 'test'
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: has_classvar_extra_annotated.json
3+
# timestamp: 2019-07-26T00:00:00+00:00
4+
5+
from __future__ import annotations
6+
7+
from typing import ClassVar
8+
9+
from pydantic import BaseModel
10+
11+
12+
class Model(BaseModel):
13+
namespace: ClassVar[str | None] = 'test'
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: has_classvar_extra_set.json
3+
# timestamp: 2019-07-26T00:00:00+00:00
4+
5+
from __future__ import annotations
6+
7+
from typing import ClassVar
8+
9+
from pydantic import BaseModel
10+
11+
12+
class Model(BaseModel):
13+
tags: ClassVar[set[str] | None] = {'a', 'b'}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"type": "object",
3+
"properties": {
4+
"namespace": {
5+
"type": "string",
6+
"x-is-classvar": true,
7+
"default": "test"
8+
}
9+
}
10+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"type": "object",
3+
"properties": {
4+
"namespace": {
5+
"type": "string",
6+
"x-is-classvar": true,
7+
"default": "test",
8+
"minLength": 1
9+
}
10+
}
11+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"type": "object",
3+
"properties": {
4+
"tags": {
5+
"type": "array",
6+
"x-is-classvar": true,
7+
"default": ["a", "b"],
8+
"uniqueItems": true,
9+
"items": {
10+
"type": "string"
11+
}
12+
}
13+
}
14+
}

tests/main/jsonschema/test_main_jsonschema.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8001,6 +8001,52 @@ def test_validators_requires_pydantic_v2(output_file: Path, tmp_path: Path, caps
80018001
)
80028002

80038003

8004+
def test_jsonschema_classvar_extra_pydantic_v2(output_file: Path) -> None:
8005+
"""Test default value handling."""
8006+
run_main_and_assert(
8007+
input_path=JSON_SCHEMA_DATA_PATH / "has_classvar_extra.json",
8008+
output_path=output_file,
8009+
input_file_type="jsonschema",
8010+
assert_func=assert_file_content,
8011+
expected_file="has_classvar_extra.py",
8012+
extra_args=["--output-model-type", "pydantic_v2.BaseModel", "--field-include-all-keys"],
8013+
)
8014+
8015+
8016+
def test_jsonschema_classvar_extra_set_pydantic_v2(output_file: Path) -> None:
8017+
"""Test ClassVar with set default value."""
8018+
run_main_and_assert(
8019+
input_path=JSON_SCHEMA_DATA_PATH / "has_classvar_extra_set.json",
8020+
output_path=output_file,
8021+
input_file_type="jsonschema",
8022+
assert_func=assert_file_content,
8023+
expected_file="has_classvar_extra_set.py",
8024+
extra_args=[
8025+
"--output-model-type",
8026+
"pydantic_v2.BaseModel",
8027+
"--field-include-all-keys",
8028+
"--use-unique-items-as-set",
8029+
],
8030+
)
8031+
8032+
8033+
def test_jsonschema_classvar_extra_annotated_pydantic_v2(output_file: Path) -> None:
8034+
"""Test ClassVar with use_annotated option."""
8035+
run_main_and_assert(
8036+
input_path=JSON_SCHEMA_DATA_PATH / "has_classvar_extra_annotated.json",
8037+
output_path=output_file,
8038+
input_file_type="jsonschema",
8039+
assert_func=assert_file_content,
8040+
expected_file="has_classvar_extra_annotated.py",
8041+
extra_args=[
8042+
"--output-model-type",
8043+
"pydantic_v2.BaseModel",
8044+
"--field-include-all-keys",
8045+
"--use-annotated",
8046+
],
8047+
)
8048+
8049+
80048050
@PYDANTIC_V2_SKIP
80058051
def test_unique_items_enum_set(output_file: Path) -> None:
80068052
"""Test set with enum items does not add __hash__ to enum (already hashable)."""

0 commit comments

Comments
 (0)