Repository: pydantic/pydantic
Pull request: Add a test demonstrating initvar works with inheritance
Closes https://github.com/pydantic/pydantic/issues/5496.

~It seems that at some point since the 2.0a4 release, this got fixed, but I've added a test written from the code in the issue. (This test fails in 2.0a4.)~

This got fixed for python 3.11 after #5760, but not for other python versions..

Selected Reviewer: @adriangb

Problem statement:
V2 pydantic dataclass: InitVar and inheritance don't work together
### Initial Checks

- [X] I confirm that I'm using Pydantic V2 installed directly from the `main` branch, or equivalent

### Description

When using `from __future__ import annotations` the code example raises the following error:
``pydantic.errors.PydanticSchemaGenerationError: Unable to generate pydantic-core schema for dataclasses.InitVar[int]. Setting `arbitrary_types_allowed=True` in the model_config may prevent this error.``

### Example Code

```Python
from __future__ import annotations
from dataclasses import InitVar
from pydantic.dataclasses import dataclass

@dataclass
class Foo:
    init: InitVar[int]


@dataclass
class Bar(Foo):
    arg: int
```


### Python, Pydantic & OS Version

```Text
pydantic version: 2.0a2
        pydantic-core version: 0.24.0 release build profile
                 install path: C:\Program Files\Python\Lib\site-packages\pydantic
               python version: 3.11.0 (main, Oct 24 2022, 18:26:48) [MSC v.1933 64 bit (AMD64)]
                     platform: Windows-10-10.0.22621-SP0
     optional deps. installed: ['typing-extensions']
```

Hints from issue discussion:
Thanks for reporting, confirmed.

I get a slightly different error `Forward references must evaluate to types. Got dataclasses.InitVar[int].`, but that might be cause I'm on 3.10.

Still definitely needs to be fixed.
I did some investigation into this, and part of the problem is that dataclasses doesn't expose via "normal" public APIs whether it has determined if a field is an InitVar or not. (In this case, if you look at what it stores on `.__dataclass_fields__`, it has a ForwardRef as the type annotation, even though internally it did realize it should be an `InitVar` by doing some sort of namespace evaluation.)

However, there _is_ a way to determine if `dataclasses` realized the type should be an `InitVar` using only public APIs — the "field" will not show up in `dataclasses.fields(MyDataclass)`, but _will_ be present in `inspect.signature(MyDataclass)`. The main challenge remaining is that recognizing the field is an `InitVar` doesn't tell you what type it is, so you still need to make sure to evaluate the ForwardRef properly. The most likely way for this to work would be to add `InitVar` to the namespace, but there are various scenarios where this might not work perfectly (e.g., `ForwardRef('dataclasses.InitVar[int]')`, importing `InitVar` under a different name, etc.).

There may be a good way to resolve this, but I haven't discovered it quite yet. PR fixing this welcome, otherwise I'm sure we'll get to it eventually.
(Oh, and you can address the "ForwardRef must evaluate to types" thing by monkeypatching a `__call__` method onto `InitVar`, but I seem to vaguely recall that that both doesn't actually solve the problem, and there is a more promising path that doesn't require it. I'm dumping my half-baked thoughts on this here so they aren't lost.)
I think the best way forward here is probably to just check pseudo-private attributes in the `__dataclass_fields__` to check for being an InitVar

Commit message: Demonstrate that the init var type is used despite coming from a forward ref

Patch to review:
diff --git a/pydantic/dataclasses.py b/pydantic/dataclasses.py
index b35758ad31c..88317a0ec88 100644
--- a/pydantic/dataclasses.py
+++ b/pydantic/dataclasses.py
@@ -211,3 +211,8 @@ def create_dataclass(cls: type[Any]) -> type[PydanticDataclass]:
 
 
 __getattr__ = getattr_migration(__name__)
