Skip to content

Conversation

Hanaasagi
Copy link
Contributor

@Hanaasagi Hanaasagi commented Jun 20, 2019

Change Summary

  • Move _pydantic_post_init into _process_class, so it can use the closure feature to capture parent scope post_init_original method.
  • Remove __post_init_original__ and __post_init_post_parse__ method.

Related issue number

It will resolve #536

Checklist

  • Unit tests for the changes exist
  • Tests pass on CI and coverage remains at 100%
  • Documentation reflects the changes where applicable
  • HISTORY.rst has been updated
    • if this is the first change since a release, please add a new section
    • include the issue number or this pull request number #<number>
    • include your github username @<whomever>

Why it can resolve the problem

Sorry for my pool English, it is not my native language.

Firstly, we should know

class A:
    def test(self):
        print(self)


class B(A):
    def test(self):
        super().test()


B().test()

Above code will print <__main__.B object at 0x7fdb1d7a1908>, it is a instance of Class B that we pass to A.test.

In pydantic, if we use the dataclass decorator, it will replace user's __post_init__ to _pydantic_post_init, and save original __post_init__ in __post_init_original__

https://github.com/samuelcolvin/pydantic/blob/dd44fda8a6f6eed961cd03ad1e70049e523cdb40/pydantic/dataclasses.py#L74-L86

According to code of issue #536,

from pydantic.dataclasses import dataclass

@dataclass
class A:
    a: int
    
    def __post_init__(self):
        print("A")

@dataclass
class B(A):
    b: int
    
    def __post_init__(self):
        super().__post_init__()
        print("B")

B(b=1, a=2)

After decorated by dataclass,

In [1]: A.__post_init__
Out[1]: <function pydantic.dataclasses._pydantic_post_init(self: 'DataclassType', *initvars: Any) -> None>

In [2]: A.__post_init_original__
Out[2]: <function __main__.A.__post_init__(self)>

In [3]: B.__post_init__
Out[3]: <function pydantic.dataclasses._pydantic_post_init(self: 'DataclassType', *initvars: Any) -> None>

In [4]: B.__post_init_original__
Out[4]: <function __main__.B.__post_init__(self)>

In [5]: A.__post_init__ is B.__post_init__
Out[5]: True

So when we create an instance of class B, like B(a=1, b=2). Python stdlib's dataclass will call __post_init__ automatically. But it calls pydantic.dataclasses._pydantic_post_initactually(we replace it).

In _pydantic_post_init, it will call original __post_init__ through __post_init_original__

https://github.com/samuelcolvin/pydantic/blob/dd44fda8a6f6eed961cd03ad1e70049e523cdb40/pydantic/dataclasses.py#L28-L35

In above code, we call super().__post_init__() in __post_init_original__, Python will find A.__post_init__ and pass instance b. Currently A.__post_init__ is also a pydantic.dataclasses._pydantic_post_init. So it will get __post_init_original__ by instance b, we fall into a infinite recursion.

@codecov
Copy link

codecov bot commented Jun 20, 2019

Codecov Report

Merging #606 into master will not change coverage.
The diff coverage is 100%.

@@          Coverage Diff          @@
##           master   #606   +/-   ##
=====================================
  Coverage     100%   100%           
=====================================
  Files          15     15           
  Lines        2584   2582    -2     
  Branches      510    510           
=====================================
- Hits         2584   2582    -2

Copy link
Member

@samuelcolvin samuelcolvin left a comment

Choose a reason for hiding this comment

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

otherwise this looks great.

@@ -75,15 +63,23 @@ def _process_class(
post_init_post_parse = getattr(_cls, '__post_init_post_parse__', None)
if post_init_original and post_init_original.__name__ == '_pydantic_post_init':
post_init_original = None

def _pydantic_post_init(instance: 'DataclassType', *initvars: Any) -> None:
Copy link
Member

Choose a reason for hiding this comment

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

is there any reason we can't call the first argument self?

Hanaasagi and others added 2 commits June 21, 2019 12:00
Co-Authored-By: Samuel Colvin <samcolvin@gmail.com>
@samuelcolvin samuelcolvin merged commit 6233554 into pydantic:master Jun 21, 2019
@samuelcolvin
Copy link
Member

great, thank you.

alexdrydew pushed a commit to alexdrydew/pydantic that referenced this pull request Dec 23, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Recursion issue with dataclass inheritance
2 participants