From 658541c4960f329864a2523fc7d52427e8190bed Mon Sep 17 00:00:00 2001 From: Elior Nataf Lackritz Date: Wed, 5 Aug 2026 21:23:28 -0400 Subject: [PATCH] chore(checkpoint-postgres,checkpoint-sqlite): enable PLC0415 lint rule (#8540) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follow-up to review on #8537: turn on ruff's `PLC0415` (`import-outside-top-level`) so deferred imports in tests stop accumulating. Scoped to `checkpoint-postgres` and `checkpoint-sqlite` rather than repo-wide, because the sweep turns up three different things and only one of them is a style problem. ### What the rule finds today ``` package tests src files checkpoint 13 10 11 checkpoint-conformance 0 10 4 checkpoint-postgres 6 0 2 checkpoint-sqlite 9 0 3 langgraph 130 23 32 prebuilt 14 3 7 cli 9 14 10 sdk-py 189 23 38 ──────────────────── 370 83 107 ``` 453 violations across 107 files, and ruff has no autofix for this rule. ### Three categories, not one **Style — hoist.** `checkpoint-sqlite/tests/test_store.py` deferred `math`, `random`, `time`, `Counter` and `defaultdict` inside methods for no reason. **Deliberate — keep, annotate.** `checkpoint-postgres/tests/test_async.py` defers behind `pytest.importorskip("langgraph.channels.delta")` because langgraph core is *not* a test dependency of that package. Hoisting would break the skip. Those get `# noqa: PLC0415` and a comment. **Redundant guard — hoist.** `checkpoint-sqlite/tests/test_conformance_delta.py` deferred imports only to get past its own `importorskip`. Imports move up; the `aiosqlite` guard stays, since that dependency genuinely can be absent. The second category is why I did not enable this everywhere in one go. Most of the 83 source-level violations look like the same pattern — optional-dependency handling and circular-import avoidance in `jsonplus.py`, `embed.py`, `encrypted.py` and friends. Blanket-enabling would mean `# noqa` on a lot of correct code, and each one wants an owner's eye rather than a mechanical pass. These two packages are clean to enforce today because both have **zero** source-level violations. ### Suggested rollout for the rest Either extend package by package as owners confirm which deferrals are intentional, or enable everywhere at once with `per-file-ignores` grandfathering the current 107 files so new code is blocked immediately and the debt burns down. Happy to do either — the second is a smaller diff but leaves a long ignore list. ### Verified `checkpoint-sqlite` 118 passed, `checkpoint-postgres` 264 passed on PG 15 and 16, `make lint` clean in both. One overlap worth flagging: `checkpoint-sqlite/tests/test_conformance_delta.py` is also touched by #8537. The change is identical in both, so it should merge cleanly either way. --- libs/checkpoint-postgres/pyproject.toml | 1 + libs/checkpoint-postgres/tests/test_async.py | 14 ++++++++------ libs/checkpoint-sqlite/pyproject.toml | 1 + .../tests/test_conformance_delta.py | 10 +++++----- libs/checkpoint-sqlite/tests/test_store.py | 13 ++++--------- 5 files changed, 19 insertions(+), 20 deletions(-) diff --git a/libs/checkpoint-postgres/pyproject.toml b/libs/checkpoint-postgres/pyproject.toml index 180166b86..864ba2c00 100644 --- a/libs/checkpoint-postgres/pyproject.toml +++ b/libs/checkpoint-postgres/pyproject.toml @@ -64,6 +64,7 @@ lint.select = [ "UP", # pyupgrade "B", # flake8-bugbear "I", # isort + "PLC0415", # import-outside-top-level "UP", # pyupgrade ] lint.ignore = ["E501", "B008"] diff --git a/libs/checkpoint-postgres/tests/test_async.py b/libs/checkpoint-postgres/tests/test_async.py index fd42146cf..ec941da90 100644 --- a/libs/checkpoint-postgres/tests/test_async.py +++ b/libs/checkpoint-postgres/tests/test_async.py @@ -380,13 +380,15 @@ async def test_delta_channel_chain_reconstruction(saver_name: str) -> None: "langgraph.channels.delta", reason="langgraph core not installed" ) - from typing import Annotated + # Deferred on purpose: langgraph core is not a test dependency of this + # package, so these must stay behind the importorskip above. + from typing import Annotated # noqa: PLC0415 - from langchain_core.messages import AIMessage, HumanMessage - from langgraph.channels.delta import DeltaChannel - from langgraph.graph import START, StateGraph - from langgraph.graph.message import _messages_delta_reducer - from typing_extensions import TypedDict + from langchain_core.messages import AIMessage, HumanMessage # noqa: PLC0415 + from langgraph.channels.delta import DeltaChannel # noqa: PLC0415 + from langgraph.graph import START, StateGraph # noqa: PLC0415 + from langgraph.graph.message import _messages_delta_reducer # noqa: PLC0415 + from typing_extensions import TypedDict # noqa: PLC0415 class State(TypedDict): messages: Annotated[list, DeltaChannel(_messages_delta_reducer)] diff --git a/libs/checkpoint-sqlite/pyproject.toml b/libs/checkpoint-sqlite/pyproject.toml index c0a040655..a9d6c945f 100644 --- a/libs/checkpoint-sqlite/pyproject.toml +++ b/libs/checkpoint-sqlite/pyproject.toml @@ -62,6 +62,7 @@ lint.select = [ "UP", # pyupgrade "B", # flake8-bugbear "I", # isort + "PLC0415", # import-outside-top-level "UP", # pyupgrade ] lint.ignore = ["E501", "B008"] diff --git a/libs/checkpoint-sqlite/tests/test_conformance_delta.py b/libs/checkpoint-sqlite/tests/test_conformance_delta.py index ba0e90f18..d900855d1 100644 --- a/libs/checkpoint-sqlite/tests/test_conformance_delta.py +++ b/libs/checkpoint-sqlite/tests/test_conformance_delta.py @@ -10,14 +10,14 @@ pytest.importorskip( ) pytest.importorskip("aiosqlite", reason="aiosqlite not installed") +from langgraph.checkpoint.conformance import validate # noqa: E402 +from langgraph.checkpoint.conformance.initializer import checkpointer_test # noqa: E402 + +from langgraph.checkpoint.sqlite.aio import AsyncSqliteSaver # noqa: E402 + @pytest.mark.asyncio async def test_delta_channel_conformance(): - from langgraph.checkpoint.conformance import validate - from langgraph.checkpoint.conformance.initializer import checkpointer_test - - from langgraph.checkpoint.sqlite.aio import AsyncSqliteSaver - @checkpointer_test(name="AsyncSqliteSaver") async def sqlite_saver(): async with AsyncSqliteSaver.from_conn_string(":memory:") as saver: diff --git a/libs/checkpoint-sqlite/tests/test_store.py b/libs/checkpoint-sqlite/tests/test_store.py index e930208f1..d4b06847e 100644 --- a/libs/checkpoint-sqlite/tests/test_store.py +++ b/libs/checkpoint-sqlite/tests/test_store.py @@ -1,7 +1,11 @@ +import math import os +import random import re import tempfile +import time import uuid +from collections import Counter, defaultdict from collections.abc import Generator, Iterable from contextlib import contextmanager from typing import Any, Literal, cast @@ -33,10 +37,6 @@ class CharacterEmbeddings(Embeddings): def __init__(self, dims: int = 50, seed: int = 42): """Initialize with embedding dimensions and random seed.""" - import math - import random - from collections import defaultdict - self._rng = random.Random(seed) self.dims = dims # Create projection vector for each character lazily @@ -48,9 +48,6 @@ class CharacterEmbeddings(Embeddings): def _embed_one(self, text: str) -> list[float]: """Embed a single text.""" - import math - from collections import Counter - counts = Counter(text) total = sum(counts.values()) @@ -338,8 +335,6 @@ class TestSqliteStore: # Test update # Small delay to ensure the updated timestamp is different - import time - time.sleep(0.01) updated_value = {"title": "Updated Document", "content": "Hello, Updated!"}