Skip to content

Commit f1ed5b2

Browse files
authored
Refactoring Redis OM Python to use pydantic 2.0 types and validators (#603)
* refactoring model to use pydantic 2.0 types and validators * adding tests for #591 * adding tests with uuid * readme fixes * fixing typo in NOT_IN
1 parent 78e9a39 commit f1ed5b2

9 files changed

+468
-97
lines changed

README.md

+9-9
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ Check out this example of modeling customer data with Redis OM. First, we create
9393
import datetime
9494
from typing import Optional
9595

96-
from pydantic.v1 import EmailStr
96+
from pydantic import EmailStr
9797

9898
from redis_om import HashModel
9999

@@ -104,7 +104,7 @@ class Customer(HashModel):
104104
email: EmailStr
105105
join_date: datetime.date
106106
age: int
107-
bio: Optional[str]
107+
bio: Optional[str] = None
108108
```
109109

110110
Now that we have a `Customer` model, let's use it to save customer data to Redis.
@@ -113,7 +113,7 @@ Now that we have a `Customer` model, let's use it to save customer data to Redis
113113
import datetime
114114
from typing import Optional
115115

116-
from pydantic.v1 import EmailStr
116+
from pydantic import EmailStr
117117

118118
from redis_om import HashModel
119119

@@ -124,7 +124,7 @@ class Customer(HashModel):
124124
email: EmailStr
125125
join_date: datetime.date
126126
age: int
127-
bio: Optional[str]
127+
bio: Optional[str] = None
128128

129129

130130
# First, we create a new `Customer` object:
@@ -168,7 +168,7 @@ For example, because we used the `EmailStr` type for the `email` field, we'll ge
168168
import datetime
169169
from typing import Optional
170170

171-
from pydantic.v1 import EmailStr, ValidationError
171+
from pydantic import EmailStr, ValidationError
172172

173173
from redis_om import HashModel
174174

@@ -179,7 +179,7 @@ class Customer(HashModel):
179179
email: EmailStr
180180
join_date: datetime.date
181181
age: int
182-
bio: Optional[str]
182+
bio: Optional[str] = None
183183

184184

185185
try:
@@ -222,7 +222,7 @@ To show how this works, we'll make a small change to the `Customer` model we def
222222
import datetime
223223
from typing import Optional
224224

225-
from pydantic.v1 import EmailStr
225+
from pydantic import EmailStr
226226

227227
from redis_om import (
228228
Field,
@@ -237,7 +237,7 @@ class Customer(HashModel):
237237
email: EmailStr
238238
join_date: datetime.date
239239
age: int = Field(index=True)
240-
bio: Optional[str]
240+
bio: Optional[str] = None
241241

242242

243243
# Now, if we use this model with a Redis deployment that has the
@@ -287,7 +287,7 @@ from redis_om import (
287287

288288
class Address(EmbeddedJsonModel):
289289
address_line_1: str
290-
address_line_2: Optional[str]
290+
address_line_2: Optional[str] = None
291291
city: str = Field(index=True)
292292
state: str = Field(index=True)
293293
country: str

aredis_om/_compat.py

+85-5
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,99 @@
1+
from dataclasses import dataclass, is_dataclass
2+
from typing import (
3+
Any,
4+
Callable,
5+
Deque,
6+
Dict,
7+
FrozenSet,
8+
List,
9+
Mapping,
10+
Sequence,
11+
Set,
12+
Tuple,
13+
Type,
14+
Union,
15+
)
16+
117
from pydantic.version import VERSION as PYDANTIC_VERSION
18+
from typing_extensions import Annotated, Literal, get_args, get_origin
219

320

421
PYDANTIC_V2 = PYDANTIC_VERSION.startswith("2.")
522

623
if PYDANTIC_V2:
7-
from pydantic.v1 import BaseModel, validator
8-
from pydantic.v1.fields import FieldInfo, ModelField, Undefined, UndefinedType
9-
from pydantic.v1.json import ENCODERS_BY_TYPE
10-
from pydantic.v1.main import ModelMetaclass, validate_model
24+
25+
def use_pydantic_2_plus():
26+
return True
27+
28+
from pydantic import BaseModel, TypeAdapter
29+
from pydantic import ValidationError as ValidationError
30+
from pydantic import validator
31+
from pydantic._internal._model_construction import ModelMetaclass
32+
from pydantic._internal._repr import Representation
33+
from pydantic.deprecated.json import ENCODERS_BY_TYPE
34+
from pydantic.fields import FieldInfo
35+
from pydantic.v1.main import validate_model
1136
from pydantic.v1.typing import NoArgAnyCallable
12-
from pydantic.v1.utils import Representation
37+
from pydantic_core import PydanticUndefined as Undefined
38+
from pydantic_core import PydanticUndefinedType as UndefinedType
39+
40+
@dataclass
41+
class ModelField:
42+
field_info: FieldInfo
43+
name: str
44+
mode: Literal["validation", "serialization"] = "validation"
45+
46+
@property
47+
def alias(self) -> str:
48+
a = self.field_info.alias
49+
return a if a is not None else self.name
50+
51+
@property
52+
def required(self) -> bool:
53+
return self.field_info.is_required()
54+
55+
@property
56+
def default(self) -> Any:
57+
return self.get_default()
58+
59+
@property
60+
def type_(self) -> Any:
61+
return self.field_info.annotation
62+
63+
def __post_init__(self) -> None:
64+
self._type_adapter: TypeAdapter[Any] = TypeAdapter(
65+
Annotated[self.field_info.annotation, self.field_info]
66+
)
67+
68+
def get_default(self) -> Any:
69+
if self.field_info.is_required():
70+
return Undefined
71+
return self.field_info.get_default(call_default_factory=True)
72+
73+
def validate(
74+
self,
75+
value: Any,
76+
values: Dict[str, Any] = {}, # noqa: B006
77+
*,
78+
loc: Tuple[Union[int, str], ...] = (),
79+
) -> Tuple[Any, Union[List[Dict[str, Any]], None]]:
80+
return (
81+
self._type_adapter.validate_python(value, from_attributes=True),
82+
None,
83+
)
84+
85+
def __hash__(self) -> int:
86+
# Each ModelField is unique for our purposes, to allow making a dict from
87+
# ModelField to its JSON Schema.
88+
return id(self)
89+
1390
else:
1491
from pydantic import BaseModel, validator
1592
from pydantic.fields import FieldInfo, ModelField, Undefined, UndefinedType
1693
from pydantic.json import ENCODERS_BY_TYPE
1794
from pydantic.main import ModelMetaclass, validate_model
1895
from pydantic.typing import NoArgAnyCallable
1996
from pydantic.utils import Representation
97+
98+
def use_pydantic_2_plus():
99+
return False

aredis_om/model/encoders.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ def jsonable_encoder(
6868
if exclude is not None and not isinstance(exclude, (set, dict)):
6969
exclude = set(exclude)
7070

71-
if isinstance(obj, BaseModel):
71+
if isinstance(obj, BaseModel) and hasattr(obj, "__config__"):
7272
encoder = getattr(obj.__config__, "json_encoders", {})
7373
if custom_encoder:
7474
encoder.update(custom_encoder)

0 commit comments

Comments
 (0)