Repository: pydantic/pydantic
Pull request: fix __post_init__ cause infinite recursion in inheritance
## 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

* [x] Unit tests for the changes exist
* [x] Tests pass on CI and coverage remains at 100%
* [ ] Documentation reflects the changes where applicable
* [x] `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

```Python
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,

```Python
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`,

```Python
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_init`actually(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.



Problem statement:
Recursion issue with dataclass inheritance
<!-- Questions, Feature Requests, and Bug Reports are all welcome -->
<!-- delete as applicable: -->
# Bug

For bugs/questions:
* OS: **Ubuntu 19.04**
* Python version `3.7.3 (default, Mar 27 2019, 22:11:17) 
[GCC 7.3.0]`
* Pydantic version `0.25`

Where possible please include a self contained code snippet describing your 
bug, question, or where applicable feature request:

```py
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)
```
**Expected Output**:

```py
A
B
B(a=2, b=1)
```

<details><summary><strong>Current Error & Stacktrace</strong></summary>
<p>

```py

---------------------------------------------------------------------------
RecursionError                            Traceback (most recent call last)
<ipython-input-28-55581d931e2a> in <module>
----> 1 B(b=1, a=2)

<string> in __init__(self, a, b)

[path_removed]/.venv/lib/python3.7/site-packages/pydantic/dataclasses.py in _pydantic_post_init(self)
     30     object.__setattr__(self, '__initialised__', True)
     31     if self.__post_init_original__:
---> 32         self.__post_init_original__()
     33 
     34 

<ipython-input-27-252024563a0b> in __post_init__(self)
     11 
     12     def __post_init__(self):
---> 13         super().__post_init__()
     14         print("B")

... last 2 frames repeated, from the frame below ...

[path_removed]/.venv/lib/python3.7/site-packages/pydantic/dataclasses.py in _pydantic_post_init(self)
     30     object.__setattr__(self, '__initialised__', True)
     31     if self.__post_init_original__:
---> 32         self.__post_init_original__()
     33 
     34 

RecursionError: maximum recursion depth exceeded while calling a Python object
```

</p>
</details>

Hints from issue discussion:
However, calling `super().__post_init_original__()` instead of `super().__post_init__()`  does seem to work as expected. Is this intended behavior? 
maybe be possible to fix this, but I can't look into this right now.

PR welcome if you can resolve it.
pydantic will replace user's `__post_init__` to `_pydantic_post_init`
https://github.com/samuelcolvin/pydantic/blob/84820bdede195b36f6862b3978cf9e0814989f16/pydantic/dataclasses.py#L74-L78

And `_pydantic_post_init`, it will call origin `__post_init__`
https://github.com/samuelcolvin/pydantic/blob/84820bdede195b36f6862b3978cf9e0814989f16/pydantic/dataclasses.py#L28-L30

From the above example code

`__post_init__` in class `B` invokes `super().__post_init__()`, and it will invoke class `A`'s `_pydantic_post_init`, and pass instance `b` as `self` argument. So it will find class `B`'s `__post_init__` again. I think this is the root cause. `_pydantic_post_init` should find the origin `__post_init__` through the class, instead of instance.
Here's a Way to Fix It.  Move `_pydantic_post_init` to `_process_class`, use closure to capture user's `__post_init__`. But it will recreate `_pydantic_post_init` function object everytime.

```Diff
diff --git a/pydantic/dataclasses.py b/pydantic/dataclasses.py
index 4dbae31..c3b7546 100644
--- a/pydantic/dataclasses.py
+++ b/pydantic/dataclasses.py
@@ -25,16 +25,6 @@ if TYPE_CHECKING:  # pragma: no cover
             pass


-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__()
-
-
 def _validate_dataclass(cls: Type['DataclassType'], v: Any) -> 'DataclassType':
     if isinstance(v, cls):
         return v
@@ -75,6 +65,16 @@ 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:
+        if post_init_original:
+            post_init_original(instance, *initvars)
+        d = validate_model(instance.__pydantic_model__, instance.__dict__, cls=instance.__class__)[0]
+        object.__setattr__(instance, '__dict__', d)
+        object.__setattr__(instance, '__initialised__', True)
+        if post_init_post_parse:
+            post_init_post_parse(instance)
+
     _cls.__post_init__ = _pydantic_post_init
     cls = dataclasses._process_class(_cls, init, repr, eq, order, unsafe_hash, frozen)  # type: ignore
```
I'm not sure I understand this, if you think there should be a change, please create a PR and we can discuss from there.

Commit message: update code quote

Patch to review:
diff --git a/HISTORY.rst b/HISTORY.rst
index e78c5558b3b..9e7911adb60 100644
--- a/HISTORY.rst
+++ b/HISTORY.rst
@@ -3,6 +3,10 @@
 History
 -------
 
+v0.30 (unreleased)
+..................
+* fix ``__post_init__`` cause infinite recursion in inheritance, #606 by @Hanaasagi
+
 v0.29 (2019-06-19)
 ..................
 * support dataclasses.InitVar, #592 by @pfrederiks
diff --git a/pydantic/dataclasses.py b/pydantic/dataclasses.py
index 4dbae314b09..5d108851d94 100644
--- a/pydantic/dataclasses.py
+++ b/pydantic/dataclasses.py
@@ -13,8 +13,6 @@
 
     class DataclassType:
         __pydantic_model__: Type[BaseModel]
-        __post_init_original__: Callable[..., None]
-        __post_init_post_parse__: Callable[..., None]
         __initialised__: bool
 
         def __init__(self, *args: Any, **kwargs: Any) -> None:
@@ -25,16 +23,6 @@ def __validate__(cls, v: Any) -> 'DataclassType':
             pass
 
 
-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__()
-
-
 def _validate_dataclass(cls: Type['DataclassType'], v: Any) -> 'DataclassType':
     if isinstance(v, cls):
         return v
@@ -75,6 +63,16 @@ 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:
+        if post_init_original is not None:
+            post_init_original(instance, *initvars)
+        d = validate_model(instance.__pydantic_model__, instance.__dict__, cls=instance.__class__)[0]
+        object.__setattr__(instance, '__dict__', d)
+        object.__setattr__(instance, '__initialised__', True)
+        if post_init_post_parse is not None:
+            post_init_post_parse(instance)
+
     _cls.__post_init__ = _pydantic_post_init
     cls = dataclasses._process_class(_cls, init, repr, eq, order, unsafe_hash, frozen)  # type: ignore
 
@@ -82,8 +80,6 @@ def _process_class(
         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
 
     validators = gather_validators(cls)
     cls.__pydantic_model__ = create_model(
diff --git a/tests/test_dataclasses.py b/tests/test_dataclasses.py
index 851eaa275a6..97abd157709 100644
--- a/tests/test_dataclasses.py
+++ b/tests/test_dataclasses.py
@@ -108,6 +108,34 @@ def __post_init__(self):
     assert post_init_called
 
 
+def test_post_init_inheritance_chain():
+    parent_post_init_called = False
+    post_init_called = False
+
+    @pydantic.dataclasses.dataclass
+    class ParentDataclass:
+        a: int
+
+        def __post_init__(self):
+            nonlocal parent_post_init_called
+            parent_post_init_called = True
+
+    @pydantic.dataclasses.dataclass
+    class MyDataclass(ParentDataclass):
+        b: int
+
+        def __post_init__(self):
+            super().__post_init__()
+            nonlocal post_init_called
+            post_init_called = True
+
+    d = MyDataclass(a=1, b=2)
+    assert d.a == 1
+    assert d.b == 2
+    assert parent_post_init_called
+    assert post_init_called
+
+
 def test_post_init_post_parse():
     post_init_post_parse_called = False
 


Code review comments:
diff_hunk: @@ -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:
path: pydantic/dataclasses.py
text: @user1:
is there any reason we can't call the first argument `self`?