diff --git a/libs/langgraph/langgraph/_internal/_runnable.py b/libs/langgraph/langgraph/_internal/_runnable.py index 9040c9b5f..82db24e92 100644 --- a/libs/langgraph/langgraph/_internal/_runnable.py +++ b/libs/langgraph/langgraph/_internal/_runnable.py @@ -4,6 +4,7 @@ import asyncio import enum import inspect import sys +import warnings from collections.abc import ( AsyncIterator, Awaitable, @@ -303,6 +304,16 @@ class RunnableCallable(Runnable): if typ != (ANY_TYPE,) and p.annotation not in typ: # A specific type is required, but the function annotation does # not match the expected type. + + # If this is a config parameter with incorrect typing, emit a warning + # because we used to support any type but are moving towards more correct typing + if kw == "config" and p.annotation != inspect.Parameter.empty: + warnings.warn( + f"The 'config' parameter should be typed as 'RunnableConfig' or " + f"'RunnableConfig | None', not '{p.annotation}'. ", + UserWarning, + stacklevel=4, + ) continue # If the kwarg is accepted by the function, store the key / runtime attribute to inject diff --git a/libs/langgraph/tests/test_deprecation.py b/libs/langgraph/tests/test_deprecation.py index b8483327e..a53099dc6 100644 --- a/libs/langgraph/tests/test_deprecation.py +++ b/libs/langgraph/tests/test_deprecation.py @@ -1,3 +1,8 @@ +from __future__ import annotations + +import warnings +from typing import Any, Optional + import pytest from langchain_core.runnables import RunnableConfig from pytest_mock import MockerFixture @@ -188,7 +193,9 @@ def test_deprecated_import() -> None: from langgraph.constants import PREVIOUS # noqa: F401 -@pytest.mark.filterwarnings("ignore:`checkpoint_during` is deprecated") +@pytest.mark.filterwarnings( + "ignore:`durability` has no effect when no checkpointer is present" +) def test_checkpoint_during_deprecation_state_graph() -> None: class CheckDurability(TypedDict): durability: NotRequired[str] @@ -228,3 +235,100 @@ def test_checkpoint_during_deprecation_state_graph() -> None: ): for chunk in graph.stream({}, checkpoint_during=False): # type: ignore[arg-type] assert chunk["plain_node"]["durability"] == "exit" + + +def test_config_parameter_incorrect_typing() -> None: + """Test that a warning is raised when config parameter is typed incorrectly.""" + builder = StateGraph(PlainState) + + # Test sync function with config: dict + with pytest.warns( + UserWarning, + match="The 'config' parameter should be typed as 'RunnableConfig' or 'RunnableConfig | None', not '.*dict.*'. ", + ): + + def sync_node_with_dict_config(state: PlainState, config: dict) -> PlainState: + return state + + builder.add_node(sync_node_with_dict_config) + + # Test async function with config: dict + with pytest.warns( + UserWarning, + match="The 'config' parameter should be typed as 'RunnableConfig' or 'RunnableConfig | None', not '.*dict.*'. ", + ): + + async def async_node_with_dict_config( + state: PlainState, config: dict + ) -> PlainState: + return state + + builder.add_node(async_node_with_dict_config) + + # Test with other incorrect types + with pytest.warns( + UserWarning, + match="The 'config' parameter should be typed as 'RunnableConfig' or 'RunnableConfig | None', not '.*Any.*'. ", + ): + + def sync_node_with_any_config(state: PlainState, config: Any) -> PlainState: + return state + + builder.add_node(sync_node_with_any_config) + + with pytest.warns( + UserWarning, + match="The 'config' parameter should be typed as 'RunnableConfig' or 'RunnableConfig | None', not '.*Any.*'. ", + ): + + async def async_node_with_any_config( + state: PlainState, config: Any + ) -> PlainState: + return state + + builder.add_node(async_node_with_any_config) + + with warnings.catch_warnings(record=True) as w: + + def node_with_correct_config( + state: PlainState, config: RunnableConfig + ) -> PlainState: + return state + + builder.add_node(node_with_correct_config) + + def node_with_optional_config( + state: PlainState, + config: Optional[RunnableConfig], # noqa: UP045 + ) -> PlainState: + return state + + builder.add_node(node_with_optional_config) + + def node_with_untyped_config(state: PlainState, config) -> PlainState: + return state + + builder.add_node(node_with_untyped_config) + + async def async_node_with_correct_config( + state: PlainState, config: RunnableConfig + ) -> PlainState: + return state + + builder.add_node(async_node_with_correct_config) + + async def async_node_with_optional_config( + state: PlainState, + config: Optional[RunnableConfig], # noqa: UP045 + ) -> PlainState: + return state + + builder.add_node(async_node_with_optional_config) + + async def async_node_with_untyped_config( + state: PlainState, config + ) -> PlainState: + return state + + builder.add_node(async_node_with_untyped_config) + assert len(w) == 0