Compare commits

...
1 Commits
Author SHA1 Message Date
11ee185999 fix(checkpoint): widen Store put value type to Mapping[str, Any] (#8617)
TypedDict values don't structurally satisfy dict[str, Any] since dict
implies full mutability. Mapping[str, Any] accepts both plain dicts and
TypedDicts while still requiring string keys, matching what put()
actually needs from callers.

Fixes #8616

Verified by running lint/type/test locally across checkpoint,
checkpoint-sqlite, checkpoint-postgres, prebuilt, sdk-py, and a scoped
langgraph subset.

LinkedIn: https://linkedin.com/in/lisandro-navarra

---------

Co-authored-by: Mason Daugherty <github@mdrxy.com>
2026-08-28 08:05:04 -05:00
8 changed files with 35 additions and 17 deletions
+1
View File
@@ -76,6 +76,7 @@ __pypackages__/
# Environments
.env
.env.*
.envrc
*.crt
*.key
@@ -7,7 +7,7 @@ import logging
import re
import threading
from collections import defaultdict
from collections.abc import Callable, Iterable, Iterator, Sequence
from collections.abc import Callable, Iterable, Iterator, Mapping, Sequence
from contextlib import contextmanager
from datetime import datetime
from typing import (
@@ -354,7 +354,7 @@ class BasePostgresStore(Generic[C]):
(
_namespace_to_text(op.namespace),
op.key,
Jsonb(cast(dict, op.value)),
Jsonb(dict(cast(Mapping[str, Any], op.value))),
)
)
if op.ttl is not None:
@@ -7,7 +7,7 @@ import re
import sqlite3
import threading
from collections import defaultdict
from collections.abc import Callable, Iterable, Iterator, Sequence
from collections.abc import Callable, Iterable, Iterator, Mapping, Sequence
from contextlib import contextmanager
from typing import Any, Literal, NamedTuple, cast
@@ -387,7 +387,7 @@ class BaseSqliteStore:
[
_namespace_to_text(op.namespace),
op.key,
orjson.dumps(cast(dict, op.value)),
orjson.dumps(dict(cast(Mapping[str, Any], op.value))),
expires_at,
op.ttl,
]
@@ -12,7 +12,7 @@ Core types:
from __future__ import annotations
from abc import ABC, abstractmethod
from collections.abc import Iterable
from collections.abc import Iterable, Mapping
from datetime import datetime
from typing import (
Any,
@@ -473,10 +473,10 @@ class PutOp(NamedTuple):
the full path would effectively be `"documents/user123/report1"`
"""
value: dict[str, Any] | None
value: Mapping[str, Any] | None
"""The data to store, or `None` to mark the item for deletion.
The value must be a dictionary with string keys and JSON-serializable values.
The value must be a mapping with string keys and JSON-serializable values.
Setting this to `None` signals that the item should be deleted.
Example:
@@ -857,7 +857,7 @@ class BaseStore(ABC):
self,
namespace: tuple[str, ...],
key: str,
value: dict[str, Any],
value: Mapping[str, Any],
index: Literal[False] | list[str] | None = None,
*,
ttl: float | None | NotProvided = NOT_PROVIDED,
@@ -869,7 +869,7 @@ class BaseStore(ABC):
Example: `("documents", "user123")`
key: Unique identifier within the namespace. Together with namespace forms
the complete path to the item.
value: Dictionary containing the item's data. Must contain string keys
value: Mapping containing the item's data. Must contain string keys
and JSON-serializable values.
index: Controls how the item's fields are indexed for search:
@@ -1110,7 +1110,7 @@ class BaseStore(ABC):
self,
namespace: tuple[str, ...],
key: str,
value: dict[str, Any],
value: Mapping[str, Any],
index: Literal[False] | list[str] | None = None,
*,
ttl: float | None | NotProvided = NOT_PROVIDED,
@@ -1122,7 +1122,7 @@ class BaseStore(ABC):
Example: `("documents", "user123")`
key: Unique identifier within the namespace. Together with namespace forms
the complete path to the item.
value: Dictionary containing the item's data. Must contain string keys
value: Mapping containing the item's data. Must contain string keys
and JSON-serializable values.
index: Controls how the item's fields are indexed for search:
@@ -5,7 +5,7 @@ from __future__ import annotations
import asyncio
import functools
import weakref
from collections.abc import Callable, Iterable
from collections.abc import Callable, Iterable, Mapping
from typing import Any, Literal, TypeVar
from langgraph.store.base import (
@@ -132,7 +132,7 @@ class AsyncBatchedBaseStore(BaseStore):
self,
namespace: tuple[str, ...],
key: str,
value: dict[str, Any],
value: Mapping[str, Any],
index: Literal[False] | list[str] | None = None,
*,
ttl: float | None | NotProvided = NOT_PROVIDED,
@@ -231,7 +231,7 @@ class AsyncBatchedBaseStore(BaseStore):
self,
namespace: tuple[str, ...],
key: str,
value: dict[str, Any],
value: Mapping[str, Any],
index: Literal[False] | list[str] | None = None,
*,
ttl: float | None | NotProvided = NOT_PROVIDED,
@@ -11,7 +11,7 @@ from __future__ import annotations
import asyncio
import functools
import json
from collections.abc import Awaitable, Callable, Sequence
from collections.abc import Awaitable, Callable, Mapping, Sequence
from typing import Any
from langchain_core.embeddings import Embeddings
@@ -244,6 +244,9 @@ def get_text_at_path(obj: Any, path: str | list[str]) -> list[str]:
- Multi-field selection: "{field1,field2}"
- Nested paths in multi-field: "{field1,nested.field2}"
"""
if isinstance(obj, Mapping) and not isinstance(obj, dict):
obj = dict(obj)
if not path or path == "$":
return [json.dumps(obj, sort_keys=True, ensure_ascii=False)]
@@ -408,7 +408,7 @@ class InMemoryStore(BaseStore):
self._vectors[namespace].pop(key, None)
else:
self._data[namespace][key] = Item(
value=op.value,
value=dict(op.value),
key=key,
namespace=namespace,
created_at=datetime.now(timezone.utc),
+15 -1
View File
@@ -1,7 +1,9 @@
import asyncio
import json
from collections.abc import Iterable
from collections import UserDict
from collections.abc import Iterable, Mapping
from datetime import datetime
from types import MappingProxyType
from typing import Any
import pytest
@@ -137,6 +139,18 @@ def test_get_text_at_path() -> None:
assert get_text_at_path(nested_data, "nested[{invalid}]") == []
@pytest.mark.parametrize(
"mapping",
[
UserDict({"text": "searchable"}),
MappingProxyType({"text": "searchable"}),
],
)
def test_get_text_at_path_with_non_dict_mapping(mapping: Mapping[str, str]) -> None:
assert get_text_at_path(mapping, "$") == ['{"text": "searchable"}']
assert get_text_at_path(mapping, "text") == ["searchable"]
async def test_async_batch_store(mocker: MockerFixture) -> None:
abatch = mocker.stub()