1"""RootModel class and type definitions."""
2
3from __future__ import annotations as _annotations
4
5from copy import copy, deepcopy
6from typing import TYPE_CHECKING, Any, Generic, Literal, TypeVar
7
8from pydantic_core import PydanticUndefined
9from typing_extensions import Self, dataclass_transform
10
11from . import PydanticUserError
12from ._internal import _model_construction, _repr
13from .main import BaseModel, _object_setattr
14
15if TYPE_CHECKING:
16 from .fields import Field as PydanticModelField
17 from .fields import PrivateAttr as PydanticModelPrivateAttr
18
19 # dataclass_transform could be applied to RootModel directly, but `ModelMetaclass`'s dataclass_transform
20 # takes priority (at least with pyright). We trick type checkers into thinking we apply dataclass_transform
21 # on a new metaclass.
22 @dataclass_transform(kw_only_default=False, field_specifiers=(PydanticModelField, PydanticModelPrivateAttr))
23 class _RootModelMetaclass(_model_construction.ModelMetaclass): ...
24else:
25 _RootModelMetaclass = _model_construction.ModelMetaclass
26
27__all__ = ('RootModel',)
28
29RootModelRootType = TypeVar('RootModelRootType')
30
31
32class RootModel(BaseModel, Generic[RootModelRootType], metaclass=_RootModelMetaclass):
33 """!!! abstract "Usage Documentation"
34 [`RootModel` and Custom Root Types](../concepts/models.md#rootmodel-and-custom-root-types)
35
36 A Pydantic `BaseModel` for the root object of the model.
37
38 Attributes:
39 root: The root object of the model.
40 __pydantic_root_model__: Whether the model is a RootModel.
41 __pydantic_private__: Private fields in the model.
42 __pydantic_extra__: Extra fields in the model.
43
44 """
45
46 __pydantic_root_model__ = True
47 __pydantic_private__ = None
48 __pydantic_extra__ = None
49
50 root: RootModelRootType
51
52 def __init_subclass__(cls, **kwargs):
53 extra = cls.model_config.get('extra')
54 if extra is not None:
55 raise PydanticUserError(
56 "`RootModel` does not support setting `model_config['extra']`", code='root-model-extra'
57 )
58 super().__init_subclass__(**kwargs)
59
60 def __init__(self, /, root: RootModelRootType = PydanticUndefined, **data) -> None: # type: ignore
61 __tracebackhide__ = True
62 if data:
63 if root is not PydanticUndefined:
64 raise ValueError(
65 '"RootModel.__init__" accepts either a single positional argument or arbitrary keyword arguments'
66 )
67 root = data # type: ignore
68 self.__pydantic_validator__.validate_python(root, self_instance=self)
69
70 __init__.__pydantic_base_init__ = True # pyright: ignore[reportFunctionMemberAccess]
71
72 @classmethod
73 def model_construct(cls, root: RootModelRootType, _fields_set: set[str] | None = None) -> Self: # type: ignore
74 """Create a new model using the provided root object and update fields set.
75
76 Args:
77 root: The root object of the model.
78 _fields_set: The set of fields to be updated.
79
80 Returns:
81 The new model.
82
83 Raises:
84 NotImplemented: If the model is not a subclass of `RootModel`.
85 """
86 return super().model_construct(root=root, _fields_set=_fields_set)
87
88 def __getstate__(self) -> dict[Any, Any]:
89 return {
90 '__dict__': self.__dict__,
91 '__pydantic_fields_set__': self.__pydantic_fields_set__,
92 }
93
94 def __setstate__(self, state: dict[Any, Any]) -> None:
95 _object_setattr(self, '__pydantic_fields_set__', state['__pydantic_fields_set__'])
96 _object_setattr(self, '__dict__', state['__dict__'])
97
98 def __copy__(self) -> Self:
99 """Returns a shallow copy of the model."""
100 cls = type(self)
101 m = cls.__new__(cls)
102 _object_setattr(m, '__dict__', copy(self.__dict__))
103 _object_setattr(m, '__pydantic_fields_set__', copy(self.__pydantic_fields_set__))
104 return m
105
106 def __deepcopy__(self, memo: dict[int, Any] | None = None) -> Self:
107 """Returns a deep copy of the model."""
108 cls = type(self)
109 m = cls.__new__(cls)
110 _object_setattr(m, '__dict__', deepcopy(self.__dict__, memo=memo))
111 # This next line doesn't need a deepcopy because __pydantic_fields_set__ is a set[str],
112 # and attempting a deepcopy would be marginally slower.
113 _object_setattr(m, '__pydantic_fields_set__', copy(self.__pydantic_fields_set__))
114 return m
115
116 if TYPE_CHECKING:
117
118 def model_dump( # type: ignore
119 self,
120 *,
121 mode: Literal['json', 'python'] | str = 'python',
122 include: Any = None,
123 exclude: Any = None,
124 context: dict[str, Any] | None = None,
125 by_alias: bool | None = None,
126 exclude_unset: bool = False,
127 exclude_defaults: bool = False,
128 exclude_none: bool = False,
129 exclude_computed_fields: bool = False,
130 round_trip: bool = False,
131 warnings: bool | Literal['none', 'warn', 'error'] = True,
132 serialize_as_any: bool = False,
133 ) -> Any:
134 """This method is included just to get a more accurate return type for type checkers.
135 It is included in this `if TYPE_CHECKING:` block since no override is actually necessary.
136
137 See the documentation of `BaseModel.model_dump` for more details about the arguments.
138
139 Generally, this method will have a return type of `RootModelRootType`, assuming that `RootModelRootType` is
140 not a `BaseModel` subclass. If `RootModelRootType` is a `BaseModel` subclass, then the return
141 type will likely be `dict[str, Any]`, as `model_dump` calls are recursive. The return type could
142 even be something different, in the case of a custom serializer.
143 Thus, `Any` is used here to catch all of these cases.
144 """
145 ...
146
147 def __eq__(self, other: Any) -> bool:
148 if not isinstance(other, RootModel):
149 return NotImplemented
150 return self.__pydantic_fields__['root'].annotation == other.__pydantic_fields__[
151 'root'
152 ].annotation and super().__eq__(other)
153
154 def __repr_args__(self) -> _repr.ReprArgs:
155 yield 'root', self.root