Repository: pydantic/pydantic (Python)

Issue:
Wrong parent namespace fetched when using an explicitly parametrized `TypeAdapter`
### Initial Checks

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

### Description

From https://github.com/pydantic/pydantic/issues/10889:

Because `TypeAdapter` is generic, parametrizing the class results in an instance of a `typing._GenericAlias`:

```python
from pydantic import TypeAdapter

TAInt = TypeAdapter[int]
TAInt.__class__
#> typing._GenericAlias
```

`_GenericAlias` proxies almost everything to it's origin, including `__call__`, so that the following works:

```python
ta = TypeAdapter[int](int)
ta.__class__
#> TypeAdapter
```

However, inside `TypeAdapter.__init__`, we use the parent frame (supposedly the one where the `TypeAdapter` instance was created) to grab the namespaces for type evaluation. Because `_GenericAlias` acts as a proxy, it adds an extra frame in the call stack: calling `TypeAdapter[int]` actually calls `_GenericAlias.__call__`, which then calls `self.__origin__(...)`.

When fetching the namespaces, we end up in the `_GenericAlias.__call__` frame, thus pulling everything from the `typing` module.

The following test is thus failing currently:

```python
from pydantic import TypeAdapter

Text = int

assert TypeAdapter[int]('Text').core_schema == {'type': 'int'}
#> AssertionError: core_schema == {'type': 'str'}, because `typing.Text` is defined as `str`
```

### Example Code

_No response_

### Python, Pydantic & OS Version

```Text
main
```



Discussion:
This is a nice write up to clarify the core issue. 

When the parameterized type is inconsistent, is a runtime error raised? Or is this error mode on the static analysis tools side of the fence? 

For example,

```python
T = TypeAdapter[int](str)
```
> When the parameterized type is inconsistent, is a runtime error raised? Or is this error mode on the static analysis tools side of the fence?

