Repository: pydantic/pydantic

Issue:
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>



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.

Resolving patch:
diff --git a/pydantic/dataclasses.py b/pydantic/dataclasses.py
--- 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(self: 'DataclassType', *initvars: Any) -> None:
+        if post_init_original is not None:
+            post_init_original(self, *initvars)
+        d = validate_model(self.__pydantic_model__, self.__dict__, cls=self.__class__)[0]
+        object.__setattr__(self, '__dict__', d)
+        object.__setattr__(self, '__initialised__', True)
+        if post_init_post_parse is not None:
+            post_init_post_parse(self)
+
     _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(


Test patch:
diff --git a/tests/test_dataclasses.py b/tests/test_dataclasses.py
--- 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