From 2963d8fc26d41c25620d2e9ed62f2d80820e27ac Mon Sep 17 00:00:00 2001 From: lr90 Date: Sat, 10 Oct 2026 12:29:24 +0800 Subject: [PATCH] fix(sdk/python): make the SDK pass its own strict mypy configuration `mypy src` reported 31 errors in 4 files despite pyproject.toml setting strict = true, in four groups: - valid-type (16): each resource class defines a method named `list`, which shadows the builtin inside the class body, so parameter annotations like `adds: list[str] | None` resolved to the method rather than the type. Qualified as `builtins.list[...]`; renaming the methods would break the public API. - no-any-return (7): `_request`/`_arequest` return Any, so returning their result directly from a function declared `-> dict[str, Any]` (or `-> float` for _backoff) lost the type. Narrowed with an explicit cast, and _backoff now converts the Any-typed int pow result. - unused-ignore (6): `# type: ignore[return-value]` comments that never matched the real error code (no-any-return). Removed along with the casts that make them unnecessary. - union-attr (2): snapshots.list built its item list with `data.get("snapshots", data.get("items", []))`, which is Any | None, and then iterated it. An empty body (204) or an explicit null field would have raised TypeError at runtime, so this one was a latent bug rather than only a typing complaint; the list is now built defensively. Fixes #266 Co-Authored-By: Claude Opus 5 --- sdk/python/src/memoria/_http.py | 2 +- sdk/python/src/memoria/resources/branches.py | 45 +++++++++++-------- sdk/python/src/memoria/resources/memories.py | 13 +++--- sdk/python/src/memoria/resources/snapshots.py | 21 ++++++--- 4 files changed, 49 insertions(+), 32 deletions(-) diff --git a/sdk/python/src/memoria/_http.py b/sdk/python/src/memoria/_http.py index 5cb590af..a460791d 100644 --- a/sdk/python/src/memoria/_http.py +++ b/sdk/python/src/memoria/_http.py @@ -115,7 +115,7 @@ def _map_error(resp: httpx.Response) -> MemoriaAPIError: def _backoff(attempt: int) -> float: """Exponential backoff: 0.5s, 1s, 2s, …""" - return 0.5 * (2 ** attempt) + return 0.5 * float(2**attempt) class _HttpTransport: diff --git a/sdk/python/src/memoria/resources/branches.py b/sdk/python/src/memoria/resources/branches.py index 1d1a81cc..87d95839 100644 --- a/sdk/python/src/memoria/resources/branches.py +++ b/sdk/python/src/memoria/resources/branches.py @@ -2,7 +2,8 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any +import builtins +from typing import TYPE_CHECKING, Any, cast from ..models import ApplyResult, Branch @@ -39,7 +40,7 @@ def checkout(self, name: str) -> None: def diff(self, name: str) -> dict[str, Any]: """Return high-level diff statistics (no pagination).""" - return self._client._request("GET", f"/v1/branches/{name}/diff") # type: ignore[return-value] + return cast(dict[str, Any], self._client._request("GET", f"/v1/branches/{name}/diff")) def diff_items( self, @@ -50,8 +51,9 @@ def diff_items( ) -> dict[str, Any]: """Return per-entry diff with optional pagination.""" params = _strip_none({"limit": limit, "cursor": cursor}) - return self._client._request( # type: ignore[return-value] - "GET", f"/v1/branches/{name}/diff-items", params=params + return cast( + dict[str, Any], + self._client._request("GET", f"/v1/branches/{name}/diff-items", params=params), ) def merge(self, name: str, *, strategy: str = "accept") -> None: @@ -64,10 +66,10 @@ def apply( self, name: str, *, - adds: list[str] | None = None, - removes: list[str] | None = None, - updates: list[dict[str, str]] | None = None, - accept_branch_conflicts: list[str] | None = None, + adds: builtins.list[str] | None = None, + removes: builtins.list[str] | None = None, + updates: builtins.list[dict[str, str]] | None = None, + accept_branch_conflicts: builtins.list[str] | None = None, ) -> ApplyResult: body = _strip_none( { @@ -97,8 +99,9 @@ def pick( "dry_run": dry_run, } ) - return self._client._request( # type: ignore[return-value] - "POST", f"/v1/branches/{name}/pick", json=body + return cast( + dict[str, Any], + self._client._request("POST", f"/v1/branches/{name}/pick", json=body), ) @@ -125,7 +128,9 @@ async def checkout(self, name: str) -> None: await self._client._arequest("POST", f"/v1/branches/{name}/checkout", json={}) async def diff(self, name: str) -> dict[str, Any]: - return await self._client._arequest("GET", f"/v1/branches/{name}/diff") # type: ignore[return-value] + return cast( + dict[str, Any], await self._client._arequest("GET", f"/v1/branches/{name}/diff") + ) async def diff_items( self, @@ -135,8 +140,9 @@ async def diff_items( cursor: str | None = None, ) -> dict[str, Any]: params = _strip_none({"limit": limit, "cursor": cursor}) - return await self._client._arequest( # type: ignore[return-value] - "GET", f"/v1/branches/{name}/diff-items", params=params + return cast( + dict[str, Any], + await self._client._arequest("GET", f"/v1/branches/{name}/diff-items", params=params), ) async def merge(self, name: str, *, strategy: str = "accept") -> None: @@ -151,10 +157,10 @@ async def apply( self, name: str, *, - adds: list[str] | None = None, - removes: list[str] | None = None, - updates: list[dict[str, str]] | None = None, - accept_branch_conflicts: list[str] | None = None, + adds: builtins.list[str] | None = None, + removes: builtins.list[str] | None = None, + updates: builtins.list[dict[str, str]] | None = None, + accept_branch_conflicts: builtins.list[str] | None = None, ) -> ApplyResult: body = _strip_none( { @@ -184,6 +190,7 @@ async def pick( "dry_run": dry_run, } ) - return await self._client._arequest( # type: ignore[return-value] - "POST", f"/v1/branches/{name}/pick", json=body + return cast( + dict[str, Any], + await self._client._arequest("POST", f"/v1/branches/{name}/pick", json=body), ) diff --git a/sdk/python/src/memoria/resources/memories.py b/sdk/python/src/memoria/resources/memories.py index 0c1052b5..f6cfb21c 100644 --- a/sdk/python/src/memoria/resources/memories.py +++ b/sdk/python/src/memoria/resources/memories.py @@ -2,6 +2,7 @@ from __future__ import annotations +import builtins import json import math from typing import TYPE_CHECKING, Any @@ -451,7 +452,7 @@ def query( *, extra_metadata_filter: dict[str, Any] | None = None, subject_id: str | None = None, - memory_types: list[str] | None = None, + memory_types: builtins.list[str] | None = None, session_id: str | None = None, trust_tier: str | None = None, branch: str | None = None, @@ -533,10 +534,10 @@ def delete( def purge( self, *, - memory_ids: list[str] | None = None, + memory_ids: builtins.list[str] | None = None, topic: str | None = None, session_id: str | None = None, - memory_types: list[str] | None = None, + memory_types: builtins.list[str] | None = None, reason: str | None = None, branch: str | None = None, ) -> PurgeResult: @@ -746,7 +747,7 @@ async def query( *, extra_metadata_filter: dict[str, Any] | None = None, subject_id: str | None = None, - memory_types: list[str] | None = None, + memory_types: builtins.list[str] | None = None, session_id: str | None = None, trust_tier: str | None = None, branch: str | None = None, @@ -828,10 +829,10 @@ async def delete( async def purge( self, *, - memory_ids: list[str] | None = None, + memory_ids: builtins.list[str] | None = None, topic: str | None = None, session_id: str | None = None, - memory_types: list[str] | None = None, + memory_types: builtins.list[str] | None = None, reason: str | None = None, branch: str | None = None, ) -> PurgeResult: diff --git a/sdk/python/src/memoria/resources/snapshots.py b/sdk/python/src/memoria/resources/snapshots.py index 7bf2a892..b3cfd292 100644 --- a/sdk/python/src/memoria/resources/snapshots.py +++ b/sdk/python/src/memoria/resources/snapshots.py @@ -2,6 +2,7 @@ from __future__ import annotations +import builtins from typing import TYPE_CHECKING, Any from ..exceptions import MemoriaValidationError @@ -28,10 +29,14 @@ def list(self, *, limit: int = 20, offset: int = 0) -> list[Snapshot]: params = {"limit": limit, "offset": offset} data = self._client._request("GET", "/v1/snapshots", params=params) # Server may return {"snapshots": [...], "has_more": bool} or a plain list + # An empty body (204) or a null field would otherwise be iterated as None. + items: builtins.list[Any] if isinstance(data, dict): - items = data.get("snapshots", data.get("items", [])) - else: + items = data.get("snapshots") or data.get("items") or [] + elif isinstance(data, builtins.list): items = data + else: + items = [] return [Snapshot.from_dict(s) for s in items] def rollback(self, name: str) -> None: @@ -41,7 +46,7 @@ def delete( self, name: str | None = None, *, - names: list[str] | None = None, + names: builtins.list[str] | None = None, prefix: str | None = None, older_than: str | None = None, ) -> None: @@ -85,10 +90,14 @@ async def create(self, name: str, *, description: str | None = None) -> Snapshot async def list(self, *, limit: int = 20, offset: int = 0) -> list[Snapshot]: params = {"limit": limit, "offset": offset} data = await self._client._arequest("GET", "/v1/snapshots", params=params) + # An empty body (204) or a null field would otherwise be iterated as None. + items: builtins.list[Any] if isinstance(data, dict): - items = data.get("snapshots", data.get("items", [])) - else: + items = data.get("snapshots") or data.get("items") or [] + elif isinstance(data, builtins.list): items = data + else: + items = [] return [Snapshot.from_dict(s) for s in items] async def rollback(self, name: str) -> None: @@ -98,7 +107,7 @@ async def delete( self, name: str | None = None, *, - names: list[str] | None = None, + names: builtins.list[str] | None = None, prefix: str | None = None, older_than: str | None = None, ) -> None: