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

fix __post_init__ cause infinite recursion in inheritance #606

Conversation

Projects
None yet
2 participants
@Hanaasagi
Copy link
Contributor

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__

post_init_original = getattr(_cls, '__post_init__', None)
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
_cls.__post_init__ = _pydantic_post_init
cls = dataclasses._process_class(_cls, init, repr, eq, order, unsafe_hash, frozen) # type: ignore
fields: Dict[str, Any] = {
field.name: (field.type, field.default if field.default != dataclasses.MISSING else Required)
for field in dataclasses.fields(cls)
}
cls.__post_init_original__ = post_init_original
cls.__post_init_post_parse__ = post_init_post_parse

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__

def _pydantic_post_init(self: 'DataclassType', *initvars: Any) -> None:
if self.__post_init_original__:
self.__post_init_original__(*initvars)
d = validate_model(self.__pydantic_model__, self.__dict__, cls=self.__class__)[0]
object.__setattr__(self, '__dict__', d)
object.__setattr__(self, '__initialised__', True)
if self.__post_init_post_parse__:
self.__post_init_post_parse__()

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

This comment has been minimized.

Copy link

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

Hanaasagi added some commits Jun 20, 2019

@samuelcolvin
Copy link
Owner

left a comment

otherwise this looks great.

Show resolved Hide resolved HISTORY.rst Outdated
@@ -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:

This comment has been minimized.

Copy link
@samuelcolvin

samuelcolvin Jun 20, 2019

Owner

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

Hanaasagi and others added some commits Jun 21, 2019

Update HISTORY.rst
Co-Authored-By: Samuel Colvin <samcolvin@gmail.com>

@samuelcolvin samuelcolvin merged commit 6233554 into samuelcolvin:master Jun 21, 2019

6 of 9 checks passed

Header rules No header rules processed
Details
Pages changed All files already uploaded
Details
Redirect rules No redirect rules processed
Details
LGTM analysis: Python No new or fixed alerts
Details
Mixed content No mixed content detected
Details
codecov/project 100% (+0%) compared to dd44fda
Details
continuous-integration/travis-ci/pr The Travis CI build passed
Details
deploy/netlify Deploy preview ready!
Details
samuelcolvin.pydantic Build #20190621.3 succeeded
Details
@samuelcolvin

This comment has been minimized.

Copy link
Owner

commented Jun 21, 2019

great, thank you.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.