+
+if (3, 8) <= sys.version_info < (3, 11):
+    # Monkeypatch dataclasses.InitVar so that typing doesn't error if it occurs as a type when evaluating type hints
+    # This is not necessary in 3.11, and we don't support InitVar in python < 3.8.
+    dataclasses.InitVar.__call__ = lambda: None  # type: ignore
diff --git a/tests/test_dataclasses.py b/tests/test_dataclasses.py
index ce4ca294d12..f34b77e2e72 100644
--- a/tests/test_dataclasses.py
+++ b/tests/test_dataclasses.py
@@ -3,6 +3,7 @@
 import re
 import sys
 from collections.abc import Hashable
+from dataclasses import InitVar
 from datetime import datetime
 from pathlib import Path
 from typing import Any, Callable, ClassVar, Dict, FrozenSet, Generic, List, Optional, Set, TypeVar, Union
@@ -2004,3 +2005,34 @@ class User:
             'input': ArgsKwargs((), {'name': 'test name', 'score': 2}),
         },
     ]
+
+
+@pytest.mark.skipif(sys.version_info < (3, 8), reason='InitVar not supported in python 3.7')
+def test_init_vars_inheritance():
+    init_vars = []
+
+    @pydantic.dataclasses.dataclass
+    class Foo:
+        init: 'InitVar[int]'
+
+    @pydantic.dataclasses.dataclass
+    class Bar(Foo):
+        arg: int
+
+        def __post_init__(self, init: int) -> None:
+            init_vars.append(init)
+
+    bar = Bar(init=1, arg=2)
+    assert TypeAdapter(Bar).dump_python(bar) == {'arg': 2}
+    assert init_vars == [1]
+
+    with pytest.raises(ValidationError) as exc_info:
+        Bar(init='a', arg=2)
+    assert exc_info.value.errors(include_url=False) == [
+        {
+            'input': 'a',
+            'loc': ('init',),
+            'msg': 'Input should be a valid integer, unable to parse string as an integer',
+            'type': 'int_parsing',
+        }
+    ]


Code review comments:
diff_hunk: @@ -211,3 +211,8 @@ def create_dataclass(cls: type[Any]) -> type[PydanticDataclass]:
 
 
 __getattr__ = getattr_migration(__name__)
+
+if (3, 8) <= sys.version_info < (3, 11):
+    # Monkeypatch dataclasses.InitVar so that typing doesn't error if it occurs as a type when evaluating type hints
+    # This is not necessary in 3.11, and we don't support InitVar in python < 3.8.
+    dataclasses.InitVar.__call__ = lambda: None  # type: ignore
path: pydantic/dataclasses.py
text: @user1:
What happens if this gets used before pydantic gets imported? I guess it just doesn't work right?

@author:
It would error if you tried to use `typing.get_type_hints` first, but currently I don't think there is a way to get to the point of building a dataclass schema without importing something from `pydantic.dataclasses`, at least the following works:
```import dataclasses

from pydantic import BaseModel

initvars = []
@user2.dataclass
class A:
    x: 'dataclasses.InitVar[int]'
    def __post_init__(self, x):
        initvars.append(x)

class B(BaseModel):
    a: A

B.model_validate({'a': {'x': 2}})

assert initvars == [2]

B.model_validate({'a': {'x': 'abc'}})  # errors
```

Do you think we need to be more careful about ensuring the monkeypatch happens no matter what import path is followed? We could do it in `pydantic.__init__` or similar, that would be a little more bulletproof I guess.


@user1:
I mean what about:

```python
from dataclasses import dataclass, InitVar

@user3
class A:
    x: InitVar[int]  # calls `InitVar.__call__`?

# maybe in another module
from pydantic import BaseModel

class B(BaseModel):
    a: A
```

@author:
`InitVar.__call__` doesn't actually get called — the `typing.get_type_hints` function errors if it doesn't think the annotations are actually types. It doesn't think `InitVar[int]` is a type pre-3.11 I guess, but one thing you can do to get it to make it think something is a type is to make that thing callable (it has a boolean check `if callable(maybe_type):` in which case it lets you use that annotation and not error). That's why this monkeypatch works — as long as `get_type_hints` doesn't get called on the type until after the monkeypatch is applied (which I think is the case because that doesn't happen until you are building the schema).