Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactoring Redis OM Python to use pydantic 2.0 types and validators #603

Merged
merged 11 commits into from
May 2, 2024
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 9 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ Check out this example of modeling customer data with Redis OM. First, we create
import datetime
from typing import Optional

from pydantic.v1 import EmailStr
from pydantic import EmailStr

from redis_om import HashModel

Expand All @@ -104,7 +104,7 @@ class Customer(HashModel):
email: EmailStr
join_date: datetime.date
age: int
bio: Optional[str]
bio: Optional[str] = None
```

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

from pydantic.v1 import EmailStr
from pydantic import EmailStr

from redis_om import HashModel

Expand All @@ -124,7 +124,7 @@ class Customer(HashModel):
email: EmailStr
join_date: datetime.date
age: int
bio: Optional[str]
bio: Optional[str] = None


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

from pydantic.v1 import EmailStr, ValidationError
from pydantic import EmailStr, ValidationError

from redis_om import HashModel

Expand All @@ -179,7 +179,7 @@ class Customer(HashModel):
email: EmailStr
join_date: datetime.date
age: int
bio: Optional[str]
bio: Optional[str] = None


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

from pydantic.v1 import EmailStr
from pydantic import EmailStr

from redis_om import (
Field,
Expand All @@ -237,7 +237,7 @@ class Customer(HashModel):
email: EmailStr
join_date: datetime.date
age: int = Field(index=True)
bio: Optional[str]
bio: Optional[str] = None


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

class Address(EmbeddedJsonModel):
address_line_1: str
address_line_2: Optional[str]
address_line_2: Optional[str] = None
city: str = Field(index=True)
state: str = Field(index=True)
country: str
Expand Down
90 changes: 85 additions & 5 deletions aredis_om/_compat.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,99 @@
from dataclasses import dataclass, is_dataclass
from typing import (
Any,
Callable,
Deque,
Dict,
FrozenSet,
List,
Mapping,
Sequence,
Set,
Tuple,
Type,
Union,
)

from pydantic.version import VERSION as PYDANTIC_VERSION
from typing_extensions import Annotated, Literal, get_args, get_origin


PYDANTIC_V2 = PYDANTIC_VERSION.startswith("2.")

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If it's helpful, I have seen some shim implementations that just use try except.. but this may not necessarily be better. Just offering some other ways to approach this:

https://github.com/langchain-ai/langchain/blob/master/libs/core/langchain_core/pydantic_v1/dataclasses.py

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is more or less the way that FastAPI does it. a try/except might be a bit more inclusive. . . hmm thoughts @banker or @bsbodden ?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The try/except approach makes me feel icky. Are we thinking of dropping Pydantic 1.0 support completely? It's seems a lot of project are doing that nowadays.


if PYDANTIC_V2:
from pydantic.v1 import BaseModel, validator
from pydantic.v1.fields import FieldInfo, ModelField, Undefined, UndefinedType
from pydantic.v1.json import ENCODERS_BY_TYPE
from pydantic.v1.main import ModelMetaclass, validate_model

def use_pydantic_2_plus():
return True

from pydantic import BaseModel, TypeAdapter
from pydantic import ValidationError as ValidationError
from pydantic import validator
from pydantic._internal._model_construction import ModelMetaclass
from pydantic._internal._repr import Representation
from pydantic.deprecated.json import ENCODERS_BY_TYPE
from pydantic.fields import FieldInfo
from pydantic.v1.main import validate_model
from pydantic.v1.typing import NoArgAnyCallable
from pydantic.v1.utils import Representation
from pydantic_core import PydanticUndefined as Undefined
from pydantic_core import PydanticUndefinedType as UndefinedType

@dataclass
class ModelField:
field_info: FieldInfo
name: str
mode: Literal["validation", "serialization"] = "validation"

@property
def alias(self) -> str:
a = self.field_info.alias
return a if a is not None else self.name

@property
def required(self) -> bool:
return self.field_info.is_required()

@property
def default(self) -> Any:
return self.get_default()

@property
def type_(self) -> Any:
return self.field_info.annotation

def __post_init__(self) -> None:
self._type_adapter: TypeAdapter[Any] = TypeAdapter(
Annotated[self.field_info.annotation, self.field_info]
)

def get_default(self) -> Any:
if self.field_info.is_required():
return Undefined
return self.field_info.get_default(call_default_factory=True)

def validate(
self,
value: Any,
values: Dict[str, Any] = {}, # noqa: B006
*,
loc: Tuple[Union[int, str], ...] = (),
) -> Tuple[Any, Union[List[Dict[str, Any]], None]]:
return (
self._type_adapter.validate_python(value, from_attributes=True),
None,
)

def __hash__(self) -> int:
# Each ModelField is unique for our purposes, to allow making a dict from
# ModelField to its JSON Schema.
return id(self)

else:
from pydantic import BaseModel, validator
from pydantic.fields import FieldInfo, ModelField, Undefined, UndefinedType
from pydantic.json import ENCODERS_BY_TYPE
from pydantic.main import ModelMetaclass, validate_model
from pydantic.typing import NoArgAnyCallable
from pydantic.utils import Representation

def use_pydantic_2_plus():
return False
2 changes: 1 addition & 1 deletion aredis_om/model/encoders.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ def jsonable_encoder(
if exclude is not None and not isinstance(exclude, (set, dict)):
exclude = set(exclude)

if isinstance(obj, BaseModel):
if isinstance(obj, BaseModel) and hasattr(obj, "__config__"):
encoder = getattr(obj.__config__, "json_encoders", {})
if custom_encoder:
encoder.update(custom_encoder)
Expand Down
Loading
Loading