fix(core): avoid eager pydantic.v1 import in @deprecated (#37308)

`langchain_core._api.deprecation` previously did `from
pydantic.v1.fields import FieldInfo as FieldInfoV1` at module scope,
which triggers Pydantic's `UserWarning("Core Pydantic V1 functionality
isn't compatible with Python 3.14 or greater.")` on every
`langchain_core` import under 3.14+. The v1 symbol is only needed inside
one runtime branch of `@deprecated`, so it's now resolved lazily.

## Changes
- Replace the top-level v1 `FieldInfo` import with
`_is_pydantic_v1_field_info`, which probes
`sys.modules.get("pydantic.v1.fields")` instead of forcing the import.
The reconstruction inside `deprecated`'s `finalize` closure imports
`FieldInfoV1` lazily, gated by the predicate — so the warning only fires
if a caller has already loaded `pydantic.v1` themselves.
- Add a subprocess-based regression test asserting that importing
`langchain_core._api.deprecation` does not pull any `pydantic.v1*`
module into `sys.modules`. Verified to fail when the eager import is
reintroduced.
- Add a v1 `FieldInfo` decoration test — the v1 branch of `@deprecated`
previously had zero direct coverage.
- Update the stale `# Last Any should be FieldInfoV1 but this leads to
circular imports` comment on `T`'s bound, which no longer reflects the
real reason (it's about the 3.14 warning, not circularity).
This commit is contained in:
Mason Daugherty
2026-05-09 20:35:17 -04:00
committed by GitHub
parent 85e491821e
commit 8b21400627
2 changed files with 68 additions and 3 deletions

View File

@@ -12,20 +12,39 @@ module.
import contextlib
import functools
import inspect
import sys
import warnings
from collections.abc import Callable, Generator
from typing import (
TYPE_CHECKING,
Any,
ParamSpec,
TypeGuard,
TypeVar,
cast,
)
from pydantic.fields import FieldInfo
from pydantic.v1.fields import FieldInfo as FieldInfoV1
from langchain_core._api.internal import is_caller_internal
if TYPE_CHECKING:
from pydantic.v1.fields import FieldInfo as FieldInfoV1
def _is_pydantic_v1_field_info(obj: Any) -> TypeGuard["FieldInfoV1"]:
"""Check if `obj` is a `pydantic.v1.fields.FieldInfo` without forcing import.
Importing `pydantic.v1` emits a `UserWarning` on Python 3.14+. Skipping the
import entirely when no caller has constructed a v1 `FieldInfo` keeps that
warning out of `langchain_core`'s import path. If a caller did construct one,
`pydantic.v1.fields` is already in `sys.modules` and isinstance is safe.
"""
mod = sys.modules.get("pydantic.v1.fields")
if mod is None:
return False
return isinstance(obj, mod.FieldInfo)
def _build_deprecation_message(
*,
@@ -59,7 +78,9 @@ class LangChainPendingDeprecationWarning(PendingDeprecationWarning):
# PUBLIC API
# Last Any should be FieldInfoV1 but this leads to circular imports
# Bound is `Any` (not `FieldInfoV1`) because importing `pydantic.v1` at module
# scope emits a `UserWarning` on Python 3.14+; v1 `FieldInfo` support is handled
# at runtime via `_is_pydantic_v1_field_info`.
T = TypeVar("T", bound=type | Callable[..., Any] | Any)
@@ -246,7 +267,7 @@ def deprecated(
)
return obj
elif isinstance(obj, FieldInfoV1):
elif _is_pydantic_v1_field_info(obj):
wrapped = None
if not _obj_type:
_obj_type = "attribute"
@@ -256,6 +277,8 @@ def deprecated(
old_doc = obj.description
def finalize(_: Callable[..., Any], new_doc: str, /) -> T:
from pydantic.v1.fields import FieldInfo as FieldInfoV1 # noqa: PLC0415
return cast(
"T",
FieldInfoV1(