Skip to content

Commit 42fcb96

Browse files
committed
fix(core): support for Python 3.14
1 parent 760fc3b commit 42fcb96

9 files changed

Lines changed: 72 additions & 29 deletions

File tree

libs/core/langchain_core/language_models/llms.py

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -91,13 +91,17 @@ def _before_sleep(retry_state: RetryCallState) -> None:
9191
if isinstance(run_manager, AsyncCallbackManagerForLLMRun):
9292
coro = run_manager.on_retry(retry_state)
9393
try:
94-
loop = asyncio.get_event_loop()
95-
if loop.is_running():
96-
# TODO: Fix RUF006 - this task should have a reference
97-
# and be awaited somewhere
98-
loop.create_task(coro) # noqa: RUF006
99-
else:
94+
try:
95+
loop = asyncio.get_event_loop()
96+
except RuntimeError:
10097
asyncio.run(coro)
98+
else:
99+
if loop.is_running():
100+
# TODO: Fix RUF006 - this task should have a reference
101+
# and be awaited somewhere
102+
loop.create_task(coro) # noqa: RUF006
103+
else:
104+
asyncio.run(coro)
101105
except Exception as e:
102106
_log_error_once(f"Error in on_retry: {e}")
103107
else:

libs/core/langchain_core/runnables/utils.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import ast
66
import asyncio
77
import inspect
8+
import sys
89
import textwrap
910
from collections.abc import Callable, Mapping, Sequence
1011
from contextvars import Context
@@ -141,10 +142,10 @@ def coro_with_context(
141142
Returns:
142143
The coroutine with the context.
143144
"""
144-
if asyncio_accepts_context():
145-
return asyncio.create_task(coro, context=context) # type: ignore[arg-type,call-arg,unused-ignore]
145+
if sys.version_info >= (3, 11):
146+
return asyncio.create_task(coro, context=context) # type: ignore[arg-type]
146147
if create_task:
147-
return asyncio.create_task(coro) # type: ignore[arg-type]
148+
return asyncio.create_task(coro) # type: ignore[arg-type, unused-ignore]
148149
return coro
149150

150151

libs/core/langchain_core/tracers/event_stream.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,10 @@ def __init__(
128128
exclude_tags=exclude_tags,
129129
)
130130

131-
loop = asyncio.get_event_loop()
131+
try:
132+
loop = asyncio.get_event_loop()
133+
except RuntimeError:
134+
loop = asyncio.new_event_loop()
132135
memory_stream = _MemoryStream[StreamEvent](loop)
133136
self.send_stream = memory_stream.get_send_stream()
134137
self.receive_stream = memory_stream.get_receive_stream()

libs/core/langchain_core/tracers/log_stream.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -264,7 +264,10 @@ def __init__(
264264
self.exclude_types = exclude_types
265265
self.exclude_tags = exclude_tags
266266

267-
loop = asyncio.get_event_loop()
267+
try:
268+
loop = asyncio.get_event_loop()
269+
except RuntimeError:
270+
loop = asyncio.new_event_loop()
268271
memory_stream = _MemoryStream[RunLogPatch](loop)
269272
self.lock = threading.Lock()
270273
self.send_stream = memory_stream.get_send_stream()

libs/core/tests/unit_tests/output_parsers/test_pydantic_parser.py

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"""Test PydanticOutputParser."""
22

3+
import sys
34
from enum import Enum
45
from typing import Literal
56

@@ -22,15 +23,23 @@ class ForecastV2(pydantic.BaseModel):
2223
forecast: str
2324

2425

25-
class ForecastV1(V1BaseModel):
26-
temperature: int
27-
f_or_c: Literal["F", "C"]
28-
forecast: str
26+
_FORECAST_MODELS: list[type[BaseModel | V1BaseModel]] = [ForecastV2]
27+
_FORECAST_MODELS_TYPES = type[ForecastV2]
28+
29+
if sys.version_info < (3, 14):
30+
31+
class ForecastV1(V1BaseModel):
32+
temperature: int
33+
f_or_c: Literal["F", "C"]
34+
forecast: str
35+
36+
_FORECAST_MODELS_TYPES |= type[ForecastV1]
37+
_FORECAST_MODELS.append(ForecastV1)
2938

3039

31-
@pytest.mark.parametrize("pydantic_object", [ForecastV2, ForecastV1])
40+
@pytest.mark.parametrize("pydantic_object", _FORECAST_MODELS)
3241
def test_pydantic_parser_chaining(
33-
pydantic_object: type[ForecastV2] | type[ForecastV1],
42+
pydantic_object: _FORECAST_MODELS_TYPES,
3443
) -> None:
3544
prompt = PromptTemplate(
3645
template="""{{
@@ -53,7 +62,7 @@ def test_pydantic_parser_chaining(
5362
assert res.forecast == "Sunny"
5463

5564

56-
@pytest.mark.parametrize("pydantic_object", [ForecastV2, ForecastV1])
65+
@pytest.mark.parametrize("pydantic_object", _FORECAST_MODELS)
5766
def test_pydantic_parser_validation(pydantic_object: TBaseModel) -> None:
5867
bad_prompt = PromptTemplate(
5968
template="""{{
@@ -75,7 +84,7 @@ def test_pydantic_parser_validation(pydantic_object: TBaseModel) -> None:
7584

7685

7786
# JSON output parser tests
78-
@pytest.mark.parametrize("pydantic_object", [ForecastV2, ForecastV1])
87+
@pytest.mark.parametrize("pydantic_object", _FORECAST_MODELS)
7988
def test_json_parser_chaining(
8089
pydantic_object: TBaseModel,
8190
) -> None:

libs/core/tests/unit_tests/test_tools.py

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -105,14 +105,6 @@ class _MockSchema(BaseModel):
105105
arg3: dict | None = None
106106

107107

108-
class _MockSchemaV1(BaseModelV1):
109-
"""Return the arguments directly."""
110-
111-
arg1: int
112-
arg2: bool
113-
arg3: dict | None = None
114-
115-
116108
class _MockStructuredTool(BaseTool):
117109
name: str = "structured_api"
118110
args_schema: type[BaseModel] = _MockSchema
@@ -206,6 +198,20 @@ def tool_func(*, arg1: int, arg2: bool, arg3: dict | None = None) -> str:
206198
assert isinstance(tool_func, BaseTool)
207199
assert tool_func.args_schema == _MockSchema
208200

201+
202+
@pytest.mark.skipif(
203+
sys.version_info >= (3, 14), reason="Pydantic v1 not supported with Python 3.14+"
204+
)
205+
def test_decorator_with_specified_schema_pydantic_v1() -> None:
206+
"""Test that manually specified schemata are passed through to the tool."""
207+
208+
class _MockSchemaV1(BaseModelV1):
209+
"""Return the arguments directly."""
210+
211+
arg1: int
212+
arg2: bool
213+
arg3: dict | None = None
214+
209215
@tool(args_schema=cast("ArgsSchema", _MockSchemaV1))
210216
def tool_func_v1(*, arg1: int, arg2: bool, arg3: dict | None = None) -> str:
211217
return f"{arg1} {arg2} {arg3}"
@@ -348,6 +354,9 @@ def structured_tool(
348354
assert result == expected
349355

350356

357+
@pytest.mark.skipif(
358+
sys.version_info >= (3, 14), reason="Pydantic v1 not supported with Python 3.14+"
359+
)
351360
def test_structured_tool_types_parsed_pydantic_v1() -> None:
352361
"""Test the non-primitive types are correctly passed to structured tools."""
353362

@@ -1880,7 +1889,10 @@ class FooV1Namespace(BaseModelV1):
18801889
# behave well with either pydantic 1 proper,
18811890
# pydantic v1 from pydantic 2,
18821891
# or pydantic 2 proper.
1883-
TEST_MODELS = generate_models() + generate_backwards_compatible_v1()
1892+
TEST_MODELS = generate_models()
1893+
1894+
if sys.version_info < (3, 14):
1895+
TEST_MODELS += generate_backwards_compatible_v1()
18841896

18851897

18861898
@pytest.mark.parametrize("pydantic_model", TEST_MODELS)
@@ -2079,6 +2091,8 @@ def test__get_all_basemodel_annotations_v2(*, use_v1_namespace: bool) -> None:
20792091
A = TypeVar("A")
20802092

20812093
if use_v1_namespace:
2094+
if sys.version_info >= (3, 14):
2095+
pytest.skip("Pydantic v1 is not supported with Python 3.14+")
20822096

20832097
class ModelA(BaseModelV1, Generic[A], extra="allow"):
20842098
a: A

libs/core/tests/unit_tests/tracers/test_memory_stream.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ def test_send_to_closed_stream() -> None:
120120
121121
We may want to handle this in a better way in the future.
122122
"""
123-
event_loop = asyncio.get_event_loop()
123+
event_loop = asyncio.get_event_loop_policy().new_event_loop()
124124
channel = _MemoryStream[str](event_loop)
125125
writer = channel.get_send_stream()
126126
# send with an open even loop

libs/core/tests/unit_tests/utils/test_pydantic.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
"""Test for some custom pydantic decorators."""
22

3+
import sys
34
import warnings
45
from typing import Any
56

7+
import pytest
68
from pydantic import BaseModel, ConfigDict, Field
79
from pydantic.v1 import BaseModel as BaseModelV1
810

@@ -139,6 +141,9 @@ class Foo(BaseModel):
139141
assert fields == {"x": Foo.model_fields["x"]}
140142

141143

144+
@pytest.mark.skipif(
145+
sys.version_info >= (3, 14), reason="Pydantic v1 not supported with Python 3.14+"
146+
)
142147
def test_fields_pydantic_v1_from_2() -> None:
143148
class Foo(BaseModelV1):
144149
x: int

libs/core/tests/unit_tests/utils/test_utils.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import os
22
import re
3+
import sys
34
from collections.abc import Callable
45
from contextlib import AbstractContextManager, nullcontext
56
from copy import deepcopy
@@ -214,6 +215,9 @@ def test_guard_import_failure(
214215
guard_import(module_name, pip_name=pip_name, package=package)
215216

216217

218+
@pytest.mark.skipif(
219+
sys.version_info >= (3, 14), reason="Pydantic v1 not supported with Python 3.14+"
220+
)
217221
def test_get_pydantic_field_names_v1_in_2() -> None:
218222
class PydanticV1Model(PydanticV1BaseModel):
219223
field1: str

0 commit comments

Comments
 (0)