Skip to content
8 changes: 1 addition & 7 deletions src/datamodel_code_generator/model/msgspec.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,6 @@
from math import isfinite
from typing import TYPE_CHECKING, Any, ClassVar, Optional, TypeVar

from pydantic import Field

from datamodel_code_generator.imports import IMPORT_UNION, Import
from datamodel_code_generator.model import DataModel, DataModelFieldBase, _rebuild_model_with_datamodel_namespace
from datamodel_code_generator.model.base import UNDEFINED, BaseClassDataType, _nested_model_default_factory
Expand All @@ -23,7 +21,7 @@
IMPORT_MSGSPEC_UNSETTYPE,
)
from datamodel_code_generator.model.pydantic_base import (
Constraints as _Constraints,
PatternConstraints as _Constraints,
)
from datamodel_code_generator.model.type_alias import TypeAliasBase
from datamodel_code_generator.model.types import DataTypeManager as _DataTypeManager
Expand Down Expand Up @@ -206,10 +204,6 @@ def create_base_class_model(
class Constraints(_Constraints):
"""Constraint model for msgspec fields."""

# To override existing pattern alias
regex: Optional[str] = Field(None, alias="regex") # noqa: UP045
pattern: Optional[str] = Field(None, alias="pattern") # noqa: UP045


@lru_cache
def get_neither_required_nor_nullable_type(type_: str, use_union_operator: bool) -> str: # noqa: FBT001
Expand Down
5 changes: 5 additions & 0 deletions src/datamodel_code_generator/model/pydantic_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,11 @@ class Constraints(ConstraintsBase):
regex: Optional[str] = Field(None, alias="pattern") # noqa: UP045


class PatternConstraints(Constraints): # noqa: D101
regex: Optional[str] = Field(None, alias="regex") # noqa: UP045
pattern: Optional[str] = Field(None, alias="pattern") # noqa: UP045


class DataModelField(DataModelFieldBase):
"""Field implementation for Pydantic models."""

Expand Down
107 changes: 107 additions & 0 deletions src/datamodel_code_generator/model/pydantic_v2/_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
"""Shared Pydantic v2 ConfigDict generation helpers."""

from __future__ import annotations

from typing import TYPE_CHECKING, Any, Literal, NamedTuple

from datamodel_code_generator.enums import TargetPydanticVersion

if TYPE_CHECKING:
from collections.abc import Iterable

from datamodel_code_generator.types import DataType


class ConfigAttribute(NamedTuple):
"""Configuration attribute mapping for ConfigDict conversion."""

from_: str
to: str
invert: bool


ConfigExtra = Literal["'allow'", "'forbid'", "'ignore'"]
ConfigParameterValue = bool | ConfigExtra | Literal['"python-re"'] | dict[str, Any]
_CONFIG_EXTRA_KEYS: frozenset[str] = frozenset({
"additionalProperties",
"allow_extra_fields",
"extra_fields",
"unevaluatedProperties",
})


def get_config_extra(extra_template_data: dict[str, Any]) -> ConfigExtra | None:
"""Get extra field configuration for ConfigDict."""
additional_properties = extra_template_data.get("additionalProperties")
unevaluated_properties = extra_template_data.get("unevaluatedProperties")
allow_extra_fields = extra_template_data.get("allow_extra_fields")
extra_fields = extra_template_data.get("extra_fields")

config_extra: ConfigExtra | None = None
if allow_extra_fields or extra_fields == "allow":
config_extra = "'allow'"
elif extra_fields == "forbid":
config_extra = "'forbid'"
elif extra_fields == "ignore":
config_extra = "'ignore'"
elif additional_properties is True:
config_extra = "'allow'"
elif additional_properties is False:
config_extra = "'forbid'"
elif unevaluated_properties is True:
config_extra = "'allow'"
elif unevaluated_properties is False:
config_extra = "'forbid'"
return config_extra


def get_config_attributes(
extra_template_data: dict[str, Any],
*,
config_attributes_v2: list[ConfigAttribute],
config_attributes_v2_11: list[ConfigAttribute],
) -> list[ConfigAttribute]:
"""Get config attributes based on target Pydantic version."""
target_version = extra_template_data.get("target_pydantic_version")
if target_version == TargetPydanticVersion.V2_11:
return config_attributes_v2_11
return config_attributes_v2


def build_base_config_parameters(
*,
extra_template_data: dict[str, Any],
all_data_types: Iterable[DataType],
config_attributes_v2: list[ConfigAttribute],
config_attributes_v2_11: list[ConfigAttribute],
include_extra: bool = True,
) -> dict[str, ConfigParameterValue]:
"""Build shared ConfigDict parameters for Pydantic v2 models."""
config_attributes = get_config_attributes(
extra_template_data,
config_attributes_v2=config_attributes_v2,
config_attributes_v2_11=config_attributes_v2_11,
)
if (
all_data_types == ()
and not (include_extra and not _CONFIG_EXTRA_KEYS.isdisjoint(extra_template_data))
and not any(from_ in extra_template_data for from_, _, _ in config_attributes)
):
return {}

config_parameters: dict[str, ConfigParameterValue] = {}

if include_extra and (extra := get_config_extra(extra_template_data)):
config_parameters["extra"] = extra

for from_, to, invert in config_attributes:
if from_ in extra_template_data:
value: bool = extra_template_data[from_]
config_parameters[to] = not value if invert else value

for data_type in all_data_types:
if data_type.is_custom_type: # pragma: no cover
config_parameters["arbitrary_types_allowed"] = True
break

return config_parameters
91 changes: 20 additions & 71 deletions src/datamodel_code_generator/model/pydantic_v2/base_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,11 @@

import re
from collections import defaultdict
from typing import TYPE_CHECKING, Any, ClassVar, Literal, NamedTuple, Optional
from typing import TYPE_CHECKING, Any, ClassVar, Optional, cast

from pydantic import Field, ValidationError, field_validator, model_validator

from datamodel_code_generator import Error
from datamodel_code_generator.enums import TargetPydanticVersion
from datamodel_code_generator.imports import IMPORT_ANY, IMPORT_DICT, Import
from datamodel_code_generator.model import _rebuild_model_with_datamodel_namespace
from datamodel_code_generator.model.base import (
Expand All @@ -23,15 +22,15 @@
DataModelFieldBase,
)
from datamodel_code_generator.model.imports import IMPORT_CLASSVAR
from datamodel_code_generator.model.pydantic_base import (
BaseModelBase,
)
from datamodel_code_generator.model.pydantic_base import (
Constraints as _Constraints,
)
from datamodel_code_generator.model.pydantic_base import BaseModelBase
from datamodel_code_generator.model.pydantic_base import Constraints as _Constraints
from datamodel_code_generator.model.pydantic_base import (
DataModelField as _PydanticBaseDataModelField,
)
from datamodel_code_generator.model.pydantic_v2._config import (
ConfigAttribute,
build_base_config_parameters,
)
from datamodel_code_generator.model.pydantic_v2.imports import (
IMPORT_ALIAS_CHOICES,
IMPORT_BASE_MODEL,
Expand Down Expand Up @@ -273,14 +272,6 @@ def imports(self) -> tuple[Import, ...]:
return base_imports


class ConfigAttribute(NamedTuple):
"""Configuration attribute mapping for ConfigDict conversion."""

from_: str
to: str
invert: bool


class BaseModel(BaseModelBase):
"""Pydantic v2 BaseModel with ConfigDict and pattern-based regex_engine support."""

Expand All @@ -290,6 +281,8 @@ class BaseModel(BaseModelBase):
BASE_CLASS_ALIAS: ClassVar[str] = "_BaseModel"
SUPPORTS_DISCRIMINATOR: ClassVar[bool] = True
SUPPORTS_FIELD_RENAMING: ClassVar[bool] = True
SUPPORTS_CONFIG_EXTRA: ClassVar[bool] = True
SUPPORTS_ARBITRARY_TYPES_ALLOWED: ClassVar[bool] = True
TYPED_EXTRA_FIELD_NAME: ClassVar[str] = "__pydantic_extra__"
# In Pydantic 2.11+, populate_by_name is deprecated in favor of validate_by_name + validate_by_alias
# Default to V2 compatible (populate_by_name) unless target_pydantic_version is specified
Expand Down Expand Up @@ -356,35 +349,25 @@ def __init__( # noqa: PLR0913
keyword_only=keyword_only,
treat_dot_as_module=treat_dot_as_module,
)
config_parameters: dict[str, Any] = {}

extra = self._get_config_extra()
if extra:
config_parameters["extra"] = extra

# Select CONFIG_ATTRIBUTES based on target_pydantic_version
config_attributes = self._get_config_attributes()
for from_, to, invert in config_attributes:
if from_ in self.extra_template_data:
config_parameters[to] = (
not self.extra_template_data[from_] if invert else self.extra_template_data[from_]
)
for data_type in self.all_data_types:
if data_type.is_custom_type: # pragma: no cover
config_parameters["arbitrary_types_allowed"] = True
break
config_parameters = build_base_config_parameters(
extra_template_data=self.extra_template_data,
all_data_types=self.all_data_types if self.SUPPORTS_ARBITRARY_TYPES_ALLOWED else (),
config_attributes_v2=self._CONFIG_ATTRIBUTES_V2,
config_attributes_v2_11=self._CONFIG_ATTRIBUTES_V2_11,
include_extra=self.SUPPORTS_CONFIG_EXTRA,
)

if self._has_lookaround_pattern():
config_parameters["regex_engine"] = '"python-re"'

if isinstance(self.extra_template_data.get("config"), dict):
for key, value in self.extra_template_data["config"].items():
config_parameters[key] = value # noqa: PERF403
if isinstance(config := self.extra_template_data.get("config"), dict):
for key, value in config.items():
config_parameters[key] = value

# Handle json_schema_extra from schema extensions (x-* fields)
model_extras = self.extra_template_data.get("model_extras")
if model_extras:
existing = config_parameters.get("json_schema_extra") or {}
existing = cast("dict[str, Any]", config_parameters.get("json_schema_extra") or {})
config_parameters["json_schema_extra"] = {**existing, **model_extras}

if config_parameters:
Expand All @@ -395,40 +378,6 @@ def __init__( # noqa: PLR0913

self._process_validators()

def _get_config_extra(self) -> Literal["'allow'", "'forbid'", "'ignore'"] | None:
additional_properties = self.extra_template_data.get("additionalProperties")
unevaluated_properties = self.extra_template_data.get("unevaluatedProperties")
allow_extra_fields = self.extra_template_data.get("allow_extra_fields")
extra_fields = self.extra_template_data.get("extra_fields")

config_extra = None
if allow_extra_fields or extra_fields == "allow":
config_extra = "'allow'"
elif extra_fields == "forbid":
config_extra = "'forbid'"
elif extra_fields == "ignore":
config_extra = "'ignore'"
elif additional_properties is True:
config_extra = "'allow'"
elif additional_properties is False:
config_extra = "'forbid'"
elif unevaluated_properties is True:
config_extra = "'allow'"
elif unevaluated_properties is False:
config_extra = "'forbid'"
return config_extra

def _get_config_attributes(self) -> list[ConfigAttribute]:
"""Get config attributes based on target Pydantic version.

If target_pydantic_version is V2_11, use validate_by_name.
Otherwise (V2 or not specified), use populate_by_name for compatibility.
"""
target_version = self.extra_template_data.get("target_pydantic_version")
if target_version == TargetPydanticVersion.V2_11:
return self._CONFIG_ATTRIBUTES_V2_11
return self._CONFIG_ATTRIBUTES_V2

def _has_lookaround_pattern(self) -> bool:
"""Check if any field has a regex pattern with lookaround assertions."""
lookaround_regex = re.compile(r"\(\?<?[=!]")
Expand Down
58 changes: 8 additions & 50 deletions src/datamodel_code_generator/model/pydantic_v2/dataclass.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@

from typing import TYPE_CHECKING, Any, ClassVar

from datamodel_code_generator.enums import TargetPydanticVersion
from datamodel_code_generator.model import DataModel, DataModelFieldBase, _rebuild_model_with_datamodel_namespace
from datamodel_code_generator.model.base import UNDEFINED
from datamodel_code_generator.model.dataclass import _DataclassReuseMixin, has_field_assignment
from datamodel_code_generator.model.pydantic_v2.base_model import (
from datamodel_code_generator.model.pydantic_v2._config import (
ConfigAttribute,
build_base_config_parameters,
)
from datamodel_code_generator.model.pydantic_v2.base_model import (
Constraints as _Constraints,
Expand Down Expand Up @@ -106,59 +106,17 @@ def __init__( # noqa: PLR0913
self.dataclass_arguments["kw_only"] = True
self._set_deprecated_decorator()

config_parameters: dict[str, Any] = {}

extra = self._get_config_extra()
if extra:
config_parameters["extra"] = extra

config_attributes = self._get_config_attributes()
for from_, to, invert in config_attributes:
if from_ in self.extra_template_data:
config_parameters[to] = (
not self.extra_template_data[from_] if invert else self.extra_template_data[from_]
)

for data_type in self.all_data_types:
if data_type.is_custom_type: # pragma: no cover
config_parameters["arbitrary_types_allowed"] = True
break
config_parameters = build_base_config_parameters(
extra_template_data=self.extra_template_data,
all_data_types=self.all_data_types,
config_attributes_v2=self._CONFIG_ATTRIBUTES_V2,
config_attributes_v2_11=self._CONFIG_ATTRIBUTES_V2_11,
)

if config_parameters:
self._additional_imports.append(IMPORT_CONFIG_DICT)
self.extra_template_data["config"] = config_parameters

def _get_config_attributes(self) -> list[ConfigAttribute]:
"""Get config attributes based on target Pydantic version."""
target_version = self.extra_template_data.get("target_pydantic_version")
if target_version == TargetPydanticVersion.V2_11:
return self._CONFIG_ATTRIBUTES_V2_11
return self._CONFIG_ATTRIBUTES_V2

def _get_config_extra(self) -> str | None:
"""Get extra field configuration for ConfigDict."""
additional_properties = self.extra_template_data.get("additionalProperties")
unevaluated_properties = self.extra_template_data.get("unevaluatedProperties")
allow_extra_fields = self.extra_template_data.get("allow_extra_fields")
extra_fields = self.extra_template_data.get("extra_fields")

config_extra = None
if allow_extra_fields or extra_fields == "allow":
config_extra = "'allow'"
elif extra_fields == "forbid":
config_extra = "'forbid'"
elif extra_fields == "ignore":
config_extra = "'ignore'"
elif additional_properties is True:
config_extra = "'allow'"
elif additional_properties is False:
config_extra = "'forbid'"
elif unevaluated_properties is True:
config_extra = "'allow'"
elif unevaluated_properties is False:
config_extra = "'forbid'"
return config_extra


if PYDANTIC_V2_DATACLASS_ALIAS_NEEDS_FALLBACK:
import keyword
Expand Down
Loading
Loading