_dataclasses.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281
  1. """Private logic for creating pydantic dataclasses."""
  2. from __future__ import annotations as _annotations
  3. import dataclasses
  4. import inspect
  5. import typing
  6. import warnings
  7. from functools import partial, wraps
  8. from inspect import Parameter, Signature
  9. from typing import Any, Callable, ClassVar
  10. from pydantic_core import (
  11. ArgsKwargs,
  12. PydanticUndefined,
  13. SchemaSerializer,
  14. SchemaValidator,
  15. core_schema,
  16. )
  17. from typing_extensions import TypeGuard
  18. from ..errors import PydanticUndefinedAnnotation
  19. from ..fields import FieldInfo
  20. from ..plugin._schema_validator import create_schema_validator
  21. from ..warnings import PydanticDeprecatedSince20
  22. from . import _config, _decorators, _typing_extra
  23. from ._config import ConfigWrapper
  24. from ._fields import collect_dataclass_fields
  25. from ._generate_schema import GenerateSchema, generate_pydantic_signature
  26. from ._generics import get_standard_typevars_map
  27. from ._mock_val_ser import set_dataclass_mocks
  28. from ._schema_generation_shared import CallbackGetCoreSchemaHandler
  29. from ._utils import is_valid_identifier
  30. if typing.TYPE_CHECKING:
  31. from ..config import ConfigDict
  32. class StandardDataclass(typing.Protocol):
  33. __dataclass_fields__: ClassVar[dict[str, Any]]
  34. __dataclass_params__: ClassVar[Any] # in reality `dataclasses._DataclassParams`
  35. __post_init__: ClassVar[Callable[..., None]]
  36. def __init__(self, *args: object, **kwargs: object) -> None:
  37. pass
  38. class PydanticDataclass(StandardDataclass, typing.Protocol):
  39. """A protocol containing attributes only available once a class has been decorated as a Pydantic dataclass.
  40. Attributes:
  41. __pydantic_config__: Pydantic-specific configuration settings for the dataclass.
  42. __pydantic_complete__: Whether dataclass building is completed, or if there are still undefined fields.
  43. __pydantic_core_schema__: The pydantic-core schema used to build the SchemaValidator and SchemaSerializer.
  44. __pydantic_decorators__: Metadata containing the decorators defined on the dataclass.
  45. __pydantic_fields__: Metadata about the fields defined on the dataclass.
  46. __pydantic_serializer__: The pydantic-core SchemaSerializer used to dump instances of the dataclass.
  47. __pydantic_validator__: The pydantic-core SchemaValidator used to validate instances of the dataclass.
  48. """
  49. __pydantic_config__: ClassVar[ConfigDict]
  50. __pydantic_complete__: ClassVar[bool]
  51. __pydantic_core_schema__: ClassVar[core_schema.CoreSchema]
  52. __pydantic_decorators__: ClassVar[_decorators.DecoratorInfos]
  53. __pydantic_fields__: ClassVar[dict[str, FieldInfo]]
  54. __pydantic_serializer__: ClassVar[SchemaSerializer]
  55. __pydantic_validator__: ClassVar[SchemaValidator]
  56. else:
  57. # See PyCharm issues https://youtrack.jetbrains.com/issue/PY-21915
  58. # and https://youtrack.jetbrains.com/issue/PY-51428
  59. DeprecationWarning = PydanticDeprecatedSince20
  60. def set_dataclass_fields(cls: type[StandardDataclass], types_namespace: dict[str, Any] | None = None) -> None:
  61. """Collect and set `cls.__pydantic_fields__`.
  62. Args:
  63. cls: The class.
  64. types_namespace: The types namespace, defaults to `None`.
  65. """
  66. typevars_map = get_standard_typevars_map(cls)
  67. fields = collect_dataclass_fields(cls, types_namespace, typevars_map=typevars_map)
  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. types_namespace: dict[str, Any] | None,
  75. ) -> bool:
  76. """Finish building a pydantic dataclass.
  77. This logic is called on a class which has already been wrapped in `dataclasses.dataclass()`.
  78. This is somewhat analogous to `pydantic._internal._model_construction.complete_model_class`.
  79. Args:
  80. cls: The class.
  81. config_wrapper: The config wrapper instance.
  82. raise_errors: Whether to raise errors, defaults to `True`.
  83. types_namespace: The types namespace.
  84. Returns:
  85. `True` if building a pydantic dataclass is successfully completed, `False` otherwise.
  86. Raises:
  87. PydanticUndefinedAnnotation: If `raise_error` is `True` and there is an undefined annotations.
  88. """
  89. if hasattr(cls, '__post_init_post_parse__'):
  90. warnings.warn(
  91. 'Support for `__post_init_post_parse__` has been dropped, the method will not be called', DeprecationWarning
  92. )
  93. if types_namespace is None:
  94. types_namespace = _typing_extra.get_cls_types_namespace(cls)
  95. set_dataclass_fields(cls, types_namespace)
  96. typevars_map = get_standard_typevars_map(cls)
  97. gen_schema = GenerateSchema(
  98. config_wrapper,
  99. types_namespace,
  100. typevars_map,
  101. )
  102. # This needs to be called before we change the __init__
  103. sig = generate_dataclass_signature(cls, cls.__pydantic_fields__, config_wrapper) # type: ignore
  104. # dataclass.__init__ must be defined here so its `__qualname__` can be changed since functions can't be copied.
  105. def __init__(__dataclass_self__: PydanticDataclass, *args: Any, **kwargs: Any) -> None:
  106. __tracebackhide__ = True
  107. s = __dataclass_self__
  108. s.__pydantic_validator__.validate_python(ArgsKwargs(args, kwargs), self_instance=s)
  109. __init__.__qualname__ = f'{cls.__qualname__}.__init__'
  110. cls.__init__ = __init__ # type: ignore
  111. cls.__pydantic_config__ = config_wrapper.config_dict # type: ignore
  112. cls.__signature__ = sig # type: ignore
  113. get_core_schema = getattr(cls, '__get_pydantic_core_schema__', None)
  114. try:
  115. if get_core_schema:
  116. schema = get_core_schema(
  117. cls,
  118. CallbackGetCoreSchemaHandler(
  119. partial(gen_schema.generate_schema, from_dunder_get_core_schema=False),
  120. gen_schema,
  121. ref_mode='unpack',
  122. ),
  123. )
  124. else:
  125. schema = gen_schema.generate_schema(cls, from_dunder_get_core_schema=False)
  126. except PydanticUndefinedAnnotation as e:
  127. if raise_errors:
  128. raise
  129. set_dataclass_mocks(cls, cls.__name__, f'`{e.name}`')
  130. return False
  131. core_config = config_wrapper.core_config(cls)
  132. try:
  133. schema = gen_schema.clean_schema(schema)
  134. except gen_schema.CollectedInvalid:
  135. set_dataclass_mocks(cls, cls.__name__, 'all referenced types')
  136. return False
  137. # We are about to set all the remaining required properties expected for this cast;
  138. # __pydantic_decorators__ and __pydantic_fields__ should already be set
  139. cls = typing.cast('type[PydanticDataclass]', cls)
  140. # debug(schema)
  141. cls.__pydantic_core_schema__ = schema
  142. cls.__pydantic_validator__ = validator = create_schema_validator(
  143. schema, cls, cls.__module__, cls.__qualname__, 'dataclass', core_config, config_wrapper.plugin_settings
  144. )
  145. cls.__pydantic_serializer__ = SchemaSerializer(schema, core_config)
  146. if config_wrapper.validate_assignment:
  147. @wraps(cls.__setattr__)
  148. def validated_setattr(instance: Any, __field: str, __value: str) -> None:
  149. validator.validate_assignment(instance, __field, __value)
  150. cls.__setattr__ = validated_setattr.__get__(None, cls) # type: ignore
  151. return True
  152. def process_param_defaults(param: Parameter) -> Parameter:
  153. """Custom processing where the parameter default is of type FieldInfo
  154. Args:
  155. param (Parameter): The parameter
  156. Returns:
  157. Parameter: The custom processed parameter
  158. """
  159. param_default = param.default
  160. if isinstance(param_default, FieldInfo):
  161. annotation = param.annotation
  162. # Replace the annotation if appropriate
  163. # inspect does "clever" things to show annotations as strings because we have
  164. # `from __future__ import annotations` in main, we don't want that
  165. if annotation == 'Any':
  166. annotation = Any
  167. # Replace the field name with the alias if present
  168. name = param.name
  169. alias = param_default.alias
  170. validation_alias = param_default.validation_alias
  171. if validation_alias is None and isinstance(alias, str) and is_valid_identifier(alias):
  172. name = alias
  173. elif isinstance(validation_alias, str) and is_valid_identifier(validation_alias):
  174. name = validation_alias
  175. # Replace the field default
  176. default = param_default.default
  177. if default is PydanticUndefined:
  178. if param_default.default_factory is PydanticUndefined:
  179. default = inspect.Signature.empty
  180. else:
  181. # this is used by dataclasses to indicate a factory exists:
  182. default = dataclasses._HAS_DEFAULT_FACTORY # type: ignore
  183. return param.replace(annotation=annotation, name=name, default=default)
  184. return param
  185. def generate_dataclass_signature(
  186. cls: type[StandardDataclass], fields: dict[str, FieldInfo], config_wrapper: ConfigWrapper
  187. ) -> Signature:
  188. """Generate signature for a pydantic dataclass.
  189. Args:
  190. cls: The dataclass.
  191. fields: The model fields.
  192. config_wrapper: The config wrapper instance.
  193. Returns:
  194. The dataclass signature.
  195. """
  196. return generate_pydantic_signature(
  197. init=cls.__init__, fields=fields, config_wrapper=config_wrapper, post_process_parameter=process_param_defaults
  198. )
  199. def is_builtin_dataclass(_cls: type[Any]) -> TypeGuard[type[StandardDataclass]]:
  200. """Returns True if a class is a stdlib dataclass and *not* a pydantic dataclass.
  201. We check that
  202. - `_cls` is a dataclass
  203. - `_cls` does not inherit from a processed pydantic dataclass (and thus have a `__pydantic_validator__`)
  204. - `_cls` does not have any annotations that are not dataclass fields
  205. e.g.
  206. ```py
  207. import dataclasses
  208. import pydantic.dataclasses
  209. @dataclasses.dataclass
  210. class A:
  211. x: int
  212. @pydantic.dataclasses.dataclass
  213. class B(A):
  214. y: int
  215. ```
  216. In this case, when we first check `B`, we make an extra check and look at the annotations ('y'),
  217. which won't be a superset of all the dataclass fields (only the stdlib fields i.e. 'x')
  218. Args:
  219. cls: The class.
  220. Returns:
  221. `True` if the class is a stdlib dataclass, `False` otherwise.
  222. """
  223. return (
  224. dataclasses.is_dataclass(_cls)
  225. and not hasattr(_cls, '__pydantic_validator__')
  226. and set(_cls.__dataclass_fields__).issuperset(set(getattr(_cls, '__annotations__', {})))
  227. )