Files
wehub-resource-sync a7d6d88f6f
CI / changes (push) Has been cancelled
CI / cd libs/checkpoint (push) Has been cancelled
CI / cd libs/checkpoint-conformance (push) Has been cancelled
CI / cd libs/checkpoint-postgres (push) Has been cancelled
CI / cd libs/checkpoint-sqlite (push) Has been cancelled
CI / cd libs/cli (push) Has been cancelled
CI / cd libs/prebuilt (push) Has been cancelled
CI / cd libs/sdk-py (push) Has been cancelled
CI / cd libs/langgraph (push) Has been cancelled
CI / Check SDK methods matching (push) Has been cancelled
CI / Check CLI schema hasn't changed #3.13 (push) Has been cancelled
CI / CLI integration test (push) Has been cancelled
CI / sdk-py integration test (push) Has been cancelled
CI / CI Success (push) Has been cancelled
baseline / benchmark (push) Has been cancelled
Deploy Redirects to GitHub Pages / deploy (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:37:18 +08:00

363 lines
11 KiB
Python

import datetime
import decimal
import ipaddress
import pathlib
import re
import sys
import uuid
from enum import Enum
from typing import Annotated, Literal, Optional
from langgraph.checkpoint.base import BaseCheckpointSaver
from pydantic import (
BaseModel,
ByteSize,
Field,
SecretStr,
confloat,
conint,
conlist,
constr,
field_validator,
model_validator,
)
from langgraph._internal._pydantic import is_supported_by_pydantic
from langgraph.constants import END, START
from langgraph.func import entrypoint, task
from langgraph.graph.state import StateGraph
from langgraph.types import Command, Interrupt, interrupt
from tests.any_str import AnyStr
def test_is_supported_by_pydantic() -> None:
"""Test if types are supported by pydantic."""
import typing
import pydantic
import typing_extensions
class TypedDictExtensions(typing_extensions.TypedDict):
x: int
assert is_supported_by_pydantic(TypedDictExtensions) is True
class VanillaClass:
x: int
assert is_supported_by_pydantic(VanillaClass) is False
class BuiltinTypedDict(typing.TypedDict): # noqa: TID251
x: int
if sys.version_info >= (3, 12):
assert is_supported_by_pydantic(BuiltinTypedDict) is True
else:
assert is_supported_by_pydantic(BuiltinTypedDict) is False
class PydanticModel(pydantic.BaseModel):
x: int
assert is_supported_by_pydantic(PydanticModel) is True
def test_nested_pydantic_models() -> None:
"""Test that nested Pydantic models are properly constructed from leaf nodes up."""
class NestedModel(BaseModel):
value: int
name: str
# For constrained types
PositiveInt = Annotated[int, Field(gt=0)]
NonNegativeFloat = Annotated[float, Field(ge=0)]
# Enum type
class UserRole(Enum):
ADMIN = "admin"
USER = "user"
GUEST = "guest"
# Forward reference model
class RecursiveModel(BaseModel):
value: str
child: Optional["RecursiveModel"] = None
# Discriminated union models
class Cat(BaseModel):
pet_type: Literal["cat"]
meow: str
class Dog(BaseModel):
pet_type: Literal["dog"]
bark: str
# Cyclic reference model
class Person(BaseModel):
id: str
name: str
friends: list[str] = Field(default_factory=list) # IDs of friends
conlist_type = conlist(item_type=int, min_length=2, max_length=5)
class State(BaseModel):
# Basic nested model tests
top_level: str
auuid: uuid.UUID
nested: NestedModel
optional_nested: Annotated[NestedModel | None, lambda x, y: y, "Foo"]
dict_nested: dict[str, NestedModel]
simple_str_list: list[str]
list_nested: Annotated[
dict | list[dict[str, NestedModel]], lambda x, y: (x or []) + [y]
]
tuple_nested: tuple[str, NestedModel]
tuple_list_nested: list[tuple[int, NestedModel]]
complex_tuple: tuple[str, dict[str, tuple[int, NestedModel]]]
# Forward reference test
recursive: RecursiveModel
# Discriminated union test
pet: Cat | Dog
# Cyclic reference test
people: dict[str, Person] # Map of ID -> Person
# Rich type adapters
ip_address: ipaddress.IPv4Address
ip_address_v6: ipaddress.IPv6Address
amount: decimal.Decimal
file_path: pathlib.Path
timestamp: datetime.datetime
date_only: datetime.date
time_only: datetime.time
duration: datetime.timedelta
immutable_set: frozenset[int]
binary_data: bytes
pattern: re.Pattern
secret: SecretStr
file_size: ByteSize
# Constrained types
positive_value: PositiveInt
non_negative: NonNegativeFloat
limited_string: constr(min_length=3, max_length=10)
bounded_int: conint(ge=10, le=100)
restricted_float: confloat(gt=0, lt=1)
required_list: conlist_type
# Enum & Literal
role: UserRole
status: Literal["active", "inactive", "pending"]
# Annotated & NewType
validated_age: Annotated[int, Field(gt=0, lt=120)]
# Generic containers with validators
decimal_list: list[decimal.Decimal]
id_tuple: tuple[uuid.UUID, uuid.UUID]
inputs = {
# Basic nested models
"top_level": "initial",
"auuid": str(uuid.uuid4()),
"nested": {"value": 42, "name": "test"},
"optional_nested": {"value": 10, "name": "optional"},
"dict_nested": {"a": {"value": 5, "name": "a"}},
"list_nested": [{"a": {"value": 6, "name": "b"}}],
"tuple_nested": ["tuple-key", {"value": 7, "name": "tuple-value"}],
"tuple_list_nested": [[1, {"value": 8, "name": "tuple-in-list"}]],
"simple_str_list": ["siss", "boom", "bah"],
"complex_tuple": [
"complex",
{"nested": [9, {"value": 10, "name": "deep"}]},
],
# Forward reference
"recursive": {"value": "parent", "child": {"value": "child", "child": None}},
# Discriminated union (using a cat in this case)
"pet": {"pet_type": "cat", "meow": "meow!"},
# Cyclic references
"people": {
"1": {
"id": "1",
"name": "Alice",
"friends": ["2", "3"], # Alice is friends with Bob and Charlie
},
"2": {
"id": "2",
"name": "Bob",
"friends": ["1"], # Bob is friends with Alice
},
"3": {
"id": "3",
"name": "Charlie",
"friends": ["1", "2"], # Charlie is friends with Alice and Bob
},
},
# Rich type adapters
"ip_address": "192.168.1.1",
"ip_address_v6": "2001:db8::1",
"amount": "123.45",
"file_path": "/tmp/test.txt",
"timestamp": "2025-04-07T10:58:04",
"date_only": "2025-04-07",
"time_only": "10:58:04",
"duration": 3600, # seconds
"immutable_set": [1, 2, 3, 4],
"binary_data": b"hello world",
"pattern": "^test$",
"secret": "password123",
"file_size": 1024,
# Constrained types
"positive_value": 42,
"non_negative": 0.0,
"limited_string": "test",
"bounded_int": 50,
"restricted_float": 0.5,
"required_list": [10, 20, 30],
# Enum & Literal
"role": "admin",
"status": "active",
# Annotated & NewType
"validated_age": 30,
# Generic containers with validators
"decimal_list": ["10.5", "20.75", "30.25"],
"id_tuple": [str(uuid.uuid4()), str(uuid.uuid4())],
}
update = {"top_level": "updated", "nested": {"value": 100, "name": "updated"}}
expected = State(**inputs)
def node_fn(state: State) -> dict:
# Basic assertions
assert isinstance(state.auuid, uuid.UUID)
assert state == expected
# Rich type assertions
assert isinstance(state.ip_address, ipaddress.IPv4Address)
assert isinstance(state.ip_address_v6, ipaddress.IPv6Address)
assert isinstance(state.amount, decimal.Decimal)
assert isinstance(state.file_path, pathlib.Path)
assert isinstance(state.timestamp, datetime.datetime)
assert isinstance(state.date_only, datetime.date)
assert isinstance(state.time_only, datetime.time)
assert isinstance(state.duration, datetime.timedelta)
assert isinstance(state.immutable_set, frozenset)
assert isinstance(state.binary_data, bytes)
assert isinstance(state.pattern, re.Pattern)
# Constrained types
assert state.positive_value > 0
assert state.non_negative >= 0
assert 3 <= len(state.limited_string) <= 10
assert 10 <= state.bounded_int <= 100
assert 0 < state.restricted_float < 1
assert 2 <= len(state.required_list) <= 5
# Enum & Literal
assert state.role == UserRole.ADMIN
assert state.status == "active"
# Annotated
assert 0 < state.validated_age < 120
# Generic containers
assert len(state.decimal_list) == 3
assert len(state.id_tuple) == 2
return update
builder = StateGraph(State)
builder.add_node("process", node_fn)
builder.set_entry_point("process")
builder.set_finish_point("process")
graph = builder.compile()
result = graph.invoke(inputs.copy())
assert result == {**inputs, **update}
new_inputs = inputs.copy()
new_inputs["list_nested"] = {"foo": "bar"}
expected = State(**new_inputs)
assert {**new_inputs, **update} == graph.invoke(new_inputs.copy())
def test_pydantic_state_field_validator():
class State(BaseModel):
name: str
text: str = ""
only_root: int = 13
@field_validator("name", mode="after")
@classmethod
def validate_name(cls, value):
if value[0].islower():
raise ValueError("Name must start with a capital letter")
return "Validated " + value
@model_validator(mode="before")
@classmethod
def validate_amodel(cls, values: "State"):
return values | {"only_root": 392}
input_state = {"name": "John"}
def process_node(state: State):
assert State.model_validate(input_state) == state
return {"text": "Hello, " + state.name + "!"}
builder = StateGraph(state_schema=State)
builder.add_node("process", process_node)
builder.add_edge(START, "process")
builder.add_edge("process", END)
g = builder.compile()
res = g.invoke(input_state)
assert res["text"] == "Hello, Validated John!"
class FunctionalState(BaseModel):
a: str
b: str | None = None
def test_interrupt_functional_pydantic(sync_checkpointer: BaseCheckpointSaver) -> None:
called_count = 0
@task
def foo(state: FunctionalState) -> FunctionalState:
nonlocal called_count
called_count += 1
return FunctionalState(**{"a": state.a + "foo"})
@task
def bar(state: FunctionalState) -> dict:
return {"a": state.a + "bar", "b": state.b}
@entrypoint(checkpointer=sync_checkpointer)
def graph(inputs: FunctionalState) -> FunctionalState:
fut_foo = foo(inputs)
value = interrupt("Provide value for bar:")
foo_res = fut_foo.result()
assert isinstance(foo_res, FunctionalState)
bar_input = FunctionalState(a=foo_res.a, b=value)
fut_bar = bar(bar_input)
return fut_bar.result()
config = {"configurable": {"thread_id": "1"}}
# First run, interrupted at bar
assert graph.invoke(FunctionalState(a=""), config) == {
"__interrupt__": [
Interrupt(
value="Provide value for bar:",
id=AnyStr(),
)
]
}
# Resume with an answer
res = graph.invoke(Command(resume="bar"), config)
assert res == {"a": "foobar", "b": "bar"}
assert called_count == 1