No runtime error is raised as it would require implementing static type checking semantics in Pydantic which is out of scope (and I'm not sure the runtime implementation of `_GenericAlias` would allow this). The type checker will error on it though.



Patch:
diff --git a/pydantic/type_adapter.py b/pydantic/type_adapter.py
index 5fe73b7357a..708dd05f477 100644
--- a/pydantic/type_adapter.py
+++ b/pydantic/type_adapter.py
@@ -4,6 +4,7 @@
 
 import sys
 from dataclasses import is_dataclass
+from types import FrameType
 from typing import (
     Any,
     Generic,
@@ -75,10 +76,44 @@ class TypeAdapter(Generic[T]):
 
     **Note:** `TypeAdapter` instances are not types, and cannot be used as type annotations for fields.
 
+    Args:
+        type: The type associated with the `TypeAdapter`.
+        config: Configuration for the `TypeAdapter`, should be a dictionary conforming to
+            [`ConfigDict`][pydantic.config.ConfigDict].
+
+            !!! note
+                You cannot provide a configuration when instantiating a `TypeAdapter` if the type you're using
+                has its own config that cannot be overridden (ex: `BaseModel`, `TypedDict`, and `dataclass`). A
+                [`type-adapter-config-unused`](../errors/usage_errors.md#type-adapter-config-unused) error will
+                be raised in this case.
+        _parent_depth: Depth at which to search for the [parent frame][frame-objects]. This frame is used when
+            resolving forward annotations during schema building, by looking for the globals and locals of this
+            frame. Defaults to 2, which will result in the frame where the `TypeAdapter` was instantiated.
+
+            !!! note
+                This parameter is named with an underscore to suggest its private nature and discourage use.
+                It may be deprecated in a minor version, so we only recommend using it if you're comfortable
+                with potential change in behavior/support. It's default value is 2 because internally,
+                the `TypeAdapter` class makes another call to fetch the frame.
+        module: The module that passes to plugin if provided.
+
     Attributes:
         core_schema: The core schema for the type.
-        validator (SchemaValidator): The schema validator for the type.
+        validator: The schema validator for the type.
         serializer: The schema serializer for the type.
+        pydantic_complete: Whether the core schema for the type is successfully built.
+
+    ??? tip "Compatibility with `mypy`"
+        Depending on the type used, `mypy` might raise an error when instantiating a `TypeAdapter`. As a workaround, you can explicitly
+        annotate your variable:
+
+        ```py
+        from typing import Union
+
+        from pydantic import TypeAdapter
+
+        ta: TypeAdapter[Union[str, int]] = TypeAdapter(Union[str, int])  # type: ignore[arg-type]
+        ```
 
     ??? info "Namespace management nuances and implementation details"
 
@@ -91,8 +126,8 @@ class TypeAdapter(Generic[T]):
         So instead we look at the globals in our parent stack frame.
 
         It is expected that the `ns_resolver` passed to this function will have the correct
-        namespace for the type we're adapting. See `TypeAdapter.__init__` and `TypeAdapter.rebuild`
-        for various ways to construct this namespace.
+        namespace for the type we're adapting. See the source code for `TypeAdapter.__init__`
+        and `TypeAdapter.rebuild` for various ways to construct this namespace.
 
         This works for the case where this function is called in a module that
         has the target of forward references in its scope, but
@@ -100,16 +135,14 @@ class TypeAdapter(Generic[T]):
 
         For example, take the following:
 
-        a.py
-        ```python
+        ```python {title="a.py"}
         from typing import Dict, List
 
         IntList = List[int]
         OuterDict = Dict[str, 'IntList']
         ```
 
-        b.py
-        ```python {test="skip"}
+        ```python {test="skip" title="b.py"}
         from a import OuterDict
 
         from pydantic import TypeAdapter
@@ -132,6 +165,11 @@ class TypeAdapter(Generic[T]):
         But at the very least this behavior is _subtly_ different from `BaseModel`'s.
     """
 
+    core_schema: CoreSchema
+    validator: SchemaValidator | PluggableSchemaValidator
+    serializer: SchemaSerializer
+    pydantic_complete: bool
+
     @overload
     def __init__(
         self,
@@ -163,41 +201,6 @@ def __init__(
         _parent_depth: int = 2,
         module: str | None = None,
     ) -> None:
-        """Initializes the TypeAdapter object.
-
-        Args:
-            type: The type associated with the `TypeAdapter`.
-            config: Configuration for the `TypeAdapter`, should be a dictionary conforming to [`ConfigDict`][pydantic.config.ConfigDict].
-            _parent_depth: Depth at which to search the for the parent namespace used for schema building.
-                We also use this as a reference level to find the global namespace (_parent_depth - 1).
-                Defaults to 2 because we expect to reference the frame that called the `TypeAdapter` constructor.
-            module: The module that passes to plugin if provided.
-
-        !!! note
-            You cannot use the `config` argument when instantiating a `TypeAdapter` if the type you're using has its own
-            config that cannot be overridden (ex: `BaseModel`, `TypedDict`, and `dataclass`). A
-            [`type-adapter-config-unused`](../errors/usage_errors.md#type-adapter-config-unused) error will be raised in this case.
-
-        !!! note
-            The `_parent_depth` argument is named with an underscore to suggest its private nature and discourage use.
-            It may be deprecated in a minor version, so we only recommend using it if you're
-            comfortable with potential change in behavior / support.
-
-        ??? tip "Compatibility with `mypy`"
-            Depending on the type used, `mypy` might raise an error when instantiating a `TypeAdapter`. As a workaround, you can explicitly
-            annotate your variable:
-
-            ```python
-            from typing import Union
-
-            from pydantic import TypeAdapter
-
-            ta: TypeAdapter[Union[str, int]] = TypeAdapter(Union[str, int])  # type: ignore[arg-type]
-            ```
-
-        Returns:
-            A type adapter configured for the specified `type`.
-        """
         if _type_has_config(type) and config is not None:
             raise PydanticUserError(
                 'Cannot use `config` when the type is a BaseModel, dataclass or TypedDict.'
@@ -210,14 +213,17 @@ def __init__(
         self._type = type
         self._config = config
         self._parent_depth = _parent_depth
+        self.pydantic_complete = False
 
-        self.core_schema: CoreSchema
-        self.validator: SchemaValidator | PluggableSchemaValidator
-        self.serializer: SchemaSerializer
-        self.pydantic_complete: bool = False
+        parent_frame = self._fetch_parent_frame()
+        if parent_frame is not None:
+            globalns = parent_frame.f_globals
+            # Do not provide a local ns if the type adapter happens to be instantiated at the module level:
+            localns = parent_frame.f_locals if parent_frame.f_locals is not globalns else {}
+        else:
+            globalns = {}
+            localns = {}
 
-        localns = _typing_extra.parent_frame_namespace(parent_depth=self._parent_depth) or {}
-        globalns = sys._getframe(max(self._parent_depth - 1, 1)).f_globals
         self._module_name = module or cast(str, globalns.get('__name__', ''))
         self._init_core_attrs(
             ns_resolver=_namespace_utils.NsResolver(
@@ -227,6 +233,17 @@ def __init__(
             force=False,
         )
 
+    def _fetch_parent_frame(self) -> FrameType | None:
+        frame = sys._getframe(self._parent_depth)
+        if frame.f_globals.get('__name__') == 'typing':
+            # Because `TypeAdapter` is generic, explicitly parametrizing the class results
+            # in a `typing._GenericAlias` instance, which proxies instantiation calls to the
+            # "real" `TypeAdapter` class and thus adding an extra frame to the call. To avoid
+            # pulling anything from the `typing` module, use the correct frame (the one before):
+            return frame.f_back
+
+        return frame
+
     def _init_core_attrs(
         self, ns_resolver: _namespace_utils.NsResolver, force: bool, raise_errors: bool = False
     ) -> bool:
@@ -332,8 +349,12 @@ def rebuild(
         Args:
             force: Whether to force the rebuilding of the type adapter's schema, defaults to `False`.
             raise_errors: Whether to raise errors, defaults to `True`.
-            _parent_namespace_depth: The depth level of the parent namespace, defaults to 2.
-            _types_namespace: The types namespace, defaults to `None`.
+            _parent_namespace_depth: Depth at which to search for the [parent frame][frame-objects]. This
+                frame is used when resolving forward annotations during schema rebuilding, by looking for
+                the locals of this frame. Defaults to 2, which will result in the frame where the method
+                was called.
+            _types_namespace: An explicit types namespace to use, instead of using the local namespace
+                from the parent frame. Defaults to `None`.
 
         Returns:
             Returns `None` if the schema is already "complete" and rebuilding was not required.
@@ -349,7 +370,7 @@ def rebuild(
         else:
             rebuild_ns = {}
 
-        # we have to manually fetch globals here because there's no type on the stack for the NsResolver
+        # we have to manually fetch globals here because there's no type on the stack of the NsResolver
         # and so we skip the globalns = get_module_ns_of(typ) call that would normally happen
         globalns = sys._getframe(max(_parent_namespace_depth - 1, 1)).f_globals
         ns_resolver = _namespace_utils.NsResolver(


Test patch:
diff --git a/tests/test_type_adapter.py b/tests/test_type_adapter.py
index 8f30064d6a7..e9120c361e2 100644
--- a/tests/test_type_adapter.py
+++ b/tests/test_type_adapter.py
@@ -592,3 +592,19 @@ class SimpleDataclass:
 def test_ta_repr(type_: Any, repr_: str) -> None:
     ta = TypeAdapter(type_)
     assert repr(ta) == f'TypeAdapter({repr_})'
+
+
+def test_correct_frame_used_parametrized(create_module) -> None:
+    """https://github.com/pydantic/pydantic/issues/10892"""
+
+    @create_module
+    def module_1() -> None:
+        from pydantic import TypeAdapter
+
+        Any = int  # noqa: F841
+
+        # 'Any' should resolve to `int`, not `typing.Any`:
+        ta = TypeAdapter[int]('Any')  # noqa: F841
+
+    with pytest.raises(ValidationError):
+        module_1.ta.validate_python('a')