fix(langgraph): Add warning for incorrect node signature with mistyped config param (#5798)

Fixes: #5787

Ensures that if `config` is not typed as one of `RunanbleConfig` or
`Optional[RunnableConfig]` a warning is raised to help developers avoid
unexpected results at invocation time.

---------

Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
Co-authored-by: Sydney Runkle <sydneymarierunkle@gmail.com>
This commit is contained in:
open-swe[bot]
2025-08-01 17:25:14 +00:00
committed by GitHub
co-authored by open-swe[bot] <open-swe@users.noreply.github.com> Sydney Runkle
parent 5152a96fce
commit 69dd20e523
2 changed files with 116 additions and 1 deletions
@@ -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
+105 -1
View File
@@ -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