Repository: pydantic/pydantic (Python)

Issue:
Unable to evaluate type annotation 'ClassVar[str]' error when creating BaseModel with ClassVar using from __future__ import annotations
### Initial Checks

- [X] I confirm that I'm using Pydantic V2

### Description

This changed in 2.9.0, possibly due to #10157. Pinning to 2.8.2 fixes it.

Now, if a `ClassVar` attribute is defined on a `BaseModel` with `from __future__ import annotations`, `inspect_namespace` calls `eval_type_backport` with a `ForwardRef(..., is_class=False)`, which calls `typing._type_check` with `allow_special_forms=False`. Perhaps `is_classvar` should to catch this case in the for loop in `inspect_namespace` to skip this check?

Is this expected? Should I not be using `from __future__ import annotations`?

### Example Code

```Python
from __future__ import annotations

from typing import ClassVar

from pydantic import BaseModel


class MyModel(BaseModel):
    _x: ClassVar[str]
```


### Python, Pydantic & OS Version

```Text
pydantic version: 2.9.0
        pydantic-core version: 2.23.2
          pydantic-core build: profile=release pgo=false
                 install path: /venv/lib/python3.11/site-packages/pydantic
               python version: 3.11.9 (main, Jul 23 2024, 07:22:22) [GCC 12.2.0]
                     platform: Linux-4.18.0-513.24.1.el8_9.x86_64-x86_64-with-glibc2.36
             related packages: mypy-1.11.2 typing_extensions-4.12.2 pyright-1.1.378
                       commit: unknown
```



Discussion:
I am running into the same issue here in version `2.9`
Thanks for the bug report, that's on me. class vars should indeed be evaluated with `is_class` set to `True`.
@sydney-runkle I have the same problem with a bit different usecase, importing the file with importlib:

# pydanticbug.py
```
from __future__ import annotations

from typing import Optional

from pydantic import BaseModel


class Model(BaseModel):
    key: Optional[str] = ""
```

# test_pydanticbug.py
```
import importlib

loader = importlib.machinery.SourceFileLoader("api_mod", "pydanticbug.py")
spec = importlib.util.spec_from_loader(loader.name, loader)
mod = importlib.util.module_from_spec(spec)
loader.exec_module(mod)
obj = mod.Model()

```


```
Traceback (most recent call last):
  File "/work/test_pydanticbug.py", line 7, in <module>
    obj = mod.Model()
          ^^^^^^^^^^^
  File "/venv/lib/python3.11/site-packages/pydantic/main.py", line 211, in __init__
    validated_self = self.__pydantic_validator__.validate_python(data, self_instance=self)
                     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/venv/lib/python3.11/site-packages/pydantic/_internal/_mock_val_ser.py", line 99, in __getattr__
    raise PydanticUserError(self._error_message, code=self._code)
pydantic.errors.PydanticUserError: `Model` is not fully defined; you should define `Optional`, then call `Model.model_rebuild()`.

For further information visit https://errors.pydantic.dev/2.9/u/class-not-fully-defined
```
@atti92, see https://github.com/pydantic/pydantic/issues/2363.
> @atti92, see #2363.

My error only happens with typing + future annotations. That's why I posted it here, because the same code works with 2.8



Patch:
diff --git a/pydantic/_internal/_model_construction.py b/pydantic/_internal/_model_construction.py
index 86c9bf05fe4..52542a3bee2 100644
--- a/pydantic/_internal/_model_construction.py
+++ b/pydantic/_internal/_model_construction.py
@@ -11,7 +11,7 @@
 from abc import ABCMeta
 from functools import lru_cache, partial
 from types import FunctionType
-from typing import Any, Callable, ForwardRef, Generic, Literal, NoReturn
+from typing import Any, Callable, Generic, Literal, NoReturn
 
 import typing_extensions
 from pydantic_core import PydanticUndefined, SchemaSerializer
@@ -30,6 +30,7 @@
 from ._schema_generation_shared import CallbackGetCoreSchemaHandler
 from ._signature import generate_pydantic_signature
 from ._typing_extra import (
+    _make_forward_ref,
     eval_type_backport,
     is_annotated,
     is_classvar,
@@ -436,6 +437,8 @@ def inspect_namespace(  # noqa C901
             is_valid_privateattr_name(ann_name)
             and ann_name not in private_attributes
             and ann_name not in ignored_names
+            # This condition is a false negative when `ann_type` is stringified,
+            # but it is handled in `set_model_fields`:
             and not is_classvar(ann_type)
             and ann_type not in all_ignored_types
             and getattr(ann_type, '__module__', None) != 'functools'
@@ -446,7 +449,9 @@ def inspect_namespace(  # noqa C901
                 frame = sys._getframe(2)
                 if frame is not None:
                     ann_type = eval_type_backport(
-                        ForwardRef(ann_type), globalns=frame.f_globals, localns=frame.f_locals
+                        _make_forward_ref(ann_type, is_argument=False, is_class=True),
+                        globalns=frame.f_globals,
+                        localns=frame.f_locals,
                     )
             if is_annotated(ann_type):
                 _, *metadata = typing_extensions.get_args(ann_type)


Test patch:
diff --git a/tests/test_forward_ref.py b/tests/test_forward_ref.py
index d9280ff6be7..4d0e1e2b2da 100644
--- a/tests/test_forward_ref.py
+++ b/tests/test_forward_ref.py
@@ -581,10 +581,12 @@ def test_class_var_as_string(create_module):
 
 class Model(BaseModel):
     a: ClassVar[int]
+    _b: ClassVar[int]
 """
     )
 
-    assert module.Model.__class_vars__ == {'a'}
+    assert module.Model.__class_vars__ == {'a', '_b'}
+    assert module.Model.__private_attributes__ == {}
 
 
 def test_json_encoder_str(create_module):