redis / redis-om-python

Object mapping, and more, for Redis and Python
MIT License
1.06k stars 108 forks source link

Can't use Pydantic validators with JsonModel #606

Closed webdz9r closed 2 months ago

webdz9r commented 2 months ago

I'm attempting to hash the password before save using Pydantic validators sample Model code


from pydantic import BaseModel, Field
import datetime
from redis_om import (EmbeddedJsonModel, Field, JsonModel)
from pydantic import PositiveInt
from typing import Optional, List

from werkzeug.security import generate_password_hash, check_password_hash
from pydantic import validator

class User(JsonModel):
    first_name: str = Field(index=True)
    last_name: str = Field(index=True)
    email: str = Field(index=True)
    mobile: str = Field(index=True)
    created_at: datetime.datetime = Field()
    updated_at: datetime.datetime = Field()
    timezone: Optional[str]
    courses: set[CourseObj.pk] = Field(default=[])
    meta: Optional[dict] = Field(default={})

    password: str = Field()
    status: str = Field()
    role: str = Field() # admin, user

    @validator('password', pre=True, always=True)
    def hash_password(cls, password):
        return generate_password_hash(password)

    def to_json(self) -> dict:

        return {
            "id": self.pk,
            "first_name": self.first_name,
            "last_name": self.last_name,
            "name": f"{self.first_name} {self.last_name}",
            "email": self.email,
            "mobile": self.mobile,
            "created_at": self.created_at.strftime('%m-%d-%Y'),
            "updated_at": self.updated_at.strftime('%m-%d-%Y'),
            "timezone": self.timezone,
        }

When I import my code it returns TypeError: cannot pickle 'classmethod' object

full stack

File "/Users/chris/.pyenv/versions/3.11.2/lib/python3.11/site-packages/redis_om/model/model.py", line 1205, in __new__
    new_class = super().__new__(cls, name, bases, attrs, **kwargs)
                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/chris/.pyenv/versions/3.11.2/lib/python3.11/site-packages/pydantic/v1/main.py", line 221, in __new__
    inferred = ModelField.infer(
               ^^^^^^^^^^^^^^^^^
  File "/Users/chris/.pyenv/versions/3.11.2/lib/python3.11/site-packages/pydantic/v1/fields.py", line 506, in infer
    return cls(
           ^^^^
  File "/Users/chris/.pyenv/versions/3.11.2/lib/python3.11/site-packages/pydantic/v1/fields.py", line 436, in __init__
    self.prepare()
  File "/Users/chris/.pyenv/versions/3.11.2/lib/python3.11/site-packages/pydantic/v1/fields.py", line 546, in prepare
    self._set_default_and_type()
  File "/Users/chris/.pyenv/versions/3.11.2/lib/python3.11/site-packages/pydantic/v1/fields.py", line 570, in _set_default_and_type
    default_value = self.get_default()
                    ^^^^^^^^^^^^^^^^^^
  File "/Users/chris/.pyenv/versions/3.11.2/lib/python3.11/site-packages/pydantic/v1/fields.py", line 439, in get_default
    return smart_deepcopy(self.default) if self.default_factory is None else self.default_factory()
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/chris/.pyenv/versions/3.11.2/lib/python3.11/site-packages/pydantic/v1/utils.py", line 693, in smart_deepcopy
    return deepcopy(obj)  # slowest way when we actually might need a deepcopy
           ^^^^^^^^^^^^^
  File "/Users/chris/.pyenv/versions/3.11.2/lib/python3.11/copy.py", line 172, in deepcopy
    y = _reconstruct(x, memo, *rv)
        ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/chris/.pyenv/versions/3.11.2/lib/python3.11/copy.py", line 271, in _reconstruct
    state = deepcopy(state, memo)
            ^^^^^^^^^^^^^^^^^^^^^
  File "/Users/chris/.pyenv/versions/3.11.2/lib/python3.11/copy.py", line 146, in deepcopy
    y = copier(x, memo)
        ^^^^^^^^^^^^^^^
  File "/Users/chris/.pyenv/versions/3.11.2/lib/python3.11/copy.py", line 231, in _deepcopy_dict
    y[deepcopy(key, memo)] = deepcopy(value, memo)
                             ^^^^^^^^^^^^^^^^^^^^^
  File "/Users/chris/.pyenv/versions/3.11.2/lib/python3.11/copy.py", line 161, in deepcopy
    rv = reductor(4)
         ^^^^^^^^^^^
TypeError: cannot pickle 'classmethod' object

When I change to to BaseModel the code works fine

class User(BaseModel):
    first_name: str = Field(index=True)
    last_name: str = Field(index=True)
    ....

Version details

pydantic==2.4.2
pydantic_core==2.10.1
redis==5.0.3
redis-om==0.2.2
slorello89 commented 2 months ago

This issue should be fixed in #603 - closing to tidy up, please let me know if you encounter this issue again.