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

Cannot use model_copy(deep=True) on a model instance which was created using model_construct, when there is a model_post_init in the class hierarchy. #9122

Closed
1 task done
babygrimes opened this issue Mar 27, 2024 · 4 comments · Fixed by #9168
Labels
bug V2 Bug related to Pydantic V2 good first issue help wanted Pull Request welcome

Comments

@babygrimes
Copy link
Contributor

Initial Checks

  • I confirm that I'm using Pydantic V2

Description

This standalone code produces the issue. When constructing a model instance using .model_construct instead of .model_validate or __init__, and if the model class defines model_post_init (on the class, or earlier in the hierarchy - both will trigger the issue), the below error is produced.

If the m.model_copy(deep=True) line is changed to m.model_validate(m, from_attributes=True, strict=True) (just to show the difference, and is what I'm currently using as a workaround) - everything appears to work as expected.

Additionally, you can comment out the model_post_init in the example below, and everything again works as expected (though I really want to use model_post_init in my real code for various reasons).

I traced this as far as I could into main.py:model_construct and it appears to be related to lines 238-249. It appears in the model_construct path there is no initialization of __pydantic_private__, resulting in the stack trace here:

  File "/home/dave/test_deepcopy.py", line 21, in <module>
    main()
  File "/home/dave/test_deepcopy.py", line 17, in main
    m.model_copy(deep=True)
  File "/home/dave/axial/src/services/axial-api.git/venv/lib/python3.11/site-packages/pydantic/main.py", line 266, in model_copy
    copied = self.__deepcopy__() if deep else self.__copy__()
             ^^^^^^^^^^^^^^^^^^^
  File "/home/dave/axial/src/services/axial-api.git/venv/lib/python3.11/site-packages/pydantic/main.py", line 723, in __deepcopy__
    if self.__pydantic_private__ is None:
       ^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/dave/axial/src/services/axial-api.git/venv/lib/python3.11/site-packages/pydantic/main.py", line 764, in __getattr__
    return super().__getattribute__(item)  # Raises AttributeError if appropriate
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
AttributeError: 'Model' object has no attribute '__pydantic_private__'. Did you mean: '__pydantic_complete__'?```



### Example Code

```Python
#!/usr/bin/env python3
# coding: utf-8
from typing import Any

import pydantic


class Model(pydantic.BaseModel, revalidate_instances="always"):
    id: int  # noqa: A003

    def model_post_init(self, context: Any) -> None:
        super().model_post_init(context)


def main() -> None:
    m = Model.model_construct({"id": 1})
    m.model_copy(deep=True)


if __name__ == "__main__":
    main()

Python, Pydantic & OS Version

pydantic version: 2.6.4
        pydantic-core version: 2.16.3
          pydantic-core build: profile=release pgo=true
                 install path: /home/dave/axial/src/services/axial-api.git/venv/lib/python3.11/site-packages/pydantic
               python version: 3.11.8 (main, Feb 25 2024, 16:41:26) [GCC 9.4.0]
                     platform: Linux-5.15.0-1056-aws-x86_64-with-glibc2.31
             related packages: fastapi-0.110.0 typing_extensions-4.10.0 mypy-1.9.0
                       commit: unknown
@babygrimes babygrimes added bug V2 Bug related to Pydantic V2 pending Awaiting a response / confirmation labels Mar 27, 2024
@sydney-runkle
Copy link
Member

Looks like a bug! Thanks for reporting this!

@sydney-runkle sydney-runkle added good first issue help wanted Pull Request welcome and removed pending Awaiting a response / confirmation labels Mar 29, 2024
@babygrimes
Copy link
Contributor Author

It occurs to me that maybe this simple patch would be sufficient to fix this. The thinking is that __pydantic_private__ is allowed to be any of:

  1. An unset attribute
  2. None
  3. Dictionary of private attributes

The issue that is triggering the bug is that it is unset when the __deepcopy__ call is made, but the internals assume it is either None/Dict. This patch adds coverage of "Unset" (pass).

(venv) [dmgrime@dave-laptop:~]$ diff -u venv/lib/python3.11/site-packages/pydantic/main.py /tmp
--- venv/lib/python3.11/site-packages/pydantic/main.py  2024-04-03 09:41:34.015680649 -0400
+++ /tmp/main.py        2024-04-03 09:40:54.425678622 -0400
@@ -720,7 +720,9 @@
         # and attempting a deepcopy would be marginally slower.
         _object_setattr(m, '__pydantic_fields_set__', copy(self.__pydantic_fields_set__))

-        if self.__pydantic_private__ is None:
+        if not hasattr(self, "__pydantic_private__"):
+            pass
+        elif self.__pydantic_private__ is None:
             _object_setattr(m, '__pydantic_private__', None)
         else:
             _object_setattr(
(venv) [dmgrime@dave-laptop:~]$

@sydney-runkle
Copy link
Member

@babygrimes,

Please open a PR, I'd be happy to review!

@babygrimes
Copy link
Contributor Author

Will get you a PR today!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug V2 Bug related to Pydantic V2 good first issue help wanted Pull Request welcome
Projects
None yet
2 participants