_dataclasses.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315
  1. """Private logic for creating pydantic dataclasses."""
  2. from __future__ import annotations as _annotations
  3. import copy
  4. import dataclasses
  5. import sys
  6. import warnings
  7. from collections.abc import Generator
  8. from contextlib import contextmanager
  9. from functools import partial
  10. from typing import TYPE_CHECKING, Any, ClassVar, Protocol, cast
  11. from pydantic_core import (
  12. ArgsKwargs,
  13. SchemaSerializer,
  14. SchemaValidator,
  15. core_schema,
  16. )
  17. from typing_extensions import TypeAlias, TypeIs
  18. from ..errors import PydanticUndefinedAnnotation
  19. from ..fields import FieldInfo
  20. from ..plugin._schema_validator import PluggableSchemaValidator, create_schema_validator
  21. from ..warnings import PydanticDeprecatedSince20
  22. from . import _config, _decorators
  23. from ._fields import collect_dataclass_fields
  24. from ._generate_schema import GenerateSchema, InvalidSchemaError
  25. from ._generics import get_standard_typevars_map
  26. from ._mock_val_ser import set_dataclass_mocks
  27. from ._namespace_utils import NsResolver
  28. from ._signature import generate_pydantic_signature
  29. from ._utils import LazyClassAttribute
  30. if TYPE_CHECKING:
  31. from _typeshed import DataclassInstance as StandardDataclass
  32. from ..config import ConfigDict
  33. class PydanticDataclass(StandardDataclass, Protocol):
  34. """A protocol containing attributes only available once a class has been decorated as a Pydantic dataclass.
  35. Attributes:
  36. __pydantic_config__: Pydantic-specific configuration settings for the dataclass.
  37. __pydantic_complete__: Whether dataclass building is completed, or if there are still undefined fields.
  38. __pydantic_core_schema__: The pydantic-core schema used to build the SchemaValidator and SchemaSerializer.
  39. __pydantic_decorators__: Metadata containing the decorators defined on the dataclass.
  40. __pydantic_fields__: Metadata about the fields defined on the dataclass.
  41. __pydantic_serializer__: The pydantic-core SchemaSerializer used to dump instances of the dataclass.
  42. __pydantic_validator__: The pydantic-core SchemaValidator used to validate instances of the dataclass.
  43. """
  44. __pydantic_config__: ClassVar[ConfigDict]
  45. __pydantic_complete__: ClassVar[bool]
  46. __pydantic_core_schema__: ClassVar[core_schema.CoreSchema]
  47. __pydantic_decorators__: ClassVar[_decorators.DecoratorInfos]
  48. __pydantic_fields__: ClassVar[dict[str, FieldInfo]]
  49. __pydantic_serializer__: ClassVar[SchemaSerializer]
  50. __pydantic_validator__: ClassVar[SchemaValidator | PluggableSchemaValidator]
  51. @classmethod
  52. def __pydantic_fields_complete__(cls) -> bool: ...
  53. def set_dataclass_fields(
  54. cls: type[StandardDataclass],
  55. config_wrapper: _config.ConfigWrapper,
  56. ns_resolver: NsResolver | None = None,
  57. ) -> None:
  58. """Collect and set `cls.__pydantic_fields__`.
  59. Args:
  60. cls: The class.
  61. config_wrapper: The config wrapper instance.
  62. ns_resolver: Namespace resolver to use when getting dataclass annotations.
  63. """
  64. typevars_map = get_standard_typevars_map(cls)
  65. fields = collect_dataclass_fields(
  66. cls, ns_resolver=ns_resolver, typevars_map=typevars_map, config_wrapper=config_wrapper
  67. )
  68. cls.__pydantic_fields__ = fields # type: ignore
  69. def complete_dataclass(
  70. cls: type[Any],
  71. config_wrapper: _config.ConfigWrapper,
  72. *,
  73. raise_errors: bool = True,
  74. ns_resolver: NsResolver | None = None,
  75. _force_build: bool = False,
  76. ) -> bool:
  77. """Finish building a pydantic dataclass.
  78. This logic is called on a class which has already been wrapped in `dataclasses.dataclass()`.
  79. This is somewhat analogous to `pydantic._internal._model_construction.complete_model_class`.
  80. Args:
  81. cls: The class.
  82. config_wrapper: The config wrapper instance.
  83. raise_errors: Whether to raise errors, defaults to `True`.
  84. ns_resolver: The namespace resolver instance to use when collecting dataclass fields
  85. and during schema building.
  86. _force_build: Whether to force building the dataclass, no matter if
  87. [`defer_build`][pydantic.config.ConfigDict.defer_build] is set.
  88. Returns:
  89. `True` if building a pydantic dataclass is successfully completed, `False` otherwise.
  90. Raises:
  91. PydanticUndefinedAnnotation: If `raise_error` is `True` and there is an undefined annotations.
  92. """
  93. original_init = cls.__init__
  94. # dataclass.__init__ must be defined here so its `__qualname__` can be changed since functions can't be copied,
  95. # and so that the mock validator is used if building was deferred:
  96. def __init__(__dataclass_self__: PydanticDataclass, *args: Any, **kwargs: Any) -> None:
  97. __tracebackhide__ = True
  98. s = __dataclass_self__
  99. s.__pydantic_validator__.validate_python(ArgsKwargs(args, kwargs), self_instance=s)
  100. __init__.__qualname__ = f'{cls.__qualname__}.__init__'
  101. cls.__init__ = __init__ # type: ignore
  102. cls.__pydantic_config__ = config_wrapper.config_dict # type: ignore
  103. set_dataclass_fields(cls, config_wrapper=config_wrapper, ns_resolver=ns_resolver)
  104. if not _force_build and config_wrapper.defer_build:
  105. set_dataclass_mocks(cls)
  106. return False
  107. if hasattr(cls, '__post_init_post_parse__'):
  108. warnings.warn(
  109. 'Support for `__post_init_post_parse__` has been dropped, the method will not be called',
  110. PydanticDeprecatedSince20,
  111. )
  112. typevars_map = get_standard_typevars_map(cls)
  113. gen_schema = GenerateSchema(
  114. config_wrapper,
  115. ns_resolver=ns_resolver,
  116. typevars_map=typevars_map,
  117. )
  118. # set __signature__ attr only for the class, but not for its instances
  119. # (because instances can define `__call__`, and `inspect.signature` shouldn't
  120. # use the `__signature__` attribute and instead generate from `__call__`).
  121. cls.__signature__ = LazyClassAttribute(
  122. '__signature__',
  123. partial(
  124. generate_pydantic_signature,
  125. # It's important that we reference the `original_init` here,
  126. # as it is the one synthesized by the stdlib `dataclass` module:
  127. init=original_init,
  128. fields=cls.__pydantic_fields__, # type: ignore
  129. validate_by_name=config_wrapper.validate_by_name,
  130. extra=config_wrapper.extra,
  131. is_dataclass=True,
  132. ),
  133. )
  134. try:
  135. schema = gen_schema.generate_schema(cls)
  136. except PydanticUndefinedAnnotation as e:
  137. if raise_errors:
  138. raise
  139. set_dataclass_mocks(cls, f'`{e.name}`')
  140. return False
  141. core_config = config_wrapper.core_config(title=cls.__name__)
  142. try:
  143. schema = gen_schema.clean_schema(schema)
  144. except InvalidSchemaError:
  145. set_dataclass_mocks(cls)
  146. return False
  147. # We are about to set all the remaining required properties expected for this cast;
  148. # __pydantic_decorators__ and __pydantic_fields__ should already be set
  149. cls = cast('type[PydanticDataclass]', cls)
  150. cls.__pydantic_core_schema__ = schema
  151. cls.__pydantic_validator__ = create_schema_validator(
  152. schema, cls, cls.__module__, cls.__qualname__, 'dataclass', core_config, config_wrapper.plugin_settings
  153. )
  154. cls.__pydantic_serializer__ = SchemaSerializer(schema, core_config)
  155. cls.__pydantic_complete__ = True
  156. return True
  157. def is_stdlib_dataclass(cls: type[Any], /) -> TypeIs[type[StandardDataclass]]:
  158. """Returns `True` if the class is a stdlib dataclass and *not* a Pydantic dataclass.
  159. Unlike the stdlib `dataclasses.is_dataclass()` function, this does *not* include subclasses
  160. of a dataclass that are themselves not dataclasses.
  161. Args:
  162. cls: The class.
  163. Returns:
  164. `True` if the class is a stdlib dataclass, `False` otherwise.
  165. """
  166. return '__dataclass_fields__' in cls.__dict__ and not hasattr(cls, '__pydantic_validator__')
  167. def as_dataclass_field(pydantic_field: FieldInfo) -> dataclasses.Field[Any]:
  168. field_args: dict[str, Any] = {'default': pydantic_field}
  169. # Needed because if `doc` is set, the dataclass slots will be a dict (field name -> doc) instead of a tuple:
  170. if sys.version_info >= (3, 14) and pydantic_field.description is not None:
  171. field_args['doc'] = pydantic_field.description
  172. # Needed as the stdlib dataclass module processes kw_only in a specific way during class construction:
  173. if sys.version_info >= (3, 10) and pydantic_field.kw_only:
  174. field_args['kw_only'] = True
  175. # Needed as the stdlib dataclass modules generates `__repr__()` during class construction:
  176. if pydantic_field.repr is not True:
  177. field_args['repr'] = pydantic_field.repr
  178. return dataclasses.field(**field_args)
  179. DcFields: TypeAlias = dict[str, dataclasses.Field[Any]]
  180. @contextmanager
  181. def patch_base_fields(cls: type[Any]) -> Generator[None]:
  182. """Temporarily patch the stdlib dataclasses bases of `cls` if the Pydantic `Field()` function is used.
  183. When creating a Pydantic dataclass, it is possible to inherit from stdlib dataclasses, where
  184. the Pydantic `Field()` function is used. To create this Pydantic dataclass, we first apply
  185. the stdlib `@dataclass` decorator on it. During the construction of the stdlib dataclass,
  186. the `kw_only` and `repr` field arguments need to be understood by the stdlib *during* the
  187. dataclass construction. To do so, we temporarily patch the fields dictionary of the affected
  188. bases.
  189. For instance, with the following example:
  190. ```python {test="skip" lint="skip"}
  191. import dataclasses as stdlib_dc
  192. import pydantic
  193. import pydantic.dataclasses as pydantic_dc
  194. @stdlib_dc.dataclass
  195. class A:
  196. a: int = pydantic.Field(repr=False)
  197. # Notice that the `repr` attribute of the dataclass field is `True`:
  198. A.__dataclass_fields__['a']
  199. #> dataclass.Field(default=FieldInfo(repr=False), repr=True, ...)
  200. @pydantic_dc.dataclass
  201. class B(A):
  202. b: int = pydantic.Field(repr=False)
  203. ```
  204. When passing `B` to the stdlib `@dataclass` decorator, it will look for fields in the parent classes
  205. and reuse them directly. When this context manager is active, `A` will be temporarily patched to be
  206. equivalent to:
  207. ```python {test="skip" lint="skip"}
  208. @stdlib_dc.dataclass
  209. class A:
  210. a: int = stdlib_dc.field(default=Field(repr=False), repr=False)
  211. ```
  212. !!! note
  213. This is only applied to the bases of `cls`, and not `cls` itself. The reason is that the Pydantic
  214. dataclass decorator "owns" `cls` (in the previous example, `B`). As such, we instead modify the fields
  215. directly (in the previous example, we simply do `setattr(B, 'b', as_dataclass_field(pydantic_field))`).
  216. !!! note
  217. This approach is far from ideal, and can probably be the source of unwanted side effects/race conditions.
  218. The previous implemented approach was mutating the `__annotations__` dict of `cls`, which is no longer a
  219. safe operation in Python 3.14+, and resulted in unexpected behavior with field ordering anyway.
  220. """
  221. # A list of two-tuples, the first element being a reference to the
  222. # dataclass fields dictionary, the second element being a mapping between
  223. # the field names that were modified, and their original `Field`:
  224. original_fields_list: list[tuple[DcFields, DcFields]] = []
  225. for base in cls.__mro__[1:]:
  226. dc_fields: dict[str, dataclasses.Field[Any]] = base.__dict__.get('__dataclass_fields__', {})
  227. dc_fields_with_pydantic_field_defaults = {
  228. field_name: field
  229. for field_name, field in dc_fields.items()
  230. if isinstance(field.default, FieldInfo)
  231. # Only do the patching if one of the affected attributes is set:
  232. and (field.default.description is not None or field.default.kw_only or field.default.repr is not True)
  233. }
  234. if dc_fields_with_pydantic_field_defaults:
  235. original_fields_list.append((dc_fields, dc_fields_with_pydantic_field_defaults))
  236. for field_name, field in dc_fields_with_pydantic_field_defaults.items():
  237. default = cast(FieldInfo, field.default)
  238. # `dataclasses.Field` isn't documented as working with `copy.copy()`.
  239. # It is a class with `__slots__`, so should work (and we hope for the best):
  240. new_dc_field = copy.copy(field)
  241. # For base fields, no need to set `doc` from `FieldInfo.description`, this is only relevant
  242. # for the class under construction and handled in `as_dataclass_field()`.
  243. if sys.version_info >= (3, 10) and default.kw_only:
  244. new_dc_field.kw_only = True
  245. if default.repr is not True:
  246. new_dc_field.repr = default.repr
  247. dc_fields[field_name] = new_dc_field
  248. try:
  249. yield
  250. finally:
  251. for fields, original_fields in original_fields_list:
  252. for field_name, original_field in original_fields.items():
  253. fields[field_name] = original_field