From 59d7f22afa2c13e9e03a7450c1a38da19306c6ee Mon Sep 17 00:00:00 2001 From: patrick-chinchill Date: Thu, 23 Apr 2026 02:52:15 -0700 Subject: [PATCH 1/9] =?UTF-8?q?chore(types):=20reduce=20pyrefly=20errors?= =?UTF-8?q?=20from=20213=20=E2=86=92=20130=20(38%=20drop)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tackles the largest clusters of real type issues without resorting to `# type: ignore`: **Protocol conformance (lock_scope)** — all 8 adapters typed `lock_scope` as `str | None` instead of `LockScope | None` (`Literal['channel', 'thread'] | None`), so every adapter failed the `Adapter` protocol, cascading through every `adapter=self` call site (ActionEvent, ReactionEvent, etc.). Fixed at the source + propagated to internal `_lock_scope` fields on stateful adapters. **Protocol conformance (parameter names)** — protocols compare parameter names, not just types. Several adapters named params `_thread_id` / `_status` / `_emoji` to signal "unused", which broke structural typing against `Adapter`. Renamed to match the protocol. **Optional dep submodule imports** — `replace-imports-with-any` only matched top-level (`slack_sdk`, `nacl`, `redis`), so submodule imports (`slack_sdk.web.async_client`, `nacl.signing`, `redis.asyncio`) still flagged. Added `.*` wildcards for each optional dep. **TypedDict keys that are Python keywords** — `WhatsAppInboundMessage` had `from_` but the runtime JSON key is `"from"`, so every `message["from"]` access flagged bad-typed-dict-key. Converted to functional-form TypedDict so the Python source can spell `"from"` as a string key. **TypedDict union duck-typing** — GitHub adapter's init detects auth mode via `config.get("token")` / `.get("app_id")`, but the public config type is a union of 4 auth-specific TypedDicts where no single variant has all keys. Added an internal superset TypedDict (`_GitHubAdapterConfigInternal`) cast at the init boundary so the duck-typing stays type-safe without fragmenting the public API. **Event types accepting None** — `ReactionEvent.thread` and `SlashCommandEvent.channel` were typed as required, but adapters legitimately don't always have a Thread/Channel at dispatch time (e.g. Slack reactions on channel-root messages, Discord DM slash commands). Relaxed to `Thread | None` / `Channel | None` with a docstring note for consumers. Baseline regenerated (`uv run pyrefly check --baseline ... --update-baseline`) — reduced from 213 → 130 entries, all remaining entries are now on the local repo paths (previous baseline had `/home/user/...` artifacts from the cloud agent env that never matched locally). Validation: - `uv run pyrefly check --baseline=.pyrefly-baseline.json` → 0 new - `uv run ruff check src/ tests/ scripts/` → clean - `uv run pytest tests/` → 3359 passed, 11 skipped --- .pyrefly-baseline.json | 1334 +++--------------- pyproject.toml | 13 +- src/chat_sdk/adapters/discord/adapter.py | 5 +- src/chat_sdk/adapters/github/adapter.py | 11 +- src/chat_sdk/adapters/github/types.py | 18 +- src/chat_sdk/adapters/google_chat/adapter.py | 5 +- src/chat_sdk/adapters/linear/adapter.py | 15 +- src/chat_sdk/adapters/slack/adapter.py | 5 +- src/chat_sdk/adapters/teams/adapter.py | 17 +- src/chat_sdk/adapters/telegram/adapter.py | 7 +- src/chat_sdk/adapters/whatsapp/adapter.py | 5 +- src/chat_sdk/adapters/whatsapp/types.py | 81 +- src/chat_sdk/types.py | 20 +- 13 files changed, 296 insertions(+), 1240 deletions(-) diff --git a/.pyrefly-baseline.json b/.pyrefly-baseline.json index de42515..7aba7b5 100644 --- a/.pyrefly-baseline.json +++ b/.pyrefly-baseline.json @@ -1,33 +1,9 @@ { "errors": [ { - "line": 276, - "column": 20, - "stop_line": 276, - "stop_column": 32, - "path": "src/chat_sdk/adapters/discord/adapter.py", - "code": -2, - "name": "missing-import", - "description": "Cannot find module `nacl.signing`\n Looked in these locations (from config in `/home/user/chat-sdk-python/pyproject.toml`):\n Import root (inferred from project layout): \"/home/user/chat-sdk-python/src\"\n Site package path queried from interpreter: [\"/home/user/chat-sdk-python/.venv/lib/python3.11/site-packages\", \"/home/user/chat-sdk-python/src\"]", - "concise_description": "Cannot find module `nacl.signing`", - "severity": "error" - }, - { - "line": 366, - "column": 25, - "stop_line": 366, - "stop_column": 29, - "path": "src/chat_sdk/adapters/discord/adapter.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `Self@DiscordAdapter` is not assignable to parameter `adapter` with type `Adapter` in function `chat_sdk.types.ActionEvent.__init__`\n Property getter for `Self@DiscordAdapter.lock_scope` has type `(self: Self@DiscordAdapter) -> str | None`, which is not assignable to `(self: Self@DiscordAdapter) -> Literal['channel', 'thread'] | None`, the property getter for `Adapter.lock_scope`", - "concise_description": "Argument `Self@DiscordAdapter` is not assignable to parameter `adapter` with type `Adapter` in function `chat_sdk.types.ActionEvent.__init__`", - "severity": "error" - }, - { - "line": 416, + "line": 417, "column": 74, - "stop_line": 416, + "stop_line": 417, "stop_column": 83, "path": "src/chat_sdk/adapters/discord/adapter.py", "code": -2, @@ -37,33 +13,9 @@ "severity": "error" }, { - "line": 449, - "column": 21, - "stop_line": 449, - "stop_column": 25, - "path": "src/chat_sdk/adapters/discord/adapter.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `Self@DiscordAdapter` is not assignable to parameter `adapter` with type `Adapter` in function `chat_sdk.types.SlashCommandEvent.__init__`\n Property getter for `Self@DiscordAdapter.lock_scope` has type `(self: Self@DiscordAdapter) -> str | None`, which is not assignable to `(self: Self@DiscordAdapter) -> Literal['channel', 'thread'] | None`, the property getter for `Adapter.lock_scope`", - "concise_description": "Argument `Self@DiscordAdapter` is not assignable to parameter `adapter` with type `Adapter` in function `chat_sdk.types.SlashCommandEvent.__init__`", - "severity": "error" - }, - { - "line": 450, - "column": 21, - "stop_line": 450, - "stop_column": 25, - "path": "src/chat_sdk/adapters/discord/adapter.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `None` is not assignable to parameter `channel` with type `Channel` in function `chat_sdk.types.SlashCommandEvent.__init__`\n Protocol `Channel` requires attribute `name`", - "concise_description": "Argument `None` is not assignable to parameter `channel` with type `Channel` in function `chat_sdk.types.SlashCommandEvent.__init__`", - "severity": "error" - }, - { - "line": 599, + "line": 600, "column": 26, - "stop_line": 599, + "stop_line": 600, "stop_column": 74, "path": "src/chat_sdk/adapters/discord/adapter.py", "code": -2, @@ -73,9 +25,9 @@ "severity": "error" }, { - "line": 612, + "line": 613, "column": 19, - "stop_line": 612, + "stop_line": 613, "stop_column": 53, "path": "src/chat_sdk/adapters/discord/adapter.py", "code": -2, @@ -85,33 +37,9 @@ "severity": "error" }, { - "line": 706, - "column": 25, - "stop_line": 706, - "stop_column": 29, - "path": "src/chat_sdk/adapters/discord/adapter.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `Self@DiscordAdapter` is not assignable to parameter `adapter` with type `Adapter` in function `chat_sdk.types.ReactionEvent.__init__`\n Property getter for `Self@DiscordAdapter.lock_scope` has type `(self: Self@DiscordAdapter) -> str | None`, which is not assignable to `(self: Self@DiscordAdapter) -> Literal['channel', 'thread'] | None`, the property getter for `Adapter.lock_scope`", - "concise_description": "Argument `Self@DiscordAdapter` is not assignable to parameter `adapter` with type `Adapter` in function `chat_sdk.types.ReactionEvent.__init__`", - "severity": "error" - }, - { - "line": 707, - "column": 24, - "stop_line": 707, - "stop_column": 28, - "path": "src/chat_sdk/adapters/discord/adapter.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `None` is not assignable to parameter `thread` with type `Thread` in function `chat_sdk.types.ReactionEvent.__init__`\n Protocol `Thread` requires attribute `channel_id`", - "concise_description": "Argument `None` is not assignable to parameter `thread` with type `Thread` in function `chat_sdk.types.ReactionEvent.__init__`", - "severity": "error" - }, - { - "line": 1162, + "line": 1163, "column": 48, - "stop_line": 1162, + "stop_line": 1163, "stop_column": 68, "path": "src/chat_sdk/adapters/discord/adapter.py", "code": -2, @@ -121,9 +49,9 @@ "severity": "error" }, { - "line": 1249, + "line": 1250, "column": 26, - "stop_line": 1249, + "stop_line": 1250, "stop_column": 76, "path": "src/chat_sdk/adapters/discord/adapter.py", "code": -2, @@ -133,9 +61,9 @@ "severity": "error" }, { - "line": 1411, + "line": 1412, "column": 24, - "stop_line": 1411, + "stop_line": 1412, "stop_column": 44, "path": "src/chat_sdk/adapters/discord/adapter.py", "code": -2, @@ -205,177 +133,9 @@ "severity": "error" }, { - "line": 121, - "column": 39, - "stop_line": 121, - "stop_column": 46, - "path": "src/chat_sdk/adapters/github/adapter.py", - "code": -2, - "name": "bad-typed-dict-key", - "description": "TypedDict `GitHubAdapterAppConfig` does not have key `token`", - "concise_description": "TypedDict `GitHubAdapterAppConfig` does not have key `token`", - "severity": "error" - }, - { - "line": 121, - "column": 39, - "stop_line": 121, - "stop_column": 46, - "path": "src/chat_sdk/adapters/github/adapter.py", - "code": -2, - "name": "bad-typed-dict-key", - "description": "TypedDict `GitHubAdapterAutoConfig` does not have key `token`", - "concise_description": "TypedDict `GitHubAdapterAutoConfig` does not have key `token`", - "severity": "error" - }, - { - "line": 121, - "column": 39, - "stop_line": 121, - "stop_column": 46, - "path": "src/chat_sdk/adapters/github/adapter.py", - "code": -2, - "name": "bad-typed-dict-key", - "description": "TypedDict `GitHubAdapterMultiTenantAppConfig` does not have key `token`", - "concise_description": "TypedDict `GitHubAdapterMultiTenantAppConfig` does not have key `token`", - "severity": "error" - }, - { - "line": 125, - "column": 38, - "stop_line": 125, - "stop_column": 46, - "path": "src/chat_sdk/adapters/github/adapter.py", - "code": -2, - "name": "bad-typed-dict-key", - "description": "TypedDict `GitHubAdapterAutoConfig` does not have key `app_id`", - "concise_description": "TypedDict `GitHubAdapterAutoConfig` does not have key `app_id`", - "severity": "error" - }, - { - "line": 125, - "column": 38, - "stop_line": 125, - "stop_column": 46, - "path": "src/chat_sdk/adapters/github/adapter.py", - "code": -2, - "name": "bad-typed-dict-key", - "description": "TypedDict `GitHubAdapterPATConfig` does not have key `app_id`", - "concise_description": "TypedDict `GitHubAdapterPATConfig` does not have key `app_id`", - "severity": "error" - }, - { - "line": 126, - "column": 43, - "stop_line": 126, - "stop_column": 56, - "path": "src/chat_sdk/adapters/github/adapter.py", - "code": -2, - "name": "bad-typed-dict-key", - "description": "TypedDict `GitHubAdapterAutoConfig` does not have key `private_key`", - "concise_description": "TypedDict `GitHubAdapterAutoConfig` does not have key `private_key`", - "severity": "error" - }, - { - "line": 126, - "column": 43, - "stop_line": 126, - "stop_column": 56, - "path": "src/chat_sdk/adapters/github/adapter.py", - "code": -2, - "name": "bad-typed-dict-key", - "description": "TypedDict `GitHubAdapterPATConfig` does not have key `private_key`", - "concise_description": "TypedDict `GitHubAdapterPATConfig` does not have key `private_key`", - "severity": "error" - }, - { - "line": 128, - "column": 48, - "stop_line": 128, - "stop_column": 65, - "path": "src/chat_sdk/adapters/github/adapter.py", - "code": -2, - "name": "bad-typed-dict-key", - "description": "TypedDict `GitHubAdapterAutoConfig` does not have key `installation_id`", - "concise_description": "TypedDict `GitHubAdapterAutoConfig` does not have key `installation_id`", - "severity": "error" - }, - { - "line": 128, - "column": 48, - "stop_line": 128, - "stop_column": 65, - "path": "src/chat_sdk/adapters/github/adapter.py", - "code": -2, - "name": "bad-typed-dict-key", - "description": "TypedDict `GitHubAdapterMultiTenantAppConfig` does not have key `installation_id`", - "concise_description": "TypedDict `GitHubAdapterMultiTenantAppConfig` does not have key `installation_id`", - "severity": "error" - }, - { - "line": 128, - "column": 48, - "stop_line": 128, - "stop_column": 65, - "path": "src/chat_sdk/adapters/github/adapter.py", - "code": -2, - "name": "bad-typed-dict-key", - "description": "TypedDict `GitHubAdapterPATConfig` does not have key `installation_id`", - "concise_description": "TypedDict `GitHubAdapterPATConfig` does not have key `installation_id`", - "severity": "error" - }, - { - "line": 131, - "column": 38, - "stop_line": 131, - "stop_column": 46, - "path": "src/chat_sdk/adapters/github/adapter.py", - "code": -2, - "name": "bad-typed-dict-key", - "description": "TypedDict `GitHubAdapterAutoConfig` does not have key `app_id`", - "concise_description": "TypedDict `GitHubAdapterAutoConfig` does not have key `app_id`", - "severity": "error" - }, - { - "line": 131, - "column": 38, - "stop_line": 131, - "stop_column": 46, - "path": "src/chat_sdk/adapters/github/adapter.py", - "code": -2, - "name": "bad-typed-dict-key", - "description": "TypedDict `GitHubAdapterPATConfig` does not have key `app_id`", - "concise_description": "TypedDict `GitHubAdapterPATConfig` does not have key `app_id`", - "severity": "error" - }, - { - "line": 132, - "column": 43, - "stop_line": 132, - "stop_column": 56, - "path": "src/chat_sdk/adapters/github/adapter.py", - "code": -2, - "name": "bad-typed-dict-key", - "description": "TypedDict `GitHubAdapterAutoConfig` does not have key `private_key`", - "concise_description": "TypedDict `GitHubAdapterAutoConfig` does not have key `private_key`", - "severity": "error" - }, - { - "line": 132, - "column": 43, - "stop_line": 132, - "stop_column": 56, - "path": "src/chat_sdk/adapters/github/adapter.py", - "code": -2, - "name": "bad-typed-dict-key", - "description": "TypedDict `GitHubAdapterPATConfig` does not have key `private_key`", - "concise_description": "TypedDict `GitHubAdapterPATConfig` does not have key `private_key`", - "severity": "error" - }, - { - "line": 317, + "line": 322, "column": 54, - "stop_line": 317, + "stop_line": 322, "stop_column": 64, "path": "src/chat_sdk/adapters/github/adapter.py", "code": -2, @@ -385,21 +145,9 @@ "severity": "error" }, { - "line": 323, - "column": 36, - "stop_line": 323, - "stop_column": 40, - "path": "src/chat_sdk/adapters/github/adapter.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `Self@GitHubAdapter` is not assignable to parameter `adapter` with type `Adapter` in function `chat_sdk.types.ChatInstance.process_message`\n Property getter for `Self@GitHubAdapter.lock_scope` has type `(self: Self@GitHubAdapter) -> str | None`, which is not assignable to `(self: Self@GitHubAdapter) -> Literal['channel', 'thread'] | None`, the property getter for `Adapter.lock_scope`", - "concise_description": "Argument `Self@GitHubAdapter` is not assignable to parameter `adapter` with type `Adapter` in function `chat_sdk.types.ChatInstance.process_message`", - "severity": "error" - }, - { - "line": 352, + "line": 357, "column": 55, - "stop_line": 352, + "stop_line": 357, "stop_column": 65, "path": "src/chat_sdk/adapters/github/adapter.py", "code": -2, @@ -409,21 +157,9 @@ "severity": "error" }, { - "line": 358, - "column": 36, - "stop_line": 358, - "stop_column": 40, - "path": "src/chat_sdk/adapters/github/adapter.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `Self@GitHubAdapter` is not assignable to parameter `adapter` with type `Adapter` in function `chat_sdk.types.ChatInstance.process_message`\n Property getter for `Self@GitHubAdapter.lock_scope` has type `(self: Self@GitHubAdapter) -> str | None`, which is not assignable to `(self: Self@GitHubAdapter) -> Literal['channel', 'thread'] | None`, the property getter for `Adapter.lock_scope`", - "concise_description": "Argument `Self@GitHubAdapter` is not assignable to parameter `adapter` with type `Adapter` in function `chat_sdk.types.ChatInstance.process_message`", - "severity": "error" - }, - { - "line": 526, + "line": 531, "column": 51, - "stop_line": 526, + "stop_line": 531, "stop_column": 69, "path": "src/chat_sdk/adapters/github/adapter.py", "code": -2, @@ -433,9 +169,9 @@ "severity": "error" }, { - "line": 894, + "line": 899, "column": 26, - "stop_line": 894, + "stop_line": 899, "stop_column": 37, "path": "src/chat_sdk/adapters/github/adapter.py", "code": -2, @@ -445,9 +181,9 @@ "severity": "error" }, { - "line": 898, + "line": 903, "column": 33, - "stop_line": 898, + "stop_line": 903, "stop_column": 50, "path": "src/chat_sdk/adapters/github/adapter.py", "code": -2, @@ -457,9 +193,9 @@ "severity": "error" }, { - "line": 898, + "line": 903, "column": 81, - "stop_line": 898, + "stop_line": 903, "stop_column": 92, "path": "src/chat_sdk/adapters/github/adapter.py", "code": -2, @@ -469,9 +205,9 @@ "severity": "error" }, { - "line": 907, + "line": 912, "column": 39, - "stop_line": 907, + "stop_line": 912, "stop_column": 54, "path": "src/chat_sdk/adapters/github/adapter.py", "code": -2, @@ -481,9 +217,9 @@ "severity": "error" }, { - "line": 910, + "line": 915, "column": 47, - "stop_line": 910, + "stop_line": 915, "stop_column": 61, "path": "src/chat_sdk/adapters/github/adapter.py", "code": -2, @@ -493,9 +229,9 @@ "severity": "error" }, { - "line": 910, + "line": 915, "column": 63, - "stop_line": 910, + "stop_line": 915, "stop_column": 80, "path": "src/chat_sdk/adapters/github/adapter.py", "code": -2, @@ -505,9 +241,9 @@ "severity": "error" }, { - "line": 1068, + "line": 1073, "column": 20, - "stop_line": 1068, + "stop_line": 1073, "stop_column": 40, "path": "src/chat_sdk/adapters/github/adapter.py", "code": -2, @@ -613,45 +349,9 @@ "severity": "error" }, { - "line": 37, - "column": 9, - "stop_line": 37, - "stop_column": 15, - "path": "src/chat_sdk/adapters/github/format_converter.py", - "code": -2, - "name": "bad-param-name-override", - "description": "Class member `GitHubFormatConverter.to_ast` overrides parent class `BaseFormatConverter` in an inconsistent manner\n Got parameter name `markdown`, expected `platform_text`", - "concise_description": "Class member `GitHubFormatConverter.to_ast` overrides parent class `BaseFormatConverter` in an inconsistent manner", - "severity": "error" - }, - { - "line": 326, - "column": 20, - "stop_line": 326, - "stop_column": 31, - "path": "src/chat_sdk/adapters/google_chat/adapter.py", - "code": -2, - "name": "missing-import", - "description": "Cannot find module `google.auth`\n Looked in these locations (from config in `/home/user/chat-sdk-python/pyproject.toml`):\n Import root (inferred from project layout): \"/home/user/chat-sdk-python/src\"\n Site package path queried from interpreter: [\"/home/user/chat-sdk-python/.venv/lib/python3.11/site-packages\", \"/home/user/chat-sdk-python/src\"]", - "concise_description": "Cannot find module `google.auth`", - "severity": "error" - }, - { - "line": 327, + "line": 765, "column": 20, - "stop_line": 327, - "stop_column": 50, - "path": "src/chat_sdk/adapters/google_chat/adapter.py", - "code": -2, - "name": "missing-import", - "description": "Cannot find module `google.auth.transport.requests`\n Looked in these locations (from config in `/home/user/chat-sdk-python/pyproject.toml`):\n Import root (inferred from project layout): \"/home/user/chat-sdk-python/src\"\n Site package path queried from interpreter: [\"/home/user/chat-sdk-python/.venv/lib/python3.11/site-packages\", \"/home/user/chat-sdk-python/src\"]", - "concise_description": "Cannot find module `google.auth.transport.requests`", - "severity": "error" - }, - { - "line": 764, - "column": 20, - "stop_line": 764, + "stop_line": 765, "stop_column": 40, "path": "src/chat_sdk/adapters/google_chat/adapter.py", "code": -2, @@ -661,69 +361,9 @@ "severity": "error" }, { - "line": 954, - "column": 13, - "stop_line": 954, - "stop_column": 17, - "path": "src/chat_sdk/adapters/google_chat/adapter.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `Self@GoogleChatAdapter` is not assignable to parameter `adapter` with type `Adapter` in function `chat_sdk.types.ChatInstance.process_message`\n Property getter for `Self@GoogleChatAdapter.lock_scope` has type `(self: Self@GoogleChatAdapter) -> str | None`, which is not assignable to `(self: Self@GoogleChatAdapter) -> Literal['channel', 'thread'] | None`, the property getter for `Adapter.lock_scope`", - "concise_description": "Argument `Self@GoogleChatAdapter` is not assignable to parameter `adapter` with type `Adapter` in function `chat_sdk.types.ChatInstance.process_message`", - "severity": "error" - }, - { - "line": 1024, - "column": 25, - "stop_line": 1024, - "stop_column": 29, - "path": "src/chat_sdk/adapters/google_chat/adapter.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `Self@GoogleChatAdapter` is not assignable to parameter `adapter` with type `Adapter` in function `chat_sdk.types.ReactionEvent.__init__`\n Property getter for `Self@GoogleChatAdapter.lock_scope` has type `(self: Self@GoogleChatAdapter) -> str | None`, which is not assignable to `(self: Self@GoogleChatAdapter) -> Literal['channel', 'thread'] | None`, the property getter for `Adapter.lock_scope`", - "concise_description": "Argument `Self@GoogleChatAdapter` is not assignable to parameter `adapter` with type `Adapter` in function `chat_sdk.types.ReactionEvent.__init__`", - "severity": "error" - }, - { - "line": 1025, - "column": 24, - "stop_line": 1025, - "stop_column": 28, - "path": "src/chat_sdk/adapters/google_chat/adapter.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `None` is not assignable to parameter `thread` with type `Thread` in function `chat_sdk.types.ReactionEvent.__init__`\n Protocol `Thread` requires attribute `channel_id`", - "concise_description": "Argument `None` is not assignable to parameter `thread` with type `Thread` in function `chat_sdk.types.ReactionEvent.__init__`", - "severity": "error" - }, - { - "line": 1192, - "column": 21, - "stop_line": 1192, - "stop_column": 25, - "path": "src/chat_sdk/adapters/google_chat/adapter.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `Self@GoogleChatAdapter` is not assignable to parameter `adapter` with type `Adapter` in function `chat_sdk.types.ActionEvent.__init__`\n Property getter for `Self@GoogleChatAdapter.lock_scope` has type `(self: Self@GoogleChatAdapter) -> str | None`, which is not assignable to `(self: Self@GoogleChatAdapter) -> Literal['channel', 'thread'] | None`, the property getter for `Adapter.lock_scope`", - "concise_description": "Argument `Self@GoogleChatAdapter` is not assignable to parameter `adapter` with type `Adapter` in function `chat_sdk.types.ActionEvent.__init__`", - "severity": "error" - }, - { - "line": 1252, - "column": 13, - "stop_line": 1252, - "stop_column": 17, - "path": "src/chat_sdk/adapters/google_chat/adapter.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `Self@GoogleChatAdapter` is not assignable to parameter `adapter` with type `Adapter` in function `chat_sdk.types.ChatInstance.process_message`\n Property getter for `Self@GoogleChatAdapter.lock_scope` has type `(self: Self@GoogleChatAdapter) -> str | None`, which is not assignable to `(self: Self@GoogleChatAdapter) -> Literal['channel', 'thread'] | None`, the property getter for `Adapter.lock_scope`", - "concise_description": "Argument `Self@GoogleChatAdapter` is not assignable to parameter `adapter` with type `Adapter` in function `chat_sdk.types.ChatInstance.process_message`", - "severity": "error" - }, - { - "line": 1320, + "line": 1321, "column": 10, - "stop_line": 1320, + "stop_line": 1321, "stop_column": 20, "path": "src/chat_sdk/adapters/google_chat/adapter.py", "code": -2, @@ -733,9 +373,9 @@ "severity": "error" }, { - "line": 1430, + "line": 1431, "column": 10, - "stop_line": 1430, + "stop_line": 1431, "stop_column": 26, "path": "src/chat_sdk/adapters/google_chat/adapter.py", "code": -2, @@ -745,9 +385,9 @@ "severity": "error" }, { - "line": 1507, + "line": 1508, "column": 10, - "stop_line": 1507, + "stop_line": 1508, "stop_column": 20, "path": "src/chat_sdk/adapters/google_chat/adapter.py", "code": -2, @@ -757,9 +397,9 @@ "severity": "error" }, { - "line": 1597, + "line": 1598, "column": 51, - "stop_line": 1597, + "stop_line": 1598, "stop_column": 76, "path": "src/chat_sdk/adapters/google_chat/adapter.py", "code": -2, @@ -769,9 +409,9 @@ "severity": "error" }, { - "line": 1690, + "line": 1691, "column": 46, - "stop_line": 1690, + "stop_line": 1691, "stop_column": 49, "path": "src/chat_sdk/adapters/google_chat/adapter.py", "code": -2, @@ -781,9 +421,9 @@ "severity": "error" }, { - "line": 1788, + "line": 1789, "column": 10, - "stop_line": 1788, + "stop_line": 1789, "stop_column": 21, "path": "src/chat_sdk/adapters/google_chat/adapter.py", "code": -2, @@ -793,9 +433,9 @@ "severity": "error" }, { - "line": 1999, + "line": 2000, "column": 53, - "stop_line": 1999, + "stop_line": 2000, "stop_column": 63, "path": "src/chat_sdk/adapters/google_chat/adapter.py", "code": -2, @@ -805,9 +445,9 @@ "severity": "error" }, { - "line": 2039, + "line": 2040, "column": 10, - "stop_line": 2039, + "stop_line": 2040, "stop_column": 21, "path": "src/chat_sdk/adapters/google_chat/adapter.py", "code": -2, @@ -817,9 +457,9 @@ "severity": "error" }, { - "line": 2232, + "line": 2233, "column": 10, - "stop_line": 2232, + "stop_line": 2233, "stop_column": 27, "path": "src/chat_sdk/adapters/google_chat/adapter.py", "code": -2, @@ -829,9 +469,9 @@ "severity": "error" }, { - "line": 2336, + "line": 2337, "column": 60, - "stop_line": 2336, + "stop_line": 2337, "stop_column": 71, "path": "src/chat_sdk/adapters/google_chat/adapter.py", "code": -2, @@ -841,9 +481,9 @@ "severity": "error" }, { - "line": 2384, + "line": 2385, "column": 10, - "stop_line": 2384, + "stop_line": 2385, "stop_column": 20, "path": "src/chat_sdk/adapters/google_chat/adapter.py", "code": -2, @@ -948,30 +588,6 @@ "concise_description": "Argument `ActionsElement | DividerElement | FieldsElement | ImageElement | LinkElement | SectionElement | TableElement | TextElement` is not assignable to parameter `element` with type `dict[str, Any]` in function `_convert_table_to_widget`", "severity": "error" }, - { - "line": 37, - "column": 9, - "stop_line": 37, - "stop_column": 15, - "path": "src/chat_sdk/adapters/google_chat/format_converter.py", - "code": -2, - "name": "bad-param-name-override", - "description": "Class member `GoogleChatFormatConverter.to_ast` overrides parent class `BaseFormatConverter` in an inconsistent manner\n Got parameter name `gchat_text`, expected `platform_text`", - "concise_description": "Class member `GoogleChatFormatConverter.to_ast` overrides parent class `BaseFormatConverter` in an inconsistent manner", - "severity": "error" - }, - { - "line": 54, - "column": 9, - "stop_line": 54, - "stop_column": 27, - "path": "src/chat_sdk/adapters/google_chat/format_converter.py", - "code": -2, - "name": "bad-param-name-override", - "description": "Class member `GoogleChatFormatConverter.extract_plain_text` overrides parent class `BaseFormatConverter` in an inconsistent manner\n Got parameter name `text`, expected `platform_text`", - "concise_description": "Class member `GoogleChatFormatConverter.extract_plain_text` overrides parent class `BaseFormatConverter` in an inconsistent manner", - "severity": "error" - }, { "line": 346, "column": 20, @@ -985,33 +601,9 @@ "severity": "error" }, { - "line": 406, - "column": 16, - "stop_line": 406, - "stop_column": 27, - "path": "src/chat_sdk/adapters/google_chat/workspace_events.py", - "code": -2, - "name": "missing-import", - "description": "Cannot find module `google.auth`\n Looked in these locations (from config in `/home/user/chat-sdk-python/pyproject.toml`):\n Import root (inferred from project layout): \"/home/user/chat-sdk-python/src\"\n Site package path queried from interpreter: [\"/home/user/chat-sdk-python/.venv/lib/python3.11/site-packages\", \"/home/user/chat-sdk-python/src\"]", - "concise_description": "Cannot find module `google.auth`", - "severity": "error" - }, - { - "line": 407, - "column": 16, - "stop_line": 407, - "stop_column": 46, - "path": "src/chat_sdk/adapters/google_chat/workspace_events.py", - "code": -2, - "name": "missing-import", - "description": "Cannot find module `google.auth.transport.requests`\n Looked in these locations (from config in `/home/user/chat-sdk-python/pyproject.toml`):\n Import root (inferred from project layout): \"/home/user/chat-sdk-python/src\"\n Site package path queried from interpreter: [\"/home/user/chat-sdk-python/.venv/lib/python3.11/site-packages\", \"/home/user/chat-sdk-python/src\"]", - "concise_description": "Cannot find module `google.auth.transport.requests`", - "severity": "error" - }, - { - "line": 305, + "line": 306, "column": 46, - "stop_line": 305, + "stop_line": 306, "stop_column": 53, "path": "src/chat_sdk/adapters/linear/adapter.py", "code": -2, @@ -1021,9 +613,9 @@ "severity": "error" }, { - "line": 307, + "line": 308, "column": 35, - "stop_line": 307, + "stop_line": 308, "stop_column": 42, "path": "src/chat_sdk/adapters/linear/adapter.py", "code": -2, @@ -1033,9 +625,9 @@ "severity": "error" }, { - "line": 354, + "line": 355, "column": 26, - "stop_line": 354, + "stop_line": 355, "stop_column": 34, "path": "src/chat_sdk/adapters/linear/adapter.py", "code": -2, @@ -1045,9 +637,9 @@ "severity": "error" }, { - "line": 355, + "line": 356, "column": 28, - "stop_line": 355, + "stop_line": 356, "stop_column": 43, "path": "src/chat_sdk/adapters/linear/adapter.py", "code": -2, @@ -1057,9 +649,9 @@ "severity": "error" }, { - "line": 359, + "line": 360, "column": 39, - "stop_line": 359, + "stop_line": 360, "stop_column": 43, "path": "src/chat_sdk/adapters/linear/adapter.py", "code": -2, @@ -1069,9 +661,9 @@ "severity": "error" }, { - "line": 359, + "line": 360, "column": 45, - "stop_line": 359, + "stop_line": 360, "stop_column": 50, "path": "src/chat_sdk/adapters/linear/adapter.py", "code": -2, @@ -1081,21 +673,9 @@ "severity": "error" }, { - "line": 367, - "column": 36, - "stop_line": 367, - "stop_column": 40, - "path": "src/chat_sdk/adapters/linear/adapter.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `Self@LinearAdapter` is not assignable to parameter `adapter` with type `Adapter` in function `chat_sdk.types.ChatInstance.process_message`\n Property getter for `Self@LinearAdapter.lock_scope` has type `(self: Self@LinearAdapter) -> str | None`, which is not assignable to `(self: Self@LinearAdapter) -> Literal['channel', 'thread'] | None`, the property getter for `Adapter.lock_scope`", - "concise_description": "Argument `Self@LinearAdapter` is not assignable to parameter `adapter` with type `Adapter` in function `chat_sdk.types.ChatInstance.process_message`", - "severity": "error" - }, - { - "line": 399, + "line": 400, "column": 21, - "stop_line": 399, + "stop_line": 400, "stop_column": 28, "path": "src/chat_sdk/adapters/linear/adapter.py", "code": -2, @@ -1105,9 +685,9 @@ "severity": "error" }, { - "line": 419, + "line": 420, "column": 38, - "stop_line": 419, + "stop_line": 420, "stop_column": 48, "path": "src/chat_sdk/adapters/linear/adapter.py", "code": -2, @@ -1117,9 +697,9 @@ "severity": "error" }, { - "line": 421, + "line": 422, "column": 38, - "stop_line": 421, + "stop_line": 422, "stop_column": 48, "path": "src/chat_sdk/adapters/linear/adapter.py", "code": -2, @@ -1129,9 +709,9 @@ "severity": "error" }, { - "line": 742, + "line": 743, "column": 28, - "stop_line": 742, + "stop_line": 743, "stop_column": 43, "path": "src/chat_sdk/adapters/linear/adapter.py", "code": -2, @@ -1141,9 +721,9 @@ "severity": "error" }, { - "line": 849, + "line": 850, "column": 25, - "stop_line": 849, + "stop_line": 850, "stop_column": 32, "path": "src/chat_sdk/adapters/linear/adapter.py", "code": -2, @@ -1153,9 +733,9 @@ "severity": "error" }, { - "line": 856, + "line": 857, "column": 39, - "stop_line": 856, + "stop_line": 857, "stop_column": 49, "path": "src/chat_sdk/adapters/linear/adapter.py", "code": -2, @@ -1165,9 +745,9 @@ "severity": "error" }, { - "line": 858, + "line": 859, "column": 39, - "stop_line": 858, + "stop_line": 859, "stop_column": 49, "path": "src/chat_sdk/adapters/linear/adapter.py", "code": -2, @@ -1177,9 +757,9 @@ "severity": "error" }, { - "line": 894, + "line": 895, "column": 48, - "stop_line": 894, + "stop_line": 895, "stop_column": 68, "path": "src/chat_sdk/adapters/linear/adapter.py", "code": -2, @@ -1189,9 +769,9 @@ "severity": "error" }, { - "line": 973, + "line": 974, "column": 24, - "stop_line": 973, + "stop_line": 974, "stop_column": 44, "path": "src/chat_sdk/adapters/linear/adapter.py", "code": -2, @@ -1249,21 +829,9 @@ "severity": "error" }, { - "line": 290, - "column": 9, - "stop_line": 290, - "stop_column": 62, - "path": "src/chat_sdk/adapters/slack/adapter.py", - "code": -2, - "name": "missing-import", - "description": "Cannot find module `slack_sdk.web.async_client`\n Looked in these locations (from config in `/home/user/chat-sdk-python/pyproject.toml`):\n Import root (inferred from project layout): \"/home/user/chat-sdk-python/src\"\n Site package path queried from interpreter: [\"/home/user/chat-sdk-python/.venv/lib/python3.11/site-packages\", \"/home/user/chat-sdk-python/src\"]", - "concise_description": "Cannot find module `slack_sdk.web.async_client`", - "severity": "error" - }, - { - "line": 401, + "line": 402, "column": 24, - "stop_line": 401, + "stop_line": 402, "stop_column": 43, "path": "src/chat_sdk/adapters/slack/adapter.py", "code": -2, @@ -1273,9 +841,9 @@ "severity": "error" }, { - "line": 402, + "line": 403, "column": 26, - "stop_line": 402, + "stop_line": 403, "stop_column": 47, "path": "src/chat_sdk/adapters/slack/adapter.py", "code": -2, @@ -1285,9 +853,9 @@ "severity": "error" }, { - "line": 403, + "line": 404, "column": 25, - "stop_line": 403, + "stop_line": 404, "stop_column": 45, "path": "src/chat_sdk/adapters/slack/adapter.py", "code": -2, @@ -1297,9 +865,9 @@ "severity": "error" }, { - "line": 646, + "line": 647, "column": 25, - "stop_line": 646, + "stop_line": 647, "stop_column": 45, "path": "src/chat_sdk/adapters/slack/adapter.py", "code": -2, @@ -1309,9 +877,9 @@ "severity": "error" }, { - "line": 667, + "line": 668, "column": 12, - "stop_line": 667, + "stop_line": 668, "stop_column": 63, "path": "src/chat_sdk/adapters/slack/adapter.py", "code": -2, @@ -1321,69 +889,9 @@ "severity": "error" }, { - "line": 865, - "column": 21, - "stop_line": 865, - "stop_column": 25, - "path": "src/chat_sdk/adapters/slack/adapter.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `Self@SlackAdapter` is not assignable to parameter `adapter` with type `Adapter` in function `chat_sdk.types.SlashCommandEvent.__init__`\n Property getter for `Self@SlackAdapter.lock_scope` has type `(self: Self@SlackAdapter) -> str`, which is not assignable to `(self: Self@SlackAdapter) -> Literal['channel', 'thread'] | None`, the property getter for `Adapter.lock_scope`", - "concise_description": "Argument `Self@SlackAdapter` is not assignable to parameter `adapter` with type `Adapter` in function `chat_sdk.types.SlashCommandEvent.__init__`", - "severity": "error" - }, - { - "line": 866, - "column": 21, - "stop_line": 866, - "stop_column": 25, - "path": "src/chat_sdk/adapters/slack/adapter.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `None` is not assignable to parameter `channel` with type `Channel` in function `chat_sdk.types.SlashCommandEvent.__init__`\n Protocol `Channel` requires attribute `name`", - "concise_description": "Argument `None` is not assignable to parameter `channel` with type `Channel` in function `chat_sdk.types.SlashCommandEvent.__init__`", - "severity": "error" - }, - { - "line": 926, - "column": 25, - "stop_line": 926, - "stop_column": 29, - "path": "src/chat_sdk/adapters/slack/adapter.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `Self@SlackAdapter` is not assignable to parameter `adapter` with type `Adapter` in function `chat_sdk.types.ActionEvent.__init__`\n Property getter for `Self@SlackAdapter.lock_scope` has type `(self: Self@SlackAdapter) -> str`, which is not assignable to `(self: Self@SlackAdapter) -> Literal['channel', 'thread'] | None`, the property getter for `Adapter.lock_scope`", - "concise_description": "Argument `Self@SlackAdapter` is not assignable to parameter `adapter` with type `Adapter` in function `chat_sdk.types.ActionEvent.__init__`", - "severity": "error" - }, - { - "line": 980, - "column": 21, - "stop_line": 980, - "stop_column": 25, - "path": "src/chat_sdk/adapters/slack/adapter.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `Self@SlackAdapter` is not assignable to parameter `adapter` with type `Adapter` in function `chat_sdk.types.ModalSubmitEvent.__init__`\n Property getter for `Self@SlackAdapter.lock_scope` has type `(self: Self@SlackAdapter) -> str`, which is not assignable to `(self: Self@SlackAdapter) -> Literal['channel', 'thread'] | None`, the property getter for `Adapter.lock_scope`", - "concise_description": "Argument `Self@SlackAdapter` is not assignable to parameter `adapter` with type `Adapter` in function `chat_sdk.types.ModalSubmitEvent.__init__`", - "severity": "error" - }, - { - "line": 1016, - "column": 21, - "stop_line": 1016, - "stop_column": 25, - "path": "src/chat_sdk/adapters/slack/adapter.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `Self@SlackAdapter` is not assignable to parameter `adapter` with type `Adapter` in function `chat_sdk.types.ModalCloseEvent.__init__`\n Property getter for `Self@SlackAdapter.lock_scope` has type `(self: Self@SlackAdapter) -> str`, which is not assignable to `(self: Self@SlackAdapter) -> Literal['channel', 'thread'] | None`, the property getter for `Adapter.lock_scope`", - "concise_description": "Argument `Self@SlackAdapter` is not assignable to parameter `adapter` with type `Adapter` in function `chat_sdk.types.ModalCloseEvent.__init__`", - "severity": "error" - }, - { - "line": 1036, + "line": 1037, "column": 44, - "stop_line": 1036, + "stop_line": 1037, "stop_column": 49, "path": "src/chat_sdk/adapters/slack/adapter.py", "code": -2, @@ -1393,93 +901,9 @@ "severity": "error" }, { - "line": 1074, - "column": 36, - "stop_line": 1074, - "stop_column": 40, - "path": "src/chat_sdk/adapters/slack/adapter.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `Self@SlackAdapter` is not assignable to parameter `adapter` with type `Adapter` in function `chat_sdk.types.ChatInstance.process_message`\n Property getter for `Self@SlackAdapter.lock_scope` has type `(self: Self@SlackAdapter) -> str`, which is not assignable to `(self: Self@SlackAdapter) -> Literal['channel', 'thread'] | None`, the property getter for `Adapter.lock_scope`", - "concise_description": "Argument `Self@SlackAdapter` is not assignable to parameter `adapter` with type `Adapter` in function `chat_sdk.types.ChatInstance.process_message`", - "severity": "error" - }, - { - "line": 1144, - "column": 24, - "stop_line": 1144, - "stop_column": 28, - "path": "src/chat_sdk/adapters/slack/adapter.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `None` is not assignable to parameter `thread` with type `Thread` in function `chat_sdk.types.ReactionEvent.__init__`\n Protocol `Thread` requires attribute `channel_id`", - "concise_description": "Argument `None` is not assignable to parameter `thread` with type `Thread` in function `chat_sdk.types.ReactionEvent.__init__`", - "severity": "error" - }, - { - "line": 1146, - "column": 25, - "stop_line": 1146, - "stop_column": 29, - "path": "src/chat_sdk/adapters/slack/adapter.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `Self@SlackAdapter` is not assignable to parameter `adapter` with type `Adapter` in function `chat_sdk.types.ReactionEvent.__init__`\n Property getter for `Self@SlackAdapter.lock_scope` has type `(self: Self@SlackAdapter) -> str`, which is not assignable to `(self: Self@SlackAdapter) -> Literal['channel', 'thread'] | None`, the property getter for `Adapter.lock_scope`", - "concise_description": "Argument `Self@SlackAdapter` is not assignable to parameter `adapter` with type `Adapter` in function `chat_sdk.types.ReactionEvent.__init__`", - "severity": "error" - }, - { - "line": 1186, - "column": 25, - "stop_line": 1186, - "stop_column": 29, - "path": "src/chat_sdk/adapters/slack/adapter.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `Self@SlackAdapter` is not assignable to parameter `adapter` with type `Adapter` in function `chat_sdk.types.AssistantThreadStartedEvent.__init__`\n Property getter for `Self@SlackAdapter.lock_scope` has type `(self: Self@SlackAdapter) -> str`, which is not assignable to `(self: Self@SlackAdapter) -> Literal['channel', 'thread'] | None`, the property getter for `Adapter.lock_scope`", - "concise_description": "Argument `Self@SlackAdapter` is not assignable to parameter `adapter` with type `Adapter` in function `chat_sdk.types.AssistantThreadStartedEvent.__init__`", - "severity": "error" - }, - { - "line": 1221, - "column": 25, - "stop_line": 1221, - "stop_column": 29, - "path": "src/chat_sdk/adapters/slack/adapter.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `Self@SlackAdapter` is not assignable to parameter `adapter` with type `Adapter` in function `chat_sdk.types.AssistantContextChangedEvent.__init__`\n Property getter for `Self@SlackAdapter.lock_scope` has type `(self: Self@SlackAdapter) -> str`, which is not assignable to `(self: Self@SlackAdapter) -> Literal['channel', 'thread'] | None`, the property getter for `Adapter.lock_scope`", - "concise_description": "Argument `Self@SlackAdapter` is not assignable to parameter `adapter` with type `Adapter` in function `chat_sdk.types.AssistantContextChangedEvent.__init__`", - "severity": "error" - }, - { - "line": 1248, - "column": 25, - "stop_line": 1248, - "stop_column": 29, - "path": "src/chat_sdk/adapters/slack/adapter.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `Self@SlackAdapter` is not assignable to parameter `adapter` with type `Adapter` in function `chat_sdk.types.AppHomeOpenedEvent.__init__`\n Property getter for `Self@SlackAdapter.lock_scope` has type `(self: Self@SlackAdapter) -> str`, which is not assignable to `(self: Self@SlackAdapter) -> Literal['channel', 'thread'] | None`, the property getter for `Adapter.lock_scope`", - "concise_description": "Argument `Self@SlackAdapter` is not assignable to parameter `adapter` with type `Adapter` in function `chat_sdk.types.AppHomeOpenedEvent.__init__`", - "severity": "error" - }, - { - "line": 1262, - "column": 25, - "stop_line": 1262, - "stop_column": 29, - "path": "src/chat_sdk/adapters/slack/adapter.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `Self@SlackAdapter` is not assignable to parameter `adapter` with type `Adapter` in function `chat_sdk.types.MemberJoinedChannelEvent.__init__`\n Property getter for `Self@SlackAdapter.lock_scope` has type `(self: Self@SlackAdapter) -> str`, which is not assignable to `(self: Self@SlackAdapter) -> Literal['channel', 'thread'] | None`, the property getter for `Adapter.lock_scope`", - "concise_description": "Argument `Self@SlackAdapter` is not assignable to parameter `adapter` with type `Adapter` in function `chat_sdk.types.MemberJoinedChannelEvent.__init__`", - "severity": "error" - }, - { - "line": 1749, + "line": 1750, "column": 86, - "stop_line": 1749, + "stop_line": 1750, "stop_column": 96, "path": "src/chat_sdk/adapters/slack/adapter.py", "code": -2, @@ -1489,9 +913,9 @@ "severity": "error" }, { - "line": 1842, + "line": 1843, "column": 10, - "stop_line": 1842, + "stop_line": 1843, "stop_column": 20, "path": "src/chat_sdk/adapters/slack/adapter.py", "code": -2, @@ -1501,9 +925,9 @@ "severity": "error" }, { - "line": 2134, + "line": 2135, "column": 10, - "stop_line": 2134, + "stop_line": 2135, "stop_column": 26, "path": "src/chat_sdk/adapters/slack/adapter.py", "code": -2, @@ -1513,9 +937,9 @@ "severity": "error" }, { - "line": 2203, + "line": 2204, "column": 10, - "stop_line": 2203, + "stop_line": 2204, "stop_column": 26, "path": "src/chat_sdk/adapters/slack/adapter.py", "code": -2, @@ -1525,9 +949,9 @@ "severity": "error" }, { - "line": 2268, + "line": 2269, "column": 46, - "stop_line": 2268, + "stop_line": 2269, "stop_column": 49, "path": "src/chat_sdk/adapters/slack/adapter.py", "code": -2, @@ -1537,9 +961,9 @@ "severity": "error" }, { - "line": 2287, + "line": 2288, "column": 106, - "stop_line": 2287, + "stop_line": 2288, "stop_column": 120, "path": "src/chat_sdk/adapters/slack/adapter.py", "code": -2, @@ -1549,9 +973,9 @@ "severity": "error" }, { - "line": 2295, + "line": 2296, "column": 36, - "stop_line": 2295, + "stop_line": 2296, "stop_column": 41, "path": "src/chat_sdk/adapters/slack/adapter.py", "code": -2, @@ -1561,9 +985,9 @@ "severity": "error" }, { - "line": 2310, + "line": 2311, "column": 74, - "stop_line": 2310, + "stop_line": 2311, "stop_column": 88, "path": "src/chat_sdk/adapters/slack/adapter.py", "code": -2, @@ -1573,9 +997,9 @@ "severity": "error" }, { - "line": 2312, + "line": 2313, "column": 36, - "stop_line": 2312, + "stop_line": 2313, "stop_column": 41, "path": "src/chat_sdk/adapters/slack/adapter.py", "code": -2, @@ -1585,9 +1009,9 @@ "severity": "error" }, { - "line": 2370, + "line": 2371, "column": 92, - "stop_line": 2370, + "stop_line": 2371, "stop_column": 103, "path": "src/chat_sdk/adapters/slack/adapter.py", "code": -2, @@ -1597,9 +1021,9 @@ "severity": "error" }, { - "line": 2455, + "line": 2456, "column": 53, - "stop_line": 2455, + "stop_line": 2456, "stop_column": 63, "path": "src/chat_sdk/adapters/slack/adapter.py", "code": -2, @@ -1609,9 +1033,9 @@ "severity": "error" }, { - "line": 2496, + "line": 2497, "column": 101, - "stop_line": 2496, + "stop_line": 2497, "stop_column": 112, "path": "src/chat_sdk/adapters/slack/adapter.py", "code": -2, @@ -1621,9 +1045,9 @@ "severity": "error" }, { - "line": 2573, + "line": 2574, "column": 97, - "stop_line": 2573, + "stop_line": 2574, "stop_column": 114, "path": "src/chat_sdk/adapters/slack/adapter.py", "code": -2, @@ -1633,9 +1057,9 @@ "severity": "error" }, { - "line": 2621, + "line": 2622, "column": 60, - "stop_line": 2621, + "stop_line": 2622, "stop_column": 71, "path": "src/chat_sdk/adapters/slack/adapter.py", "code": -2, @@ -1645,117 +1069,9 @@ "severity": "error" }, { - "line": 40, - "column": 9, - "stop_line": 40, - "stop_column": 15, - "path": "src/chat_sdk/adapters/slack/format_converter.py", - "code": -2, - "name": "bad-param-name-override", - "description": "Class member `SlackFormatConverter.to_ast` overrides parent class `BaseFormatConverter` in an inconsistent manner\n Got parameter name `mrkdwn`, expected `platform_text`", - "concise_description": "Class member `SlackFormatConverter.to_ast` overrides parent class `BaseFormatConverter` in an inconsistent manner", - "severity": "error" - }, - { - "line": 97, - "column": 9, - "stop_line": 97, - "stop_column": 27, - "path": "src/chat_sdk/adapters/slack/format_converter.py", - "code": -2, - "name": "bad-param-name-override", - "description": "Class member `SlackFormatConverter.extract_plain_text` overrides parent class `BaseFormatConverter` in an inconsistent manner\n Got parameter name `mrkdwn`, expected `platform_text`", - "concise_description": "Class member `SlackFormatConverter.extract_plain_text` overrides parent class `BaseFormatConverter` in an inconsistent manner", - "severity": "error" - }, - { - "line": 351, - "column": 36, - "stop_line": 351, - "stop_column": 40, - "path": "src/chat_sdk/adapters/teams/adapter.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `Self@TeamsAdapter` is not assignable to parameter `adapter` with type `Adapter` in function `chat_sdk.types.ChatInstance.process_message`\n Property getter for `Self@TeamsAdapter.lock_scope` has type `(self: Self@TeamsAdapter) -> str | None`, which is not assignable to `(self: Self@TeamsAdapter) -> Literal['channel', 'thread'] | None`, the property getter for `Adapter.lock_scope`", - "concise_description": "Argument `Self@TeamsAdapter` is not assignable to parameter `adapter` with type `Adapter` in function `chat_sdk.types.ChatInstance.process_message`", - "severity": "error" - }, - { - "line": 417, - "column": 25, - "stop_line": 417, - "stop_column": 29, - "path": "src/chat_sdk/adapters/teams/adapter.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `Self@TeamsAdapter` is not assignable to parameter `adapter` with type `Adapter` in function `chat_sdk.types.ActionEvent.__init__`\n Property getter for `Self@TeamsAdapter.lock_scope` has type `(self: Self@TeamsAdapter) -> str | None`, which is not assignable to `(self: Self@TeamsAdapter) -> Literal['channel', 'thread'] | None`, the property getter for `Adapter.lock_scope`", - "concise_description": "Argument `Self@TeamsAdapter` is not assignable to parameter `adapter` with type `Adapter` in function `chat_sdk.types.ActionEvent.__init__`", - "severity": "error" - }, - { - "line": 460, - "column": 25, - "stop_line": 460, - "stop_column": 29, - "path": "src/chat_sdk/adapters/teams/adapter.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `Self@TeamsAdapter` is not assignable to parameter `adapter` with type `Adapter` in function `chat_sdk.types.ActionEvent.__init__`\n Property getter for `Self@TeamsAdapter.lock_scope` has type `(self: Self@TeamsAdapter) -> str | None`, which is not assignable to `(self: Self@TeamsAdapter) -> Literal['channel', 'thread'] | None`, the property getter for `Adapter.lock_scope`", - "concise_description": "Argument `Self@TeamsAdapter` is not assignable to parameter `adapter` with type `Adapter` in function `chat_sdk.types.ActionEvent.__init__`", - "severity": "error" - }, - { - "line": 506, - "column": 28, - "stop_line": 506, - "stop_column": 32, - "path": "src/chat_sdk/adapters/teams/adapter.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `None` is not assignable to parameter `thread` with type `Thread` in function `chat_sdk.types.ReactionEvent.__init__`\n Protocol `Thread` requires attribute `channel_id`", - "concise_description": "Argument `None` is not assignable to parameter `thread` with type `Thread` in function `chat_sdk.types.ReactionEvent.__init__`", - "severity": "error" - }, - { - "line": 507, - "column": 29, - "stop_line": 507, - "stop_column": 33, - "path": "src/chat_sdk/adapters/teams/adapter.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `Self@TeamsAdapter` is not assignable to parameter `adapter` with type `Adapter` in function `chat_sdk.types.ReactionEvent.__init__`\n Property getter for `Self@TeamsAdapter.lock_scope` has type `(self: Self@TeamsAdapter) -> str | None`, which is not assignable to `(self: Self@TeamsAdapter) -> Literal['channel', 'thread'] | None`, the property getter for `Adapter.lock_scope`", - "concise_description": "Argument `Self@TeamsAdapter` is not assignable to parameter `adapter` with type `Adapter` in function `chat_sdk.types.ReactionEvent.__init__`", - "severity": "error" - }, - { - "line": 523, - "column": 28, - "stop_line": 523, - "stop_column": 32, - "path": "src/chat_sdk/adapters/teams/adapter.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `None` is not assignable to parameter `thread` with type `Thread` in function `chat_sdk.types.ReactionEvent.__init__`\n Protocol `Thread` requires attribute `channel_id`", - "concise_description": "Argument `None` is not assignable to parameter `thread` with type `Thread` in function `chat_sdk.types.ReactionEvent.__init__`", - "severity": "error" - }, - { - "line": 524, - "column": 29, - "stop_line": 524, - "stop_column": 33, - "path": "src/chat_sdk/adapters/teams/adapter.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `Self@TeamsAdapter` is not assignable to parameter `adapter` with type `Adapter` in function `chat_sdk.types.ReactionEvent.__init__`\n Property getter for `Self@TeamsAdapter.lock_scope` has type `(self: Self@TeamsAdapter) -> str | None`, which is not assignable to `(self: Self@TeamsAdapter) -> Literal['channel', 'thread'] | None`, the property getter for `Adapter.lock_scope`", - "concise_description": "Argument `Self@TeamsAdapter` is not assignable to parameter `adapter` with type `Adapter` in function `chat_sdk.types.ReactionEvent.__init__`", - "severity": "error" - }, - { - "line": 582, + "line": 583, "column": 18, - "stop_line": 582, + "stop_line": 583, "stop_column": 26, "path": "src/chat_sdk/adapters/teams/adapter.py", "code": -2, @@ -1765,9 +1081,9 @@ "severity": "error" }, { - "line": 1781, + "line": 1782, "column": 24, - "stop_line": 1781, + "stop_line": 1782, "stop_column": 44, "path": "src/chat_sdk/adapters/teams/adapter.py", "code": -2, @@ -1789,21 +1105,9 @@ "severity": "error" }, { - "line": 674, - "column": 36, - "stop_line": 674, - "stop_column": 40, - "path": "src/chat_sdk/adapters/telegram/adapter.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `Self@TelegramAdapter` is not assignable to parameter `adapter` with type `Adapter` in function `chat_sdk.types.ChatInstance.process_message`\n Property getter for `Self@TelegramAdapter.lock_scope` has type `(self: Self@TelegramAdapter) -> str`, which is not assignable to `(self: Self@TelegramAdapter) -> Literal['channel', 'thread'] | None`, the property getter for `Adapter.lock_scope`", - "concise_description": "Argument `Self@TelegramAdapter` is not assignable to parameter `adapter` with type `Adapter` in function `chat_sdk.types.ChatInstance.process_message`", - "severity": "error" - }, - { - "line": 705, + "line": 706, "column": 28, - "stop_line": 705, + "stop_line": 706, "stop_column": 37, "path": "src/chat_sdk/adapters/telegram/adapter.py", "code": -2, @@ -1813,21 +1117,9 @@ "severity": "error" }, { - "line": 718, - "column": 25, - "stop_line": 718, - "stop_column": 29, - "path": "src/chat_sdk/adapters/telegram/adapter.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `Self@TelegramAdapter` is not assignable to parameter `adapter` with type `Adapter` in function `chat_sdk.types.ActionEvent.__init__`\n Property getter for `Self@TelegramAdapter.lock_scope` has type `(self: Self@TelegramAdapter) -> str`, which is not assignable to `(self: Self@TelegramAdapter) -> Literal['channel', 'thread'] | None`, the property getter for `Adapter.lock_scope`", - "concise_description": "Argument `Self@TelegramAdapter` is not assignable to parameter `adapter` with type `Adapter` in function `chat_sdk.types.ActionEvent.__init__`", - "severity": "error" - }, - { - "line": 723, + "line": 724, "column": 27, - "stop_line": 723, + "stop_line": 724, "stop_column": 36, "path": "src/chat_sdk/adapters/telegram/adapter.py", "code": -2, @@ -1837,57 +1129,9 @@ "severity": "error" }, { - "line": 783, - "column": 33, - "stop_line": 783, - "stop_column": 37, - "path": "src/chat_sdk/adapters/telegram/adapter.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `Self@TelegramAdapter` is not assignable to parameter `adapter` with type `Adapter` in function `chat_sdk.types.ReactionEvent.__init__`\n Property getter for `Self@TelegramAdapter.lock_scope` has type `(self: Self@TelegramAdapter) -> str`, which is not assignable to `(self: Self@TelegramAdapter) -> Literal['channel', 'thread'] | None`, the property getter for `Adapter.lock_scope`", - "concise_description": "Argument `Self@TelegramAdapter` is not assignable to parameter `adapter` with type `Adapter` in function `chat_sdk.types.ReactionEvent.__init__`", - "severity": "error" - }, - { - "line": 784, - "column": 32, - "stop_line": 784, - "stop_column": 36, - "path": "src/chat_sdk/adapters/telegram/adapter.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `None` is not assignable to parameter `thread` with type `Thread` in function `chat_sdk.types.ReactionEvent.__init__`\n Protocol `Thread` requires attribute `channel_id`", - "concise_description": "Argument `None` is not assignable to parameter `thread` with type `Thread` in function `chat_sdk.types.ReactionEvent.__init__`", - "severity": "error" - }, - { - "line": 801, - "column": 33, - "stop_line": 801, - "stop_column": 37, - "path": "src/chat_sdk/adapters/telegram/adapter.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `Self@TelegramAdapter` is not assignable to parameter `adapter` with type `Adapter` in function `chat_sdk.types.ReactionEvent.__init__`\n Property getter for `Self@TelegramAdapter.lock_scope` has type `(self: Self@TelegramAdapter) -> str`, which is not assignable to `(self: Self@TelegramAdapter) -> Literal['channel', 'thread'] | None`, the property getter for `Adapter.lock_scope`", - "concise_description": "Argument `Self@TelegramAdapter` is not assignable to parameter `adapter` with type `Adapter` in function `chat_sdk.types.ReactionEvent.__init__`", - "severity": "error" - }, - { - "line": 802, - "column": 32, - "stop_line": 802, - "stop_column": 36, - "path": "src/chat_sdk/adapters/telegram/adapter.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `None` is not assignable to parameter `thread` with type `Thread` in function `chat_sdk.types.ReactionEvent.__init__`\n Protocol `Thread` requires attribute `channel_id`", - "concise_description": "Argument `None` is not assignable to parameter `thread` with type `Thread` in function `chat_sdk.types.ReactionEvent.__init__`", - "severity": "error" - }, - { - "line": 1239, + "line": 1240, "column": 37, - "stop_line": 1239, + "stop_line": 1240, "stop_column": 46, "path": "src/chat_sdk/adapters/telegram/adapter.py", "code": -2, @@ -1897,9 +1141,9 @@ "severity": "error" }, { - "line": 1728, + "line": 1729, "column": 20, - "stop_line": 1728, + "stop_line": 1729, "stop_column": 45, "path": "src/chat_sdk/adapters/telegram/adapter.py", "code": -2, @@ -1909,9 +1153,9 @@ "severity": "error" }, { - "line": 1734, + "line": 1735, "column": 30, - "stop_line": 1734, + "stop_line": 1735, "stop_column": 55, "path": "src/chat_sdk/adapters/telegram/adapter.py", "code": -2, @@ -1921,9 +1165,9 @@ "severity": "error" }, { - "line": 1913, + "line": 1914, "column": 20, - "stop_line": 1913, + "stop_line": 1914, "stop_column": 40, "path": "src/chat_sdk/adapters/telegram/adapter.py", "code": -2, @@ -1933,9 +1177,9 @@ "severity": "error" }, { - "line": 1917, + "line": 1918, "column": 24, - "stop_line": 1917, + "stop_line": 1918, "stop_column": 36, "path": "src/chat_sdk/adapters/telegram/adapter.py", "code": -2, @@ -1945,21 +1189,9 @@ "severity": "error" }, { - "line": 53, - "column": 9, - "stop_line": 53, - "stop_column": 15, - "path": "src/chat_sdk/adapters/telegram/format_converter.py", - "code": -2, - "name": "bad-param-name-override", - "description": "Class member `TelegramFormatConverter.to_ast` overrides parent class `BaseFormatConverter` in an inconsistent manner\n Got parameter name `text`, expected `platform_text`", - "concise_description": "Class member `TelegramFormatConverter.to_ast` overrides parent class `BaseFormatConverter` in an inconsistent manner", - "severity": "error" - }, - { - "line": 210, + "line": 211, "column": 33, - "stop_line": 210, + "stop_line": 211, "stop_column": 40, "path": "src/chat_sdk/adapters/whatsapp/adapter.py", "code": -2, @@ -1969,141 +1201,9 @@ "severity": "error" }, { - "line": 310, - "column": 36, - "stop_line": 310, - "stop_column": 42, - "path": "src/chat_sdk/adapters/whatsapp/adapter.py", - "code": -2, - "name": "bad-typed-dict-key", - "description": "TypedDict `WhatsAppInboundMessage` does not have key `from`\n Did you mean `from_`?", - "concise_description": "TypedDict `WhatsAppInboundMessage` does not have key `from`", - "severity": "error" - }, - { - "line": 315, - "column": 36, - "stop_line": 315, - "stop_column": 40, - "path": "src/chat_sdk/adapters/whatsapp/adapter.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `Self@WhatsAppAdapter` is not assignable to parameter `adapter` with type `Adapter` in function `chat_sdk.types.ChatInstance.process_message`\n Property getter for `Self@WhatsAppAdapter.lock_scope` has type `(self: Self@WhatsAppAdapter) -> str`, which is not assignable to `(self: Self@WhatsAppAdapter) -> Literal['channel', 'thread'] | None`, the property getter for `Adapter.lock_scope`", - "concise_description": "Argument `Self@WhatsAppAdapter` is not assignable to parameter `adapter` with type `Adapter` in function `chat_sdk.types.ChatInstance.process_message`", - "severity": "error" - }, - { - "line": 331, - "column": 36, - "stop_line": 331, - "stop_column": 42, - "path": "src/chat_sdk/adapters/whatsapp/adapter.py", - "code": -2, - "name": "bad-typed-dict-key", - "description": "TypedDict `WhatsAppInboundMessage` does not have key `from`\n Did you mean `from_`?", - "concise_description": "TypedDict `WhatsAppInboundMessage` does not have key `from`", - "severity": "error" - }, - { - "line": 339, - "column": 86, - "stop_line": 339, - "stop_column": 92, - "path": "src/chat_sdk/adapters/whatsapp/adapter.py", - "code": -2, - "name": "bad-typed-dict-key", - "description": "TypedDict `WhatsAppInboundMessage` does not have key `from`\n Did you mean `from_`?", - "concise_description": "TypedDict `WhatsAppInboundMessage` does not have key `from`", - "severity": "error" - }, - { - "line": 341, - "column": 29, - "stop_line": 341, - "stop_column": 35, - "path": "src/chat_sdk/adapters/whatsapp/adapter.py", - "code": -2, - "name": "bad-typed-dict-key", - "description": "TypedDict `WhatsAppInboundMessage` does not have key `from`\n Did you mean `from_`?", - "concise_description": "TypedDict `WhatsAppInboundMessage` does not have key `from`", - "severity": "error" - }, - { - "line": 350, - "column": 25, - "stop_line": 350, - "stop_column": 29, - "path": "src/chat_sdk/adapters/whatsapp/adapter.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `Self@WhatsAppAdapter` is not assignable to parameter `adapter` with type `Adapter` in function `chat_sdk.types.ReactionEvent.__init__`\n Property getter for `Self@WhatsAppAdapter.lock_scope` has type `(self: Self@WhatsAppAdapter) -> str`, which is not assignable to `(self: Self@WhatsAppAdapter) -> Literal['channel', 'thread'] | None`, the property getter for `Adapter.lock_scope`", - "concise_description": "Argument `Self@WhatsAppAdapter` is not assignable to parameter `adapter` with type `Adapter` in function `chat_sdk.types.ReactionEvent.__init__`", - "severity": "error" - }, - { - "line": 351, - "column": 24, - "stop_line": 351, - "stop_column": 28, - "path": "src/chat_sdk/adapters/whatsapp/adapter.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `None` is not assignable to parameter `thread` with type `Thread` in function `chat_sdk.types.ReactionEvent.__init__`\n Protocol `Thread` requires attribute `channel_id`", - "concise_description": "Argument `None` is not assignable to parameter `thread` with type `Thread` in function `chat_sdk.types.ReactionEvent.__init__`", - "severity": "error" - }, - { - "line": 377, - "column": 36, - "stop_line": 377, - "stop_column": 42, - "path": "src/chat_sdk/adapters/whatsapp/adapter.py", - "code": -2, - "name": "bad-typed-dict-key", - "description": "TypedDict `WhatsAppInboundMessage` does not have key `from`\n Did you mean `from_`?", - "concise_description": "TypedDict `WhatsAppInboundMessage` does not have key `from`", - "severity": "error" - }, - { - "line": 398, - "column": 86, - "stop_line": 398, - "stop_column": 92, - "path": "src/chat_sdk/adapters/whatsapp/adapter.py", - "code": -2, - "name": "bad-typed-dict-key", - "description": "TypedDict `WhatsAppInboundMessage` does not have key `from`\n Did you mean `from_`?", - "concise_description": "TypedDict `WhatsAppInboundMessage` does not have key `from`", - "severity": "error" - }, - { - "line": 401, - "column": 25, - "stop_line": 401, - "stop_column": 29, - "path": "src/chat_sdk/adapters/whatsapp/adapter.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `Self@WhatsAppAdapter` is not assignable to parameter `adapter` with type `Adapter` in function `chat_sdk.types.ActionEvent.__init__`\n Property getter for `Self@WhatsAppAdapter.lock_scope` has type `(self: Self@WhatsAppAdapter) -> str`, which is not assignable to `(self: Self@WhatsAppAdapter) -> Literal['channel', 'thread'] | None`, the property getter for `Adapter.lock_scope`", - "concise_description": "Argument `Self@WhatsAppAdapter` is not assignable to parameter `adapter` with type `Adapter` in function `chat_sdk.types.ActionEvent.__init__`", - "severity": "error" - }, - { - "line": 406, - "column": 37, - "stop_line": 406, - "stop_column": 43, - "path": "src/chat_sdk/adapters/whatsapp/adapter.py", - "code": -2, - "name": "bad-typed-dict-key", - "description": "TypedDict `WhatsAppInboundMessage` does not have key `from`\n Did you mean `from_`?", - "concise_description": "TypedDict `WhatsAppInboundMessage` does not have key `from`", - "severity": "error" - }, - { - "line": 412, + "line": 413, "column": 27, - "stop_line": 412, + "stop_line": 413, "stop_column": 36, "path": "src/chat_sdk/adapters/whatsapp/adapter.py", "code": -2, @@ -2113,81 +1213,9 @@ "severity": "error" }, { - "line": 433, - "column": 36, - "stop_line": 433, - "stop_column": 42, - "path": "src/chat_sdk/adapters/whatsapp/adapter.py", - "code": -2, - "name": "bad-typed-dict-key", - "description": "TypedDict `WhatsAppInboundMessage` does not have key `from`\n Did you mean `from_`?", - "concise_description": "TypedDict `WhatsAppInboundMessage` does not have key `from`", - "severity": "error" - }, - { - "line": 437, - "column": 86, - "stop_line": 437, - "stop_column": 92, - "path": "src/chat_sdk/adapters/whatsapp/adapter.py", - "code": -2, - "name": "bad-typed-dict-key", - "description": "TypedDict `WhatsAppInboundMessage` does not have key `from`\n Did you mean `from_`?", - "concise_description": "TypedDict `WhatsAppInboundMessage` does not have key `from`", - "severity": "error" - }, - { - "line": 440, - "column": 25, - "stop_line": 440, - "stop_column": 29, - "path": "src/chat_sdk/adapters/whatsapp/adapter.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `Self@WhatsAppAdapter` is not assignable to parameter `adapter` with type `Adapter` in function `chat_sdk.types.ActionEvent.__init__`\n Property getter for `Self@WhatsAppAdapter.lock_scope` has type `(self: Self@WhatsAppAdapter) -> str`, which is not assignable to `(self: Self@WhatsAppAdapter) -> Literal['channel', 'thread'] | None`, the property getter for `Adapter.lock_scope`", - "concise_description": "Argument `Self@WhatsAppAdapter` is not assignable to parameter `adapter` with type `Adapter` in function `chat_sdk.types.ActionEvent.__init__`", - "severity": "error" - }, - { - "line": 445, - "column": 37, - "stop_line": 445, - "stop_column": 43, - "path": "src/chat_sdk/adapters/whatsapp/adapter.py", - "code": -2, - "name": "bad-typed-dict-key", - "description": "TypedDict `WhatsAppInboundMessage` does not have key `from`\n Did you mean `from_`?", - "concise_description": "TypedDict `WhatsAppInboundMessage` does not have key `from`", - "severity": "error" - }, - { - "line": 499, - "column": 86, - "stop_line": 499, - "stop_column": 92, - "path": "src/chat_sdk/adapters/whatsapp/adapter.py", - "code": -2, - "name": "bad-typed-dict-key", - "description": "TypedDict `WhatsAppInboundMessage` does not have key `from`\n Did you mean `from_`?", - "concise_description": "TypedDict `WhatsAppInboundMessage` does not have key `from`", - "severity": "error" - }, - { - "line": 501, - "column": 29, - "stop_line": 501, - "stop_column": 35, - "path": "src/chat_sdk/adapters/whatsapp/adapter.py", - "code": -2, - "name": "bad-typed-dict-key", - "description": "TypedDict `WhatsAppInboundMessage` does not have key `from`\n Did you mean `from_`?", - "concise_description": "TypedDict `WhatsAppInboundMessage` does not have key `from`", - "severity": "error" - }, - { - "line": 511, + "line": 512, "column": 24, - "stop_line": 511, + "stop_line": 512, "stop_column": 31, "path": "src/chat_sdk/adapters/whatsapp/adapter.py", "code": -2, @@ -2197,9 +1225,9 @@ "severity": "error" }, { - "line": 713, + "line": 714, "column": 87, - "stop_line": 713, + "stop_line": 714, "stop_column": 100, "path": "src/chat_sdk/adapters/whatsapp/adapter.py", "code": -2, @@ -2209,9 +1237,9 @@ "severity": "error" }, { - "line": 718, + "line": 719, "column": 51, - "stop_line": 718, + "stop_line": 719, "stop_column": 57, "path": "src/chat_sdk/adapters/whatsapp/adapter.py", "code": -2, @@ -2221,9 +1249,9 @@ "severity": "error" }, { - "line": 843, + "line": 844, "column": 51, - "stop_line": 843, + "stop_line": 844, "stop_column": 76, "path": "src/chat_sdk/adapters/whatsapp/adapter.py", "code": -2, @@ -2233,9 +1261,9 @@ "severity": "error" }, { - "line": 953, + "line": 954, "column": 43, - "stop_line": 953, + "stop_line": 954, "stop_column": 57, "path": "src/chat_sdk/adapters/whatsapp/adapter.py", "code": -2, @@ -2245,9 +1273,9 @@ "severity": "error" }, { - "line": 955, + "line": 956, "column": 47, - "stop_line": 955, + "stop_line": 956, "stop_column": 61, "path": "src/chat_sdk/adapters/whatsapp/adapter.py", "code": -2, @@ -2257,9 +1285,9 @@ "severity": "error" }, { - "line": 1044, + "line": 1045, "column": 20, - "stop_line": 1044, + "stop_line": 1045, "stop_column": 40, "path": "src/chat_sdk/adapters/whatsapp/adapter.py", "code": -2, @@ -2280,18 +1308,6 @@ "concise_description": "Returned type `list[object | str]` is not assignable to declared return type `list[str]`", "severity": "error" }, - { - "line": 82, - "column": 9, - "stop_line": 82, - "stop_column": 15, - "path": "src/chat_sdk/adapters/whatsapp/format_converter.py", - "code": -2, - "name": "bad-param-name-override", - "description": "Class member `WhatsAppFormatConverter.to_ast` overrides parent class `BaseFormatConverter` in an inconsistent manner\n Got parameter name `markdown`, expected `platform_text`", - "concise_description": "Class member `WhatsAppFormatConverter.to_ast` overrides parent class `BaseFormatConverter` in an inconsistent manner", - "severity": "error" - }, { "line": 772, "column": 61, @@ -2336,8 +1352,8 @@ "path": "src/chat_sdk/chat.py", "code": -2, "name": "bad-argument-type", - "description": "Argument `ChannelImpl` is not assignable to parameter `channel` with type `Channel` in function `chat_sdk.types.SlashCommandEvent.__init__`\n `ChannelImpl.set_state` has type `(self: ChannelImpl, new_state: dict[str, Any], *, replace: bool = False) -> Coroutine[Unknown, Unknown, None]`, which is not assignable to `(self: ChannelImpl, state: dict[str, Any], *, replace: bool = False) -> Coroutine[Unknown, Unknown, None]`, the type of `Channel.set_state`", - "concise_description": "Argument `ChannelImpl` is not assignable to parameter `channel` with type `Channel` in function `chat_sdk.types.SlashCommandEvent.__init__`", + "description": "Argument `ChannelImpl` is not assignable to parameter `channel` with type `Channel | None` in function `chat_sdk.types.SlashCommandEvent.__init__`\n `ChannelImpl.set_state` has type `(self: ChannelImpl, new_state: dict[str, Any], *, replace: bool = False) -> Coroutine[Unknown, Unknown, None]`, which is not assignable to `(self: ChannelImpl, state: dict[str, Any], *, replace: bool = False) -> Coroutine[Unknown, Unknown, None]`, the type of `Channel.set_state`", + "concise_description": "Argument `ChannelImpl` is not assignable to parameter `channel` with type `Channel | None` in function `chat_sdk.types.SlashCommandEvent.__init__`", "severity": "error" }, { @@ -2348,7 +1364,7 @@ "path": "src/chat_sdk/chat.py", "code": -2, "name": "bad-argument-type", - "description": "Argument `ThreadImpl | None` is not assignable to parameter `thread` with type `Thread | None` in function `chat_sdk.types.ActionEvent.__init__`", + "description": "Argument `ThreadImpl | None` is not assignable to parameter `thread` with type `Thread | None` in function `chat_sdk.types.ActionEvent.__init__`\n Property getter for `ThreadImpl.channel` has type `(self: ThreadImpl) -> ChannelImpl`, which is not assignable to `(self: ThreadImpl) -> Channel`, the property getter for `Thread.channel`", "concise_description": "Argument `ThreadImpl | None` is not assignable to parameter `thread` with type `Thread | None` in function `chat_sdk.types.ActionEvent.__init__`", "severity": "error" }, @@ -2360,8 +1376,8 @@ "path": "src/chat_sdk/chat.py", "code": -2, "name": "bad-argument-type", - "description": "Argument `ThreadImpl` is not assignable to parameter `thread` with type `Thread` in function `chat_sdk.types.ReactionEvent.__init__`\n Property getter for `ThreadImpl.channel` has type `(self: ThreadImpl) -> ChannelImpl`, which is not assignable to `(self: ThreadImpl) -> Channel`, the property getter for `Thread.channel`", - "concise_description": "Argument `ThreadImpl` is not assignable to parameter `thread` with type `Thread` in function `chat_sdk.types.ReactionEvent.__init__`", + "description": "Argument `ThreadImpl` is not assignable to parameter `thread` with type `Thread | None` in function `chat_sdk.types.ReactionEvent.__init__`\n Property getter for `ThreadImpl.channel` has type `(self: ThreadImpl) -> ChannelImpl`, which is not assignable to `(self: ThreadImpl) -> Channel`, the property getter for `Thread.channel`", + "concise_description": "Argument `ThreadImpl` is not assignable to parameter `thread` with type `Thread | None` in function `chat_sdk.types.ReactionEvent.__init__`", "severity": "error" }, { @@ -2520,18 +1536,6 @@ "concise_description": "Argument `None` is not assignable to parameter `thread_id` with type `str` in function `chat_sdk.types.RawMessage.__init__`", "severity": "error" }, - { - "line": 109, - "column": 20, - "stop_line": 109, - "stop_column": 45, - "path": "src/chat_sdk/state/redis.py", - "code": -2, - "name": "missing-import", - "description": "Cannot find module `redis.asyncio`\n Looked in these locations (from config in `/home/user/chat-sdk-python/pyproject.toml`):\n Import root (inferred from project layout): \"/home/user/chat-sdk-python/src\"\n Site package path queried from interpreter: [\"/home/user/chat-sdk-python/.venv/lib/python3.11/site-packages\", \"/home/user/chat-sdk-python/src\"]", - "concise_description": "Cannot find module `redis.asyncio`", - "severity": "error" - }, { "line": 578, "column": 27, @@ -2545,9 +1549,9 @@ "severity": "error" }, { - "line": 975, + "line": 1129, "column": 23, - "stop_line": 975, + "stop_line": 1129, "stop_column": 27, "path": "src/chat_sdk/thread.py", "code": -2, diff --git a/pyproject.toml b/pyproject.toml index c563cf5..dd36b42 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -99,19 +99,26 @@ python-platform = "linux" # Optional adapter deps that aren't in the default install. Treating them as # Any keeps pyrefly from flagging missing-import every time we lazy-load one. replace-imports-with-any = [ - # Slack + # Slack — covers top-level + submodule imports like slack_sdk.web.async_client "slack_sdk", - # Discord + "slack_sdk.*", + # Discord — signed-request verification uses pynacl's nacl.signing "nacl", - # Google Chat + "nacl.*", + # Google Chat — auth uses google.auth.* subpackages "google", + "google.*", # State backends "redis", + "redis.*", "asyncpg", + "asyncpg.*", # HTTP clients used in lazy paths "httpx", + "httpx.*", # GitHub App auth "jwt", + "jwt.*", ] [tool.pyrefly.errors] diff --git a/src/chat_sdk/adapters/discord/adapter.py b/src/chat_sdk/adapters/discord/adapter.py index bfbd379..aed5d16 100644 --- a/src/chat_sdk/adapters/discord/adapter.py +++ b/src/chat_sdk/adapters/discord/adapter.py @@ -52,6 +52,7 @@ FetchResult, FileUpload, FormattedContent, + LockScope, Message, MessageMetadata, RawMessage, @@ -160,7 +161,7 @@ def bot_user_id(self) -> str | None: return self._bot_user_id @property - def lock_scope(self) -> str | None: + def lock_scope(self) -> LockScope | None: return None @property @@ -953,7 +954,7 @@ async def remove_reaction( "DELETE", ) - async def start_typing(self, thread_id: str, _status: str | None = None) -> None: + async def start_typing(self, thread_id: str, status: str | None = None) -> None: """Start typing indicator in a Discord channel or thread.""" decoded = self.decode_thread_id(thread_id) target_channel_id = decoded.thread_id or decoded.channel_id diff --git a/src/chat_sdk/adapters/github/adapter.py b/src/chat_sdk/adapters/github/adapter.py index a39bac2..0e3501b 100644 --- a/src/chat_sdk/adapters/github/adapter.py +++ b/src/chat_sdk/adapters/github/adapter.py @@ -17,7 +17,7 @@ import time from collections.abc import AsyncIterable from datetime import datetime, timezone -from typing import Any, Literal +from typing import Any, Literal, cast from chat_sdk.adapters.github.cards import card_to_github_markdown from chat_sdk.adapters.github.format_converter import GitHubFormatConverter @@ -31,6 +31,7 @@ GitHubUser, IssueCommentWebhookPayload, PullRequestReviewCommentWebhookPayload, + _GitHubAdapterConfigInternal, ) from chat_sdk.emoji import convert_emoji_placeholders from chat_sdk.logger import ConsoleLogger, Logger @@ -47,6 +48,7 @@ FormattedContent, ListThreadsOptions, ListThreadsResult, + LockScope, Message, MessageMetadata, RawMessage, @@ -89,7 +91,10 @@ class GitHubAdapter: """ def __init__(self, config: GitHubAdapterConfig | None = None) -> None: - config = config or {} + # The public API surface is the auth-mode-specific TypedDict union + # above; internally we duck-type with `.get()` so narrow to the + # superset TypedDict that admits every possible key. + config = cast(_GitHubAdapterConfigInternal, config or {}) self._name = "github" webhook_secret = config.get("webhook_secret") or os.environ.get("GITHUB_WEBHOOK_SECRET") @@ -177,7 +182,7 @@ def bot_user_id(self) -> str | None: return str(self._bot_user_id) if self._bot_user_id else None @property - def lock_scope(self) -> str | None: + def lock_scope(self) -> LockScope | None: return None @property diff --git a/src/chat_sdk/adapters/github/types.py b/src/chat_sdk/adapters/github/types.py index 0c24979..f88882e 100644 --- a/src/chat_sdk/adapters/github/types.py +++ b/src/chat_sdk/adapters/github/types.py @@ -80,7 +80,23 @@ class GitHubAdapterAutoConfig(GitHubAdapterBaseConfig, total=False): """Configuration with no auth fields - will auto-detect from env vars.""" -# Union of all configuration types +class _GitHubAdapterConfigInternal(GitHubAdapterBaseConfig, total=False): + """Internal superset used for duck-typed `config.get(...)` auth detection. + + The public API surface is the auth-mode-specific TypedDicts above — they + document which fields are valid for each auth mode. Inside `__init__` we + do mode detection via `.get("token")`, `.get("app_id")`, etc., which + requires a type that admits every possible key. This type is never + exposed to consumers. + """ + + token: str + app_id: str + installation_id: int + private_key: str + + +# Union of all public configuration types GitHubAdapterConfig = ( GitHubAdapterPATConfig | GitHubAdapterAppConfig | GitHubAdapterMultiTenantAppConfig | GitHubAdapterAutoConfig ) diff --git a/src/chat_sdk/adapters/google_chat/adapter.py b/src/chat_sdk/adapters/google_chat/adapter.py index 97c6340..d2ed9cc 100644 --- a/src/chat_sdk/adapters/google_chat/adapter.py +++ b/src/chat_sdk/adapters/google_chat/adapter.py @@ -69,6 +69,7 @@ FormattedContent, ListThreadsOptions, ListThreadsResult, + LockScope, Message, RawMessage, ReactionEvent, @@ -130,7 +131,7 @@ def __init__(self, config: GoogleChatAdapterConfig | None = None) -> None: config = GoogleChatAdapterConfig() self._name = "gchat" - self._lock_scope: str | None = None + self._lock_scope: LockScope | None = None self._persist_message_history: bool | None = None self._logger: Logger = config.logger or ConsoleLogger("info").child("gchat") self._user_name = config.user_name or "bot" @@ -211,7 +212,7 @@ def bot_user_id(self) -> str | None: return self._bot_user_id @property - def lock_scope(self) -> str | None: + def lock_scope(self) -> LockScope | None: return self._lock_scope @property diff --git a/src/chat_sdk/adapters/linear/adapter.py b/src/chat_sdk/adapters/linear/adapter.py index 5dc2b79..dfc91dd 100644 --- a/src/chat_sdk/adapters/linear/adapter.py +++ b/src/chat_sdk/adapters/linear/adapter.py @@ -47,6 +47,7 @@ FetchOptions, FetchResult, FormattedContent, + LockScope, Message, MessageMetadata, RawMessage, @@ -169,7 +170,7 @@ def bot_user_id(self) -> str | None: return self._bot_user_id @property - def lock_scope(self) -> str | None: + def lock_scope(self) -> LockScope | None: return None @property @@ -537,7 +538,7 @@ async def edit_message( ), ) - async def delete_message(self, _thread_id: str, message_id: str) -> None: + async def delete_message(self, thread_id: str, message_id: str) -> None: """Delete a message (delete a comment).""" await self._ensure_valid_token() @@ -554,7 +555,7 @@ async def delete_message(self, _thread_id: str, message_id: str) -> None: async def add_reaction( self, - _thread_id: str, + thread_id: str, message_id: str, emoji: EmojiValue | str, ) -> None: @@ -575,14 +576,14 @@ async def add_reaction( async def remove_reaction( self, - _thread_id: str, - _message_id: str, - _emoji: EmojiValue | str, + thread_id: str, + message_id: str, + emoji: EmojiValue | str, ) -> None: """Remove a reaction from a comment (limited support).""" self._logger.warn("removeReaction is not fully supported on Linear - reaction ID lookup would be required") - async def start_typing(self, _thread_id: str, _status: str | None = None) -> None: + async def start_typing(self, thread_id: str, status: str | None = None) -> None: """Start typing indicator. Not supported by Linear.""" pass diff --git a/src/chat_sdk/adapters/slack/adapter.py b/src/chat_sdk/adapters/slack/adapter.py index d1f70e6..c83c1d7 100644 --- a/src/chat_sdk/adapters/slack/adapter.py +++ b/src/chat_sdk/adapters/slack/adapter.py @@ -73,6 +73,7 @@ LinkPreview, ListThreadsOptions, ListThreadsResult, + LockScope, MemberJoinedChannelEvent, Message, MessageMetadata, @@ -203,7 +204,7 @@ def __init__(self, config: SlackAdapterConfig | None = None) -> None: self._bot_id: str | None = None # Bot app ID (B_xxx) self._chat: ChatInstance | None = None self._format_converter = SlackFormatConverter() - self._lock_scope = "thread" + self._lock_scope: LockScope = "thread" self._persist_message_history = False # Channel external/shared cache @@ -245,7 +246,7 @@ def bot_user_id(self) -> str | None: return self._bot_user_id @property - def lock_scope(self) -> str: + def lock_scope(self) -> LockScope: return self._lock_scope @property diff --git a/src/chat_sdk/adapters/teams/adapter.py b/src/chat_sdk/adapters/teams/adapter.py index 562ac06..f915002 100644 --- a/src/chat_sdk/adapters/teams/adapter.py +++ b/src/chat_sdk/adapters/teams/adapter.py @@ -45,6 +45,7 @@ FetchOptions, FetchResult, FormattedContent, + LockScope, Message, MessageMetadata, RawMessage, @@ -195,7 +196,7 @@ def bot_user_id(self) -> str | None: return self._bot_user_id @property - def lock_scope(self) -> str | None: + def lock_scope(self) -> LockScope | None: return None @property @@ -767,23 +768,23 @@ async def delete_message(self, thread_id: str, message_id: str) -> None: async def add_reaction( self, - _thread_id: str, - _message_id: str, - _emoji: EmojiValue | str, + thread_id: str, + message_id: str, + emoji: EmojiValue | str, ) -> None: """Add a reaction (not supported by Teams Bot Framework API).""" self._logger.warn("addReaction is not supported by the Teams Bot Framework API") async def remove_reaction( self, - _thread_id: str, - _message_id: str, - _emoji: EmojiValue | str, + thread_id: str, + message_id: str, + emoji: EmojiValue | str, ) -> None: """Remove a reaction (not supported by Teams Bot Framework API).""" self._logger.warn("removeReaction is not supported by the Teams Bot Framework API") - async def start_typing(self, thread_id: str, _status: str | None = None) -> None: + async def start_typing(self, thread_id: str, status: str | None = None) -> None: """Send typing indicator to a Teams conversation.""" decoded = self.decode_thread_id(thread_id) diff --git a/src/chat_sdk/adapters/telegram/adapter.py b/src/chat_sdk/adapters/telegram/adapter.py index 441f3b3..0da6f16 100644 --- a/src/chat_sdk/adapters/telegram/adapter.py +++ b/src/chat_sdk/adapters/telegram/adapter.py @@ -68,6 +68,7 @@ FetchOptions, FetchResult, FormattedContent, + LockScope, Message, MessageMetadata, RawMessage, @@ -257,7 +258,7 @@ def __init__(self, config: TelegramAdapterConfig | None = None) -> None: ) self._name: str = "telegram" - self._lock_scope: str = "channel" + self._lock_scope: LockScope = "channel" self._persist_message_history: bool = True self._bot_token: str = bot_token @@ -300,7 +301,7 @@ def name(self) -> str: return self._name @property - def lock_scope(self) -> str: + def lock_scope(self) -> LockScope: return self._lock_scope @property @@ -1018,7 +1019,7 @@ async def remove_reaction( self, thread_id: str, message_id: str, - _emoji: EmojiValue | str, + emoji: EmojiValue | str, ) -> None: """Remove a reaction from a Telegram message.""" parsed_thread = self._resolve_thread_id(thread_id) diff --git a/src/chat_sdk/adapters/whatsapp/adapter.py b/src/chat_sdk/adapters/whatsapp/adapter.py index 299d9b6..75024db 100644 --- a/src/chat_sdk/adapters/whatsapp/adapter.py +++ b/src/chat_sdk/adapters/whatsapp/adapter.py @@ -44,6 +44,7 @@ FetchOptions, FetchResult, FormattedContent, + LockScope, Message, MessageMetadata, RawMessage, @@ -102,7 +103,7 @@ class WhatsAppAdapter: def __init__(self, config: WhatsAppAdapterConfig) -> None: self._name = "whatsapp" - self._lock_scope = "channel" + self._lock_scope: LockScope = "channel" self._persist_message_history = True self._user_name = config.user_name self._access_token = config.access_token @@ -124,7 +125,7 @@ def name(self) -> str: return self._name @property - def lock_scope(self) -> str: + def lock_scope(self) -> LockScope: return self._lock_scope @property diff --git a/src/chat_sdk/adapters/whatsapp/types.py b/src/chat_sdk/adapters/whatsapp/types.py index 31e21b0..68092b3 100644 --- a/src/chat_sdk/adapters/whatsapp/types.py +++ b/src/chat_sdk/adapters/whatsapp/types.py @@ -132,44 +132,49 @@ class WhatsAppWebhookPayload(TypedDict): # ============================================================================= -class WhatsAppInboundMessage(TypedDict, total=False): - """Inbound message from a user. - - See: https://developers.facebook.com/docs/whatsapp/cloud-api/webhooks/payload-examples - """ - - # Audio message content - audio: dict[str, Any] - # Legacy button response (from template quick replies) - button: dict[str, str] - # Context for quoted replies - context: dict[str, str] - # Document message content - document: dict[str, Any] - # Sender's WhatsApp ID - from_: str # mapped from "from" in JSON - # Unique message ID - id: str - # Image message content - image: dict[str, Any] - # Interactive message reply - interactive: dict[str, Any] - # Location message content - location: dict[str, Any] - # Reaction to a message - reaction: dict[str, str] - # Sticker message content - sticker: dict[str, Any] - # Text message content - text: dict[str, str] - # Unix timestamp string - timestamp: str - # Message type - type: str - # Video message content - video: dict[str, Any] - # Voice message content - voice: dict[str, Any] +# Inbound message from a user. The `"from"` field name matches the raw JSON +# key (a Python keyword at class-body level, so we use the functional +# TypedDict form to preserve it verbatim). +# +# See: https://developers.facebook.com/docs/whatsapp/cloud-api/webhooks/payload-examples +WhatsAppInboundMessage = TypedDict( + "WhatsAppInboundMessage", + { + # Audio message content + "audio": dict[str, Any], + # Legacy button response (from template quick replies) + "button": dict[str, str], + # Context for quoted replies + "context": dict[str, str], + # Document message content + "document": dict[str, Any], + # Sender's WhatsApp ID + "from": str, + # Unique message ID + "id": str, + # Image message content + "image": dict[str, Any], + # Interactive message reply + "interactive": dict[str, Any], + # Location message content + "location": dict[str, Any], + # Reaction to a message + "reaction": dict[str, str], + # Sticker message content + "sticker": dict[str, Any], + # Text message content + "text": dict[str, str], + # Unix timestamp string + "timestamp": str, + # Message type + "type": str, + # Video message content + "video": dict[str, Any], + # Voice message content + "voice": dict[str, Any], + }, + total=False, +) # ============================================================================= diff --git a/src/chat_sdk/types.py b/src/chat_sdk/types.py index e4386a4..49f3804 100644 --- a/src/chat_sdk/types.py +++ b/src/chat_sdk/types.py @@ -944,10 +944,16 @@ class PostEphemeralOptions: @dataclass class ReactionEvent: - """Reaction event data.""" + """Reaction event data. + + `thread` is optional because some platforms deliver reactions without a + usable thread context (e.g. Slack reactions on a top-level channel + message; Teams reactions where the message lookup failed). Consumers + should `if event.thread is None: return` before using it. + """ adapter: Adapter - thread: Thread + thread: Thread | None thread_id: str message_id: str user: Author @@ -1021,10 +1027,16 @@ class ModalResponse: @dataclass class SlashCommandEvent: - """Slash command event data.""" + """Slash command event data. + + `channel` is optional because some platforms (notably Discord) fire + slash commands without a channel context (DM slash commands), and + because adapters may not always resolve the channel before dispatch. + Consumers should `if event.channel is None: return` before using it. + """ adapter: Adapter - channel: Channel + channel: Channel | None user: Author command: str text: str From 93ff6c7d33816f666d9a6fefd98521f7c7cea28d Mon Sep 17 00:00:00 2001 From: patrick-chinchill Date: Thu, 23 Apr 2026 03:40:37 -0700 Subject: [PATCH 2/9] =?UTF-8?q?chore(types):=20reduce=20pyrefly=20errors?= =?UTF-8?q?=20130=20=E2=86=92=2021=20(another=2084%=20drop=20on=20top=20of?= =?UTF-8?q?=2038%)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Continues the clean-up on fix/pyrefly-zero-errors. Combined with the prior commit, the total reduction this branch is 205 → 21 errors (90%). **NoReturn annotations for error re-raisers** (−22 errors): Slack's `_handle_slack_error` and Google Chat's `_handle_google_chat_error` always raise but were typed `-> None`/`-> Any`, so every `try/except` that propagated through them showed as "missing return". Annotated both as `NoReturn` to let pyrefly skip the downstream paths. **Framework-agnostic `request` duck-typing** (−14 not-async errors): `hasattr(request, "text") and callable(request.text)` narrows `Any` to `object`, making `await request.text()` fail type-checking. Swapped to `getattr(request, "text", None)` (preserves `Any`) plus `inspect.isawaitable` for the sync-vs-async branch — same runtime behavior, but properly typed. Applied across all 7 adapters that duck-type aiohttp / FastAPI / Flask requests. Bonus: also fixes the latent runtime bug where a sync `request.text` (Flask) would have raised `TypeError: object is not awaitable`. **Handler sync/async union** (−3 not-async errors): handler types are `Callable[..., Awaitable[None] | None]` but `_run_handlers`, `_direct_message_handlers`, and pattern handlers all `await` unconditionally. Switched to `inspect.isawaitable` guards — same runtime fix, same pragmatism about the TS port's `Promise | void` convention. **Protocol conformance**: - `_ChatSingleton` now `Protocol` (was plain class), so `Chat` structurally satisfies it without an import cycle - `ThreadImpl.channel` returns `Channel` (protocol) not `ChannelImpl` so `ThreadImpl` passes the `Thread` protocol - `set_state` param renamed `new_state` → `state` to match protocol - `_ChannelImplConfigForChat` removed (duplicate of `_ChannelImplConfigWithAdapter`) - `ChatInstance` protocol now declares `handle_incoming_message` (Discord adapter was calling it but the protocol didn't advertise it) **TypedDict unions duck-typed via `cast`**: many adapters `.get()` against TypedDict unions where the key only exists on some variants. Added narrow `cast(...)` calls where the code has already runtime-verified the variant via a `type` check. **Card-element helpers**: helpers typed `dict[str, Any]` but callers passed `CardChild` (union of TypedDicts). Added `cast("dict[str, Any]", child)` at the `_render_child` / `_convert_child_to_widgets` branches — the variant is already narrowed via `child.get("type")`. **Misc**: - `WhatsAppRawMessage.message` tightened `dict[str, Any]` → `WhatsAppInboundMessage` - `GitHubAdapter._parse_issue_comment` / `_parse_review_comment` param typed `GitHubRepository` instead of `dict[str, Any]` - `_get_attachment_type` returns `Literal["audio","file","image","video"]` - `is_encrypted_token_data` isn't a TypeGuard, so added an `assert` to collapse the `Optional` before the attribute access Baseline regenerated: 213 → 21 entries (90% of original errors gone). `pyrefly check --baseline=.pyrefly-baseline.json` shows 0 new errors. Validation: - `uv run pytest tests/ -q` → 3359 passed, 11 skipped - `uv run ruff check src/ tests/ scripts/` → clean - `uv run ruff format --check src/ tests/ scripts/` → clean - `uv run pyrefly check --baseline=.pyrefly-baseline.json` → 0 new --- .pyrefly-baseline.json | 1360 +---------------- src/chat_sdk/adapters/discord/adapter.py | 38 +- src/chat_sdk/adapters/discord/cards.py | 14 +- src/chat_sdk/adapters/github/adapter.py | 44 +- src/chat_sdk/adapters/github/cards.py | 32 +- src/chat_sdk/adapters/google_chat/adapter.py | 36 +- src/chat_sdk/adapters/google_chat/cards.py | 28 +- .../adapters/google_chat/workspace_events.py | 7 +- src/chat_sdk/adapters/linear/adapter.py | 81 +- src/chat_sdk/adapters/linear/cards.py | 14 +- src/chat_sdk/adapters/slack/adapter.py | 46 +- src/chat_sdk/adapters/teams/adapter.py | 24 +- src/chat_sdk/adapters/telegram/adapter.py | 16 +- src/chat_sdk/adapters/whatsapp/adapter.py | 26 +- src/chat_sdk/adapters/whatsapp/types.py | 2 +- src/chat_sdk/channel.py | 3 +- src/chat_sdk/chat.py | 48 +- src/chat_sdk/thread.py | 27 +- src/chat_sdk/types.py | 9 + tests/test_coverage_gaps.py | 4 +- tests/test_critical_fixes.py | 6 +- 21 files changed, 349 insertions(+), 1516 deletions(-) diff --git a/.pyrefly-baseline.json b/.pyrefly-baseline.json index 7aba7b5..0e64351 100644 --- a/.pyrefly-baseline.json +++ b/.pyrefly-baseline.json @@ -1,1077 +1,9 @@ { "errors": [ { - "line": 417, - "column": 74, - "stop_line": 417, - "stop_column": 83, - "path": "src/chat_sdk/adapters/discord/adapter.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `Literal['options']` is not assignable to parameter `key` with type `Literal['name']` in function `dict.get`", - "concise_description": "Argument `Literal['options']` is not assignable to parameter `key` with type `Literal['name']` in function `dict.get`", - "severity": "error" - }, - { - "line": 600, - "column": 26, - "stop_line": 600, - "stop_column": 74, - "path": "src/chat_sdk/adapters/discord/adapter.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `str` is not assignable to parameter `type` with type `Literal['audio', 'file', 'image', 'video']` in function `chat_sdk.types.Attachment.__init__`", - "concise_description": "Argument `str` is not assignable to parameter `type` with type `Literal['audio', 'file', 'image', 'video']` in function `chat_sdk.types.Attachment.__init__`", - "severity": "error" - }, - { - "line": 613, - "column": 19, - "stop_line": 613, - "stop_column": 53, - "path": "src/chat_sdk/adapters/discord/adapter.py", - "code": -2, - "name": "missing-attribute", - "description": "Object of class `ChatInstance` has no attribute `handle_incoming_message`", - "concise_description": "Object of class `ChatInstance` has no attribute `handle_incoming_message`", - "severity": "error" - }, - { - "line": 1163, - "column": 48, - "stop_line": 1163, - "stop_column": 68, - "path": "src/chat_sdk/adapters/discord/adapter.py", - "code": -2, - "name": "bad-assignment", - "description": "`dict[str, str | Unknown]` is not assignable to `PostableAst | PostableCard | PostableMarkdown | PostableRaw | str | CardElement`", - "concise_description": "`dict[str, str | Unknown]` is not assignable to `PostableAst | PostableCard | PostableMarkdown | PostableRaw | str | CardElement`", - "severity": "error" - }, - { - "line": 1250, - "column": 26, - "stop_line": 1250, - "stop_column": 76, - "path": "src/chat_sdk/adapters/discord/adapter.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `str` is not assignable to parameter `type` with type `Literal['audio', 'file', 'image', 'video']` in function `chat_sdk.types.Attachment.__init__`", - "concise_description": "Argument `str` is not assignable to parameter `type` with type `Literal['audio', 'file', 'image', 'video']` in function `chat_sdk.types.Attachment.__init__`", - "severity": "error" - }, - { - "line": 1412, - "column": 24, - "stop_line": 1412, - "stop_column": 44, - "path": "src/chat_sdk/adapters/discord/adapter.py", - "code": -2, - "name": "not-async", - "description": "Type `object` is not awaitable", - "concise_description": "Type `object` is not awaitable", - "severity": "error" - }, - { - "line": 118, - "column": 46, - "stop_line": 118, - "stop_column": 51, - "path": "src/chat_sdk/adapters/discord/cards.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `object | str` is not assignable to parameter `text` with type `str` in function `_convert_emoji`", - "concise_description": "Argument `object | str` is not assignable to parameter `text` with type `str` in function `_convert_emoji`", - "severity": "error" - }, - { - "line": 122, - "column": 54, - "stop_line": 122, - "stop_column": 61, - "path": "src/chat_sdk/adapters/discord/cards.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `list[str] | object` is not assignable to parameter `headers` with type `list[str]` in function `chat_sdk.shared.card_utils.render_gfm_table`", - "concise_description": "Argument `list[str] | object` is not assignable to parameter `headers` with type `list[str]` in function `chat_sdk.shared.card_utils.render_gfm_table`", - "severity": "error" - }, - { - "line": 122, - "column": 63, - "stop_line": 122, - "stop_column": 67, - "path": "src/chat_sdk/adapters/discord/cards.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `list[list[str]] | object` is not assignable to parameter `rows` with type `list[list[str]]` in function `chat_sdk.shared.card_utils.render_gfm_table`", - "concise_description": "Argument `list[list[str]] | object` is not assignable to parameter `rows` with type `list[list[str]]` in function `chat_sdk.shared.card_utils.render_gfm_table`", - "severity": "error" - }, - { - "line": 275, - "column": 47, - "stop_line": 275, - "stop_column": 54, - "path": "src/chat_sdk/adapters/discord/cards.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `list[str] | object` is not assignable to parameter `headers` with type `list[str]` in function `chat_sdk.cards.table_element_to_ascii`", - "concise_description": "Argument `list[str] | object` is not assignable to parameter `headers` with type `list[str]` in function `chat_sdk.cards.table_element_to_ascii`", - "severity": "error" - }, - { - "line": 275, - "column": 56, - "stop_line": 275, - "stop_column": 60, - "path": "src/chat_sdk/adapters/discord/cards.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `list[list[str]] | object` is not assignable to parameter `rows` with type `list[list[str]]` in function `chat_sdk.cards.table_element_to_ascii`", - "concise_description": "Argument `list[list[str]] | object` is not assignable to parameter `rows` with type `list[list[str]]` in function `chat_sdk.cards.table_element_to_ascii`", - "severity": "error" - }, - { - "line": 322, - "column": 54, - "stop_line": 322, - "stop_column": 64, - "path": "src/chat_sdk/adapters/github/adapter.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `GitHubRepository` is not assignable to parameter `repository` with type `dict[str, Any]` in function `GitHubAdapter._parse_issue_comment`", - "concise_description": "Argument `GitHubRepository` is not assignable to parameter `repository` with type `dict[str, Any]` in function `GitHubAdapter._parse_issue_comment`", - "severity": "error" - }, - { - "line": 357, - "column": 55, - "stop_line": 357, - "stop_column": 65, - "path": "src/chat_sdk/adapters/github/adapter.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `GitHubRepository` is not assignable to parameter `repository` with type `dict[str, Any]` in function `GitHubAdapter._parse_review_comment`", - "concise_description": "Argument `GitHubRepository` is not assignable to parameter `repository` with type `dict[str, Any]` in function `GitHubAdapter._parse_review_comment`", - "severity": "error" - }, - { - "line": 531, - "column": 51, - "stop_line": 531, - "stop_column": 69, - "path": "src/chat_sdk/adapters/github/adapter.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `dict[str, str]` is not assignable to parameter `message` with type `PostableAst | PostableCard | PostableMarkdown | PostableRaw | str | CardElement` in function `GitHubAdapter.post_message`", - "concise_description": "Argument `dict[str, str]` is not assignable to parameter `message` with type `PostableAst | PostableCard | PostableMarkdown | PostableRaw | str | CardElement` in function `GitHubAdapter.post_message`", - "severity": "error" - }, - { - "line": 899, - "column": 26, - "stop_line": 899, - "stop_column": 37, - "path": "src/chat_sdk/adapters/github/adapter.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `object | str` is not assignable to parameter `type` with type `Literal['issue', 'pr'] | None` in function `chat_sdk.adapters.github.types.GitHubThreadId.__init__`", - "concise_description": "Argument `object | str` is not assignable to parameter `type` with type `Literal['issue', 'pr'] | None` in function `chat_sdk.adapters.github.types.GitHubThreadId.__init__`", - "severity": "error" - }, - { - "line": 903, - "column": 33, - "stop_line": 903, - "stop_column": 50, - "path": "src/chat_sdk/adapters/github/adapter.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `GitHubRepository` is not assignable to parameter `repository` with type `dict[str, Any]` in function `GitHubAdapter._parse_issue_comment`", - "concise_description": "Argument `GitHubRepository` is not assignable to parameter `repository` with type `dict[str, Any]` in function `GitHubAdapter._parse_issue_comment`", - "severity": "error" - }, - { - "line": 903, - "column": 81, - "stop_line": 903, - "stop_column": 92, - "path": "src/chat_sdk/adapters/github/adapter.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `object | str` is not assignable to parameter `thread_type` with type `Literal['issue', 'pr']` in function `GitHubAdapter._parse_issue_comment`", - "concise_description": "Argument `object | str` is not assignable to parameter `thread_type` with type `Literal['issue', 'pr']` in function `GitHubAdapter._parse_issue_comment`", - "severity": "error" - }, - { - "line": 912, - "column": 39, - "stop_line": 912, - "stop_column": 54, - "path": "src/chat_sdk/adapters/github/adapter.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `int | object` is not assignable to parameter `review_comment_id` with type `int | None` in function `chat_sdk.adapters.github.types.GitHubThreadId.__init__`", - "concise_description": "Argument `int | object` is not assignable to parameter `review_comment_id` with type `int | None` in function `chat_sdk.adapters.github.types.GitHubThreadId.__init__`", - "severity": "error" - }, - { - "line": 915, - "column": 47, - "stop_line": 915, - "stop_column": 61, - "path": "src/chat_sdk/adapters/github/adapter.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `GitHubIssueComment | GitHubReviewComment` is not assignable to parameter `comment` with type `GitHubReviewComment` in function `GitHubAdapter._parse_review_comment`\n Field `commit_id` is present in `GitHubReviewComment` and absent in `GitHubIssueComment`", - "concise_description": "Argument `GitHubIssueComment | GitHubReviewComment` is not assignable to parameter `comment` with type `GitHubReviewComment` in function `GitHubAdapter._parse_review_comment`", - "severity": "error" - }, - { - "line": 915, - "column": 63, - "stop_line": 915, - "stop_column": 80, - "path": "src/chat_sdk/adapters/github/adapter.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `GitHubRepository` is not assignable to parameter `repository` with type `dict[str, Any]` in function `GitHubAdapter._parse_review_comment`", - "concise_description": "Argument `GitHubRepository` is not assignable to parameter `repository` with type `dict[str, Any]` in function `GitHubAdapter._parse_review_comment`", - "severity": "error" - }, - { - "line": 1073, - "column": 20, - "stop_line": 1073, - "stop_column": 40, - "path": "src/chat_sdk/adapters/github/adapter.py", - "code": -2, - "name": "not-async", - "description": "Type `object` is not awaitable", - "concise_description": "Type `object` is not awaitable", - "severity": "error" - }, - { - "line": 99, - "column": 29, - "stop_line": 99, - "stop_column": 34, - "path": "src/chat_sdk/adapters/github/cards.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `ActionsElement | DividerElement | FieldsElement | ImageElement | LinkElement | SectionElement | TableElement | TextElement` is not assignable to parameter `text` with type `dict[str, Any]` in function `_render_text`", - "concise_description": "Argument `ActionsElement | DividerElement | FieldsElement | ImageElement | LinkElement | SectionElement | TableElement | TextElement` is not assignable to parameter `text` with type `dict[str, Any]` in function `_render_text`", - "severity": "error" - }, - { - "line": 102, - "column": 31, - "stop_line": 102, - "stop_column": 36, - "path": "src/chat_sdk/adapters/github/cards.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `ActionsElement | DividerElement | FieldsElement | ImageElement | LinkElement | SectionElement | TableElement | TextElement` is not assignable to parameter `fields` with type `dict[str, Any]` in function `_render_fields`", - "concise_description": "Argument `ActionsElement | DividerElement | FieldsElement | ImageElement | LinkElement | SectionElement | TableElement | TextElement` is not assignable to parameter `fields` with type `dict[str, Any]` in function `_render_fields`", - "severity": "error" - }, - { - "line": 105, - "column": 32, - "stop_line": 105, - "stop_column": 37, - "path": "src/chat_sdk/adapters/github/cards.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `ActionsElement | DividerElement | FieldsElement | ImageElement | LinkElement | SectionElement | TableElement | TextElement` is not assignable to parameter `actions` with type `dict[str, Any]` in function `_render_actions`", - "concise_description": "Argument `ActionsElement | DividerElement | FieldsElement | ImageElement | LinkElement | SectionElement | TableElement | TextElement` is not assignable to parameter `actions` with type `dict[str, Any]` in function `_render_actions`", - "severity": "error" - }, - { - "line": 110, - "column": 30, - "stop_line": 110, - "stop_column": 55, - "path": "src/chat_sdk/adapters/github/cards.py", - "code": -2, - "name": "not-iterable", - "description": "Type `object` is not iterable", - "concise_description": "Type `object` is not iterable", - "severity": "error" - }, - { - "line": 118, - "column": 43, - "stop_line": 118, - "stop_column": 46, - "path": "src/chat_sdk/adapters/github/cards.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `object | str` is not assignable to parameter `text` with type `str` in function `_escape_markdown`", - "concise_description": "Argument `object | str` is not assignable to parameter `text` with type `str` in function `_escape_markdown`", - "severity": "error" - }, - { - "line": 124, - "column": 38, - "stop_line": 124, - "stop_column": 43, - "path": "src/chat_sdk/adapters/github/cards.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `object | str` is not assignable to parameter `text` with type `str` in function `_escape_markdown`", - "concise_description": "Argument `object | str` is not assignable to parameter `text` with type `str` in function `_escape_markdown`", - "severity": "error" - }, - { - "line": 130, - "column": 30, - "stop_line": 130, - "stop_column": 35, - "path": "src/chat_sdk/adapters/github/cards.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `ActionsElement | DividerElement | FieldsElement | ImageElement | LinkElement | SectionElement | TableElement | TextElement` is not assignable to parameter `table` with type `dict[str, Any]` in function `_render_table`", - "concise_description": "Argument `ActionsElement | DividerElement | FieldsElement | ImageElement | LinkElement | SectionElement | TableElement | TextElement` is not assignable to parameter `table` with type `dict[str, Any]` in function `_render_table`", - "severity": "error" - }, - { - "line": 236, - "column": 40, - "stop_line": 236, - "stop_column": 45, - "path": "src/chat_sdk/adapters/github/cards.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `ActionsElement | DividerElement | FieldsElement | ImageElement | LinkElement | SectionElement | TableElement | TextElement` is not assignable to parameter `table` with type `dict[str, Any]` in function `_render_table`", - "concise_description": "Argument `ActionsElement | DividerElement | FieldsElement | ImageElement | LinkElement | SectionElement | TableElement | TextElement` is not assignable to parameter `table` with type `dict[str, Any]` in function `_render_table`", - "severity": "error" - }, - { - "line": 765, - "column": 20, - "stop_line": 765, - "stop_column": 40, - "path": "src/chat_sdk/adapters/google_chat/adapter.py", - "code": -2, - "name": "not-async", - "description": "Type `object` is not awaitable", - "concise_description": "Type `object` is not awaitable", - "severity": "error" - }, - { - "line": 1321, - "column": 10, - "stop_line": 1321, - "stop_column": 20, - "path": "src/chat_sdk/adapters/google_chat/adapter.py", - "code": -2, - "name": "bad-return", - "description": "Function declared to return `RawMessage`, but one or more paths are missing an explicit `return`", - "concise_description": "Function declared to return `RawMessage`, but one or more paths are missing an explicit `return`", - "severity": "error" - }, - { - "line": 1431, - "column": 10, - "stop_line": 1431, - "stop_column": 26, - "path": "src/chat_sdk/adapters/google_chat/adapter.py", - "code": -2, - "name": "bad-return", - "description": "Function declared to return `EphemeralMessage`, but one or more paths are missing an explicit `return`", - "concise_description": "Function declared to return `EphemeralMessage`, but one or more paths are missing an explicit `return`", - "severity": "error" - }, - { - "line": 1508, - "column": 10, - "stop_line": 1508, - "stop_column": 20, - "path": "src/chat_sdk/adapters/google_chat/adapter.py", - "code": -2, - "name": "bad-return", - "description": "Function declared to return `RawMessage`, but one or more paths are missing an explicit `return`", - "concise_description": "Function declared to return `RawMessage`, but one or more paths are missing an explicit `return`", - "severity": "error" - }, - { - "line": 1598, - "column": 51, - "stop_line": 1598, - "stop_column": 76, - "path": "src/chat_sdk/adapters/google_chat/adapter.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `dict[str, str]` is not assignable to parameter `message` with type `PostableAst | PostableCard | PostableMarkdown | PostableRaw | str | CardElement` in function `GoogleChatAdapter.post_message`", - "concise_description": "Argument `dict[str, str]` is not assignable to parameter `message` with type `PostableAst | PostableCard | PostableMarkdown | PostableRaw | str | CardElement` in function `GoogleChatAdapter.post_message`", - "severity": "error" - }, - { - "line": 1691, - "column": 46, - "stop_line": 1691, - "stop_column": 49, - "path": "src/chat_sdk/adapters/google_chat/adapter.py", - "code": -2, - "name": "bad-return", - "description": "Function declared to return `str`, but one or more paths are missing an explicit `return`", - "concise_description": "Function declared to return `str`, but one or more paths are missing an explicit `return`", - "severity": "error" - }, - { - "line": 1789, - "column": 10, - "stop_line": 1789, - "stop_column": 21, - "path": "src/chat_sdk/adapters/google_chat/adapter.py", - "code": -2, - "name": "bad-return", - "description": "Function declared to return `FetchResult`, but one or more paths are missing an explicit `return`", - "concise_description": "Function declared to return `FetchResult`, but one or more paths are missing an explicit `return`", - "severity": "error" - }, - { - "line": 2000, - "column": 53, - "stop_line": 2000, - "stop_column": 63, - "path": "src/chat_sdk/adapters/google_chat/adapter.py", - "code": -2, - "name": "bad-return", - "description": "Function declared to return `ThreadInfo`, but one or more paths are missing an explicit `return`", - "concise_description": "Function declared to return `ThreadInfo`, but one or more paths are missing an explicit `return`", - "severity": "error" - }, - { - "line": 2040, - "column": 10, - "stop_line": 2040, - "stop_column": 21, - "path": "src/chat_sdk/adapters/google_chat/adapter.py", - "code": -2, - "name": "bad-return", - "description": "Function declared to return `FetchResult`, but one or more paths are missing an explicit `return`", - "concise_description": "Function declared to return `FetchResult`, but one or more paths are missing an explicit `return`", - "severity": "error" - }, - { - "line": 2233, - "column": 10, - "stop_line": 2233, - "stop_column": 27, - "path": "src/chat_sdk/adapters/google_chat/adapter.py", - "code": -2, - "name": "bad-return", - "description": "Function declared to return `ListThreadsResult`, but one or more paths are missing an explicit `return`", - "concise_description": "Function declared to return `ListThreadsResult`, but one or more paths are missing an explicit `return`", - "severity": "error" - }, - { - "line": 2337, - "column": 60, - "stop_line": 2337, - "stop_column": 71, - "path": "src/chat_sdk/adapters/google_chat/adapter.py", - "code": -2, - "name": "bad-return", - "description": "Function declared to return `ChannelInfo`, but one or more paths are missing an explicit `return`", - "concise_description": "Function declared to return `ChannelInfo`, but one or more paths are missing an explicit `return`", - "severity": "error" - }, - { - "line": 2385, - "column": 10, - "stop_line": 2385, - "stop_column": 20, - "path": "src/chat_sdk/adapters/google_chat/adapter.py", - "code": -2, - "name": "bad-return", - "description": "Function declared to return `RawMessage`, but one or more paths are missing an explicit `return`", - "concise_description": "Function declared to return `RawMessage`, but one or more paths are missing an explicit `return`", - "severity": "error" - }, - { - "line": 76, - "column": 59, - "stop_line": 76, - "stop_column": 64, - "path": "src/chat_sdk/adapters/google_chat/cards.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `ActionsElement | DividerElement | FieldsElement | ImageElement | LinkElement | SectionElement | TableElement | TextElement | Unknown` is not assignable to parameter `element` with type `dict[str, Any]` in function `_convert_section_to_widgets`", - "concise_description": "Argument `ActionsElement | DividerElement | FieldsElement | ImageElement | LinkElement | SectionElement | TableElement | TextElement | Unknown` is not assignable to parameter `element` with type `dict[str, Any]` in function `_convert_section_to_widgets`", - "severity": "error" - }, - { - "line": 115, - "column": 41, - "stop_line": 115, - "stop_column": 46, - "path": "src/chat_sdk/adapters/google_chat/cards.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `ActionsElement | DividerElement | FieldsElement | ImageElement | LinkElement | SectionElement | TableElement | TextElement` is not assignable to parameter `element` with type `dict[str, Any]` in function `_convert_text_to_widget`", - "concise_description": "Argument `ActionsElement | DividerElement | FieldsElement | ImageElement | LinkElement | SectionElement | TableElement | TextElement` is not assignable to parameter `element` with type `dict[str, Any]` in function `_convert_text_to_widget`", - "severity": "error" - }, - { - "line": 117, - "column": 42, - "stop_line": 117, - "stop_column": 47, - "path": "src/chat_sdk/adapters/google_chat/cards.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `ActionsElement | DividerElement | FieldsElement | ImageElement | LinkElement | SectionElement | TableElement | TextElement` is not assignable to parameter `element` with type `dict[str, Any]` in function `_convert_image_to_widget`", - "concise_description": "Argument `ActionsElement | DividerElement | FieldsElement | ImageElement | LinkElement | SectionElement | TableElement | TextElement` is not assignable to parameter `element` with type `dict[str, Any]` in function `_convert_image_to_widget`", - "severity": "error" - }, - { - "line": 121, - "column": 44, - "stop_line": 121, - "stop_column": 49, - "path": "src/chat_sdk/adapters/google_chat/cards.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `ActionsElement | DividerElement | FieldsElement | ImageElement | LinkElement | SectionElement | TableElement | TextElement` is not assignable to parameter `element` with type `dict[str, Any]` in function `_convert_actions_to_widget`", - "concise_description": "Argument `ActionsElement | DividerElement | FieldsElement | ImageElement | LinkElement | SectionElement | TableElement | TextElement` is not assignable to parameter `element` with type `dict[str, Any]` in function `_convert_actions_to_widget`", - "severity": "error" - }, - { - "line": 123, - "column": 44, - "stop_line": 123, - "stop_column": 49, - "path": "src/chat_sdk/adapters/google_chat/cards.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `ActionsElement | DividerElement | FieldsElement | ImageElement | LinkElement | SectionElement | TableElement | TextElement` is not assignable to parameter `element` with type `dict[str, Any]` in function `_convert_section_to_widgets`", - "concise_description": "Argument `ActionsElement | DividerElement | FieldsElement | ImageElement | LinkElement | SectionElement | TableElement | TextElement` is not assignable to parameter `element` with type `dict[str, Any]` in function `_convert_section_to_widgets`", - "severity": "error" - }, - { - "line": 125, - "column": 43, - "stop_line": 125, - "stop_column": 48, - "path": "src/chat_sdk/adapters/google_chat/cards.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `ActionsElement | DividerElement | FieldsElement | ImageElement | LinkElement | SectionElement | TableElement | TextElement` is not assignable to parameter `element` with type `dict[str, Any]` in function `_convert_fields_to_widgets`", - "concise_description": "Argument `ActionsElement | DividerElement | FieldsElement | ImageElement | LinkElement | SectionElement | TableElement | TextElement` is not assignable to parameter `element` with type `dict[str, Any]` in function `_convert_fields_to_widgets`", - "severity": "error" - }, - { - "line": 132, - "column": 62, - "stop_line": 132, - "stop_column": 67, - "path": "src/chat_sdk/adapters/google_chat/cards.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `object | str` is not assignable to parameter with type `str`", - "concise_description": "Argument `object | str` is not assignable to parameter with type `str`", - "severity": "error" - }, - { - "line": 137, - "column": 42, - "stop_line": 137, - "stop_column": 47, - "path": "src/chat_sdk/adapters/google_chat/cards.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `ActionsElement | DividerElement | FieldsElement | ImageElement | LinkElement | SectionElement | TableElement | TextElement` is not assignable to parameter `element` with type `dict[str, Any]` in function `_convert_table_to_widget`", - "concise_description": "Argument `ActionsElement | DividerElement | FieldsElement | ImageElement | LinkElement | SectionElement | TableElement | TextElement` is not assignable to parameter `element` with type `dict[str, Any]` in function `_convert_table_to_widget`", - "severity": "error" - }, - { - "line": 346, - "column": 20, - "stop_line": 346, - "stop_column": 39, - "path": "src/chat_sdk/adapters/google_chat/workspace_events.py", - "code": -2, - "name": "not-async", - "description": "Type `object` is not awaitable", - "concise_description": "Type `object` is not awaitable", - "severity": "error" - }, - { - "line": 306, - "column": 46, - "stop_line": 306, - "stop_column": 53, - "path": "src/chat_sdk/adapters/linear/adapter.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `dict[str, Any]` is not assignable to parameter `payload` with type `CommentWebhookPayload` in function `LinearAdapter._handle_comment_created`", - "concise_description": "Argument `dict[str, Any]` is not assignable to parameter `payload` with type `CommentWebhookPayload` in function `LinearAdapter._handle_comment_created`", - "severity": "error" - }, - { - "line": 308, - "column": 35, - "stop_line": 308, - "stop_column": 42, - "path": "src/chat_sdk/adapters/linear/adapter.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `dict[str, Any]` is not assignable to parameter `payload` with type `ReactionWebhookPayload` in function `LinearAdapter._handle_reaction`", - "concise_description": "Argument `dict[str, Any]` is not assignable to parameter `payload` with type `ReactionWebhookPayload` in function `LinearAdapter._handle_reaction`", - "severity": "error" - }, - { - "line": 355, - "column": 26, - "stop_line": 355, - "stop_column": 34, - "path": "src/chat_sdk/adapters/linear/adapter.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `object | str | Unknown` is not assignable to parameter `issue_id` with type `str` in function `chat_sdk.adapters.linear.types.LinearThreadId.__init__`", - "concise_description": "Argument `object | str | Unknown` is not assignable to parameter `issue_id` with type `str` in function `chat_sdk.adapters.linear.types.LinearThreadId.__init__`", - "severity": "error" - }, - { - "line": 356, - "column": 28, - "stop_line": 356, - "stop_column": 43, - "path": "src/chat_sdk/adapters/linear/adapter.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `object | str | Unknown | None` is not assignable to parameter `comment_id` with type `str | None` in function `chat_sdk.adapters.linear.types.LinearThreadId.__init__`", - "concise_description": "Argument `object | str | Unknown | None` is not assignable to parameter `comment_id` with type `str | None` in function `chat_sdk.adapters.linear.types.LinearThreadId.__init__`", - "severity": "error" - }, - { - "line": 360, - "column": 39, - "stop_line": 360, - "stop_column": 43, - "path": "src/chat_sdk/adapters/linear/adapter.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `dict[Unknown, Unknown] | LinearCommentData` is not assignable to parameter `comment` with type `LinearCommentData` in function `LinearAdapter._build_message`", - "concise_description": "Argument `dict[Unknown, Unknown] | LinearCommentData` is not assignable to parameter `comment` with type `LinearCommentData` in function `LinearAdapter._build_message`", - "severity": "error" - }, - { - "line": 360, - "column": 45, - "stop_line": 360, - "stop_column": 50, - "path": "src/chat_sdk/adapters/linear/adapter.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `dict[Unknown, Unknown] | LinearWebhookActor` is not assignable to parameter `actor` with type `LinearWebhookActor` in function `LinearAdapter._build_message`", - "concise_description": "Argument `dict[Unknown, Unknown] | LinearWebhookActor` is not assignable to parameter `actor` with type `LinearWebhookActor` in function `LinearAdapter._build_message`", - "severity": "error" - }, - { - "line": 400, - "column": 21, - "stop_line": 400, - "stop_column": 28, - "path": "src/chat_sdk/adapters/linear/adapter.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `object | str` is not assignable to parameter `user_id` with type `str` in function `chat_sdk.types.Author.__init__`", - "concise_description": "Argument `object | str` is not assignable to parameter `user_id` with type `str` in function `chat_sdk.types.Author.__init__`", - "severity": "error" - }, - { - "line": 420, - "column": 38, - "stop_line": 420, - "stop_column": 48, - "path": "src/chat_sdk/adapters/linear/adapter.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `object | str` is not assignable to parameter `s` with type `str` in function `chat_sdk.types._parse_iso`", - "concise_description": "Argument `object | str` is not assignable to parameter `s` with type `str` in function `chat_sdk.types._parse_iso`", - "severity": "error" - }, - { - "line": 422, - "column": 38, - "stop_line": 422, - "stop_column": 48, - "path": "src/chat_sdk/adapters/linear/adapter.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `object | str` is not assignable to parameter `s` with type `str` in function `chat_sdk.types._parse_iso`", - "concise_description": "Argument `object | str` is not assignable to parameter `s` with type `str` in function `chat_sdk.types._parse_iso`", - "severity": "error" - }, - { - "line": 743, - "column": 28, - "stop_line": 743, - "stop_column": 43, - "path": "src/chat_sdk/adapters/linear/adapter.py", - "code": -2, - "name": "bad-typed-dict-key", - "description": "`Any | None` is not assignable to TypedDict key `url` with type `str`", - "concise_description": "`Any | None` is not assignable to TypedDict key `url` with type `str`", - "severity": "error" - }, - { - "line": 850, - "column": 25, - "stop_line": 850, - "stop_column": 32, - "path": "src/chat_sdk/adapters/linear/adapter.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `object | str | Unknown` is not assignable to parameter `user_id` with type `str` in function `chat_sdk.types.Author.__init__`", - "concise_description": "Argument `object | str | Unknown` is not assignable to parameter `user_id` with type `str` in function `chat_sdk.types.Author.__init__`", - "severity": "error" - }, - { - "line": 857, - "column": 39, - "stop_line": 857, - "stop_column": 49, - "path": "src/chat_sdk/adapters/linear/adapter.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `object | str | Unknown` is not assignable to parameter `s` with type `str` in function `chat_sdk.types._parse_iso`", - "concise_description": "Argument `object | str | Unknown` is not assignable to parameter `s` with type `str` in function `chat_sdk.types._parse_iso`", - "severity": "error" - }, - { - "line": 859, - "column": 39, - "stop_line": 859, - "stop_column": 49, - "path": "src/chat_sdk/adapters/linear/adapter.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `object | str | Unknown` is not assignable to parameter `s` with type `str` in function `chat_sdk.types._parse_iso`", - "concise_description": "Argument `object | str | Unknown` is not assignable to parameter `s` with type `str` in function `chat_sdk.types._parse_iso`", - "severity": "error" - }, - { - "line": 895, - "column": 48, - "stop_line": 895, - "stop_column": 68, - "path": "src/chat_sdk/adapters/linear/adapter.py", - "code": -2, - "name": "bad-assignment", - "description": "`dict[str, str | Unknown]` is not assignable to `PostableAst | PostableCard | PostableMarkdown | PostableRaw | str | CardElement`", - "concise_description": "`dict[str, str | Unknown]` is not assignable to `PostableAst | PostableCard | PostableMarkdown | PostableRaw | str | CardElement`", - "severity": "error" - }, - { - "line": 974, - "column": 24, - "stop_line": 974, - "stop_column": 44, - "path": "src/chat_sdk/adapters/linear/adapter.py", - "code": -2, - "name": "not-async", - "description": "Type `object` is not awaitable", - "concise_description": "Type `object` is not awaitable", - "severity": "error" - }, - { - "line": 94, - "column": 43, - "stop_line": 94, - "stop_column": 46, - "path": "src/chat_sdk/adapters/linear/cards.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `object | str` is not assignable to parameter `text` with type `str` in function `_escape_markdown`", - "concise_description": "Argument `object | str` is not assignable to parameter `text` with type `str` in function `_escape_markdown`", - "severity": "error" - }, - { - "line": 99, - "column": 38, - "stop_line": 99, - "stop_column": 43, - "path": "src/chat_sdk/adapters/linear/cards.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `object | str` is not assignable to parameter `text` with type `str` in function `_escape_markdown`", - "concise_description": "Argument `object | str` is not assignable to parameter `text` with type `str` in function `_escape_markdown`", - "severity": "error" - }, - { - "line": 190, - "column": 43, - "stop_line": 190, - "stop_column": 50, - "path": "src/chat_sdk/adapters/linear/cards.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `list[str] | object` is not assignable to parameter `headers` with type `list[str]` in function `chat_sdk.shared.card_utils.render_gfm_table`", - "concise_description": "Argument `list[str] | object` is not assignable to parameter `headers` with type `list[str]` in function `chat_sdk.shared.card_utils.render_gfm_table`", - "severity": "error" - }, - { - "line": 190, - "column": 52, - "stop_line": 190, - "stop_column": 56, - "path": "src/chat_sdk/adapters/linear/cards.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `list[list[str]] | object` is not assignable to parameter `rows` with type `list[list[str]]` in function `chat_sdk.shared.card_utils.render_gfm_table`", - "concise_description": "Argument `list[list[str]] | object` is not assignable to parameter `rows` with type `list[list[str]]` in function `chat_sdk.shared.card_utils.render_gfm_table`", - "severity": "error" - }, - { - "line": 402, - "column": 24, - "stop_line": 402, - "stop_column": 43, - "path": "src/chat_sdk/adapters/slack/adapter.py", - "code": -2, - "name": "unsupported-operation", - "description": "`None` is not subscriptable", - "concise_description": "`None` is not subscriptable", - "severity": "error" - }, - { - "line": 403, - "column": 26, - "stop_line": 403, - "stop_column": 47, - "path": "src/chat_sdk/adapters/slack/adapter.py", - "code": -2, - "name": "unsupported-operation", - "description": "`None` is not subscriptable", - "concise_description": "`None` is not subscriptable", - "severity": "error" - }, - { - "line": 404, - "column": 25, - "stop_line": 404, - "stop_column": 45, - "path": "src/chat_sdk/adapters/slack/adapter.py", - "code": -2, - "name": "unsupported-operation", - "description": "`None` is not subscriptable", - "concise_description": "`None` is not subscriptable", - "severity": "error" - }, - { - "line": 647, - "column": 25, - "stop_line": 647, - "stop_column": 45, - "path": "src/chat_sdk/adapters/slack/adapter.py", - "code": -2, - "name": "not-async", - "description": "Type `object` is not awaitable", - "concise_description": "Type `object` is not awaitable", - "severity": "error" - }, - { - "line": 668, - "column": 12, - "stop_line": 668, - "stop_column": 63, - "path": "src/chat_sdk/adapters/slack/adapter.py", - "code": -2, - "name": "not-iterable", - "description": "`in` is not supported between `Literal['application/x-www-form-urlencoded']` and `None`", - "concise_description": "`in` is not supported between `Literal['application/x-www-form-urlencoded']` and `None`", - "severity": "error" - }, - { - "line": 1037, - "column": 44, - "stop_line": 1037, - "stop_column": 49, - "path": "src/chat_sdk/adapters/slack/adapter.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `dict[Any, Any]` is not assignable to parameter `modal` with type `ModalElement` in function `chat_sdk.adapters.slack.modals.modal_to_slack_view`", - "concise_description": "Argument `dict[Any, Any]` is not assignable to parameter `modal` with type `ModalElement` in function `chat_sdk.adapters.slack.modals.modal_to_slack_view`", - "severity": "error" - }, - { - "line": 1750, - "column": 86, - "stop_line": 1750, - "stop_column": 96, - "path": "src/chat_sdk/adapters/slack/adapter.py", - "code": -2, - "name": "bad-return", - "description": "Function declared to return `RawMessage`, but one or more paths are missing an explicit `return`", - "concise_description": "Function declared to return `RawMessage`, but one or more paths are missing an explicit `return`", - "severity": "error" - }, - { - "line": 1843, - "column": 10, - "stop_line": 1843, - "stop_column": 20, - "path": "src/chat_sdk/adapters/slack/adapter.py", - "code": -2, - "name": "bad-return", - "description": "Function declared to return `RawMessage`, but one or more paths are missing an explicit `return`", - "concise_description": "Function declared to return `RawMessage`, but one or more paths are missing an explicit `return`", - "severity": "error" - }, - { - "line": 2135, - "column": 10, - "stop_line": 2135, - "stop_column": 26, - "path": "src/chat_sdk/adapters/slack/adapter.py", - "code": -2, - "name": "bad-return", - "description": "Function declared to return `EphemeralMessage`, but one or more paths are missing an explicit `return`", - "concise_description": "Function declared to return `EphemeralMessage`, but one or more paths are missing an explicit `return`", - "severity": "error" - }, - { - "line": 2204, - "column": 10, - "stop_line": 2204, - "stop_column": 26, - "path": "src/chat_sdk/adapters/slack/adapter.py", - "code": -2, - "name": "bad-return", - "description": "Function declared to return `ScheduledMessage`, but one or more paths are missing an explicit `return`", - "concise_description": "Function declared to return `ScheduledMessage`, but one or more paths are missing an explicit `return`", - "severity": "error" - }, - { - "line": 2269, - "column": 46, - "stop_line": 2269, - "stop_column": 49, - "path": "src/chat_sdk/adapters/slack/adapter.py", - "code": -2, - "name": "bad-return", - "description": "Function declared to return `str`, but one or more paths are missing an explicit `return`", - "concise_description": "Function declared to return `str`, but one or more paths are missing an explicit `return`", - "severity": "error" - }, - { - "line": 2288, - "column": 106, - "stop_line": 2288, - "stop_column": 120, - "path": "src/chat_sdk/adapters/slack/adapter.py", - "code": -2, - "name": "bad-return", - "description": "Function declared to return `dict[str, str]`, but one or more paths are missing an explicit `return`", - "concise_description": "Function declared to return `dict[str, str]`, but one or more paths are missing an explicit `return`", - "severity": "error" - }, - { - "line": 2296, - "column": 36, - "stop_line": 2296, - "stop_column": 41, - "path": "src/chat_sdk/adapters/slack/adapter.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `dict[str, Any]` is not assignable to parameter `modal` with type `ModalElement` in function `chat_sdk.adapters.slack.modals.modal_to_slack_view`", - "concise_description": "Argument `dict[str, Any]` is not assignable to parameter `modal` with type `ModalElement` in function `chat_sdk.adapters.slack.modals.modal_to_slack_view`", - "severity": "error" - }, - { - "line": 2311, - "column": 74, - "stop_line": 2311, - "stop_column": 88, - "path": "src/chat_sdk/adapters/slack/adapter.py", - "code": -2, - "name": "bad-return", - "description": "Function declared to return `dict[str, str]`, but one or more paths are missing an explicit `return`", - "concise_description": "Function declared to return `dict[str, str]`, but one or more paths are missing an explicit `return`", - "severity": "error" - }, - { - "line": 2313, - "column": 36, - "stop_line": 2313, - "stop_column": 41, - "path": "src/chat_sdk/adapters/slack/adapter.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `dict[str, Any]` is not assignable to parameter `modal` with type `ModalElement` in function `chat_sdk.adapters.slack.modals.modal_to_slack_view`", - "concise_description": "Argument `dict[str, Any]` is not assignable to parameter `modal` with type `ModalElement` in function `chat_sdk.adapters.slack.modals.modal_to_slack_view`", - "severity": "error" - }, - { - "line": 2371, - "column": 92, - "stop_line": 2371, - "stop_column": 103, - "path": "src/chat_sdk/adapters/slack/adapter.py", - "code": -2, - "name": "bad-return", - "description": "Function declared to return `FetchResult`, but one or more paths are missing an explicit `return`", - "concise_description": "Function declared to return `FetchResult`, but one or more paths are missing an explicit `return`", - "severity": "error" - }, - { - "line": 2456, - "column": 53, - "stop_line": 2456, - "stop_column": 63, - "path": "src/chat_sdk/adapters/slack/adapter.py", - "code": -2, - "name": "bad-return", - "description": "Function declared to return `ThreadInfo`, but one or more paths are missing an explicit `return`", - "concise_description": "Function declared to return `ThreadInfo`, but one or more paths are missing an explicit `return`", - "severity": "error" - }, - { - "line": 2497, - "column": 101, - "stop_line": 2497, - "stop_column": 112, - "path": "src/chat_sdk/adapters/slack/adapter.py", - "code": -2, - "name": "bad-return", - "description": "Function declared to return `FetchResult`, but one or more paths are missing an explicit `return`", - "concise_description": "Function declared to return `FetchResult`, but one or more paths are missing an explicit `return`", - "severity": "error" - }, - { - "line": 2574, - "column": 97, - "stop_line": 2574, - "stop_column": 114, - "path": "src/chat_sdk/adapters/slack/adapter.py", - "code": -2, - "name": "bad-return", - "description": "Function declared to return `ListThreadsResult`, but one or more paths are missing an explicit `return`", - "concise_description": "Function declared to return `ListThreadsResult`, but one or more paths are missing an explicit `return`", - "severity": "error" - }, - { - "line": 2622, - "column": 60, - "stop_line": 2622, - "stop_column": 71, - "path": "src/chat_sdk/adapters/slack/adapter.py", - "code": -2, - "name": "bad-return", - "description": "Function declared to return `ChannelInfo`, but one or more paths are missing an explicit `return`", - "concise_description": "Function declared to return `ChannelInfo`, but one or more paths are missing an explicit `return`", - "severity": "error" - }, - { - "line": 583, + "line": 584, "column": 18, - "stop_line": 583, + "stop_line": 584, "stop_column": 26, "path": "src/chat_sdk/adapters/teams/adapter.py", "code": -2, @@ -1080,18 +12,6 @@ "concise_description": "Argument `str` is not assignable to parameter `type` with type `Literal['audio', 'file', 'image', 'video']` in function `chat_sdk.types.Attachment.__init__`", "severity": "error" }, - { - "line": 1782, - "column": 24, - "stop_line": 1782, - "stop_column": 44, - "path": "src/chat_sdk/adapters/teams/adapter.py", - "code": -2, - "name": "not-async", - "description": "Type `object` is not awaitable", - "concise_description": "Type `object` is not awaitable", - "severity": "error" - }, { "line": 138, "column": 48, @@ -1105,9 +25,9 @@ "severity": "error" }, { - "line": 706, + "line": 707, "column": 28, - "stop_line": 706, + "stop_line": 707, "stop_column": 37, "path": "src/chat_sdk/adapters/telegram/adapter.py", "code": -2, @@ -1117,9 +37,9 @@ "severity": "error" }, { - "line": 724, + "line": 725, "column": 27, - "stop_line": 724, + "stop_line": 725, "stop_column": 36, "path": "src/chat_sdk/adapters/telegram/adapter.py", "code": -2, @@ -1129,9 +49,9 @@ "severity": "error" }, { - "line": 1240, + "line": 1241, "column": 37, - "stop_line": 1240, + "stop_line": 1241, "stop_column": 46, "path": "src/chat_sdk/adapters/telegram/adapter.py", "code": -2, @@ -1141,9 +61,9 @@ "severity": "error" }, { - "line": 1729, + "line": 1730, "column": 20, - "stop_line": 1729, + "stop_line": 1730, "stop_column": 45, "path": "src/chat_sdk/adapters/telegram/adapter.py", "code": -2, @@ -1153,9 +73,9 @@ "severity": "error" }, { - "line": 1735, + "line": 1736, "column": 30, - "stop_line": 1735, + "stop_line": 1736, "stop_column": 55, "path": "src/chat_sdk/adapters/telegram/adapter.py", "code": -2, @@ -1165,69 +85,9 @@ "severity": "error" }, { - "line": 1914, - "column": 20, - "stop_line": 1914, - "stop_column": 40, - "path": "src/chat_sdk/adapters/telegram/adapter.py", - "code": -2, - "name": "not-async", - "description": "Type `object` is not awaitable", - "concise_description": "Type `object` is not awaitable", - "severity": "error" - }, - { - "line": 1918, - "column": 24, - "stop_line": 1918, - "stop_column": 36, - "path": "src/chat_sdk/adapters/telegram/adapter.py", - "code": -2, - "name": "not-async", - "description": "Type `object` is not awaitable", - "concise_description": "Type `object` is not awaitable", - "severity": "error" - }, - { - "line": 211, - "column": 33, - "stop_line": 211, - "stop_column": 40, - "path": "src/chat_sdk/adapters/whatsapp/adapter.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `dict[str, Any]` is not assignable to parameter `inbound` with type `WhatsAppInboundMessage` in function `WhatsAppAdapter._handle_inbound_message`", - "concise_description": "Argument `dict[str, Any]` is not assignable to parameter `inbound` with type `WhatsAppInboundMessage` in function `WhatsAppAdapter._handle_inbound_message`", - "severity": "error" - }, - { - "line": 413, - "column": 27, - "stop_line": 413, - "stop_column": 36, - "path": "src/chat_sdk/adapters/whatsapp/adapter.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `str | None` is not assignable to parameter `action_id` with type `str` in function `chat_sdk.types.ActionEvent.__init__`", - "concise_description": "Argument `str | None` is not assignable to parameter `action_id` with type `str` in function `chat_sdk.types.ActionEvent.__init__`", - "severity": "error" - }, - { - "line": 512, - "column": 24, - "stop_line": 512, - "stop_column": 31, - "path": "src/chat_sdk/adapters/whatsapp/adapter.py", - "code": -2, - "name": "bad-typed-dict-key", - "description": "`WhatsAppInboundMessage` is not assignable to TypedDict key `message` with type `dict[str, Any]`", - "concise_description": "`WhatsAppInboundMessage` is not assignable to TypedDict key `message` with type `dict[str, Any]`", - "severity": "error" - }, - { - "line": 714, + "line": 718, "column": 87, - "stop_line": 714, + "stop_line": 718, "stop_column": 100, "path": "src/chat_sdk/adapters/whatsapp/adapter.py", "code": -2, @@ -1237,9 +97,9 @@ "severity": "error" }, { - "line": 719, + "line": 723, "column": 51, - "stop_line": 719, + "stop_line": 723, "stop_column": 57, "path": "src/chat_sdk/adapters/whatsapp/adapter.py", "code": -2, @@ -1248,54 +108,6 @@ "concise_description": "TypedDict `WhatsAppCardResultInteractive` does not have key `text`", "severity": "error" }, - { - "line": 844, - "column": 51, - "stop_line": 844, - "stop_column": 76, - "path": "src/chat_sdk/adapters/whatsapp/adapter.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `dict[str, str]` is not assignable to parameter `message` with type `PostableAst | PostableCard | PostableMarkdown | PostableRaw | str | CardElement` in function `WhatsAppAdapter.post_message`", - "concise_description": "Argument `dict[str, str]` is not assignable to parameter `message` with type `PostableAst | PostableCard | PostableMarkdown | PostableRaw | str | CardElement` in function `WhatsAppAdapter.post_message`", - "severity": "error" - }, - { - "line": 954, - "column": 43, - "stop_line": 954, - "stop_column": 57, - "path": "src/chat_sdk/adapters/whatsapp/adapter.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `dict[str, Any]` is not assignable to parameter `message` with type `WhatsAppInboundMessage` in function `WhatsAppAdapter._extract_text_content`", - "concise_description": "Argument `dict[str, Any]` is not assignable to parameter `message` with type `WhatsAppInboundMessage` in function `WhatsAppAdapter._extract_text_content`", - "severity": "error" - }, - { - "line": 956, - "column": 47, - "stop_line": 956, - "stop_column": 61, - "path": "src/chat_sdk/adapters/whatsapp/adapter.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `dict[str, Any]` is not assignable to parameter `inbound` with type `WhatsAppInboundMessage` in function `WhatsAppAdapter._build_attachments`", - "concise_description": "Argument `dict[str, Any]` is not assignable to parameter `inbound` with type `WhatsAppInboundMessage` in function `WhatsAppAdapter._build_attachments`", - "severity": "error" - }, - { - "line": 1045, - "column": 20, - "stop_line": 1045, - "stop_column": 40, - "path": "src/chat_sdk/adapters/whatsapp/adapter.py", - "code": -2, - "name": "not-async", - "description": "Type `object` is not awaitable", - "concise_description": "Type `object` is not awaitable", - "severity": "error" - }, { "line": 231, "column": 16, @@ -1309,135 +121,15 @@ "severity": "error" }, { - "line": 772, - "column": 61, - "stop_line": 772, - "stop_column": 65, - "path": "src/chat_sdk/chat.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `Self@Chat` is not assignable to parameter `chat` with type `_ChatSingleton | None` in function `chat_sdk.thread.ThreadImpl.from_json`", - "concise_description": "Argument `Self@Chat` is not assignable to parameter `chat` with type `_ChatSingleton | None` in function `chat_sdk.thread.ThreadImpl.from_json`", - "severity": "error" - }, - { - "line": 774, - "column": 62, - "stop_line": 774, - "stop_column": 66, - "path": "src/chat_sdk/chat.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `Self@Chat` is not assignable to parameter `chat` with type `_ChatSingleton | None` in function `chat_sdk.channel.ChannelImpl.from_json`", - "concise_description": "Argument `Self@Chat` is not assignable to parameter `chat` with type `_ChatSingleton | None` in function `chat_sdk.channel.ChannelImpl.from_json`", - "severity": "error" - }, - { - "line": 1049, - "column": 13, - "stop_line": 1053, - "stop_column": 14, - "path": "src/chat_sdk/chat.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `_ChannelImplConfigForChat` is not assignable to parameter `config` with type `_ChannelImplConfigForThread | _ChannelImplConfigLazy | _ChannelImplConfigWithAdapter` in function `chat_sdk.channel.ChannelImpl.__init__`", - "concise_description": "Argument `_ChannelImplConfigForChat` is not assignable to parameter `config` with type `_ChannelImplConfigForThread | _ChannelImplConfigLazy | _ChannelImplConfigWithAdapter` in function `chat_sdk.channel.ChannelImpl.__init__`", - "severity": "error" - }, - { - "line": 1071, - "column": 21, - "stop_line": 1071, - "stop_column": 28, - "path": "src/chat_sdk/chat.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `ChannelImpl` is not assignable to parameter `channel` with type `Channel | None` in function `chat_sdk.types.SlashCommandEvent.__init__`\n `ChannelImpl.set_state` has type `(self: ChannelImpl, new_state: dict[str, Any], *, replace: bool = False) -> Coroutine[Unknown, Unknown, None]`, which is not assignable to `(self: ChannelImpl, state: dict[str, Any], *, replace: bool = False) -> Coroutine[Unknown, Unknown, None]`, the type of `Channel.set_state`", - "concise_description": "Argument `ChannelImpl` is not assignable to parameter `channel` with type `Channel | None` in function `chat_sdk.types.SlashCommandEvent.__init__`", - "severity": "error" - }, - { - "line": 1244, - "column": 20, - "stop_line": 1244, - "stop_column": 26, - "path": "src/chat_sdk/chat.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `ThreadImpl | None` is not assignable to parameter `thread` with type `Thread | None` in function `chat_sdk.types.ActionEvent.__init__`\n Property getter for `ThreadImpl.channel` has type `(self: ThreadImpl) -> ChannelImpl`, which is not assignable to `(self: ThreadImpl) -> Channel`, the property getter for `Thread.channel`", - "concise_description": "Argument `ThreadImpl | None` is not assignable to parameter `thread` with type `Thread | None` in function `chat_sdk.types.ActionEvent.__init__`", - "severity": "error" - }, - { - "line": 1306, - "column": 20, - "stop_line": 1306, - "stop_column": 26, - "path": "src/chat_sdk/chat.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `ThreadImpl` is not assignable to parameter `thread` with type `Thread | None` in function `chat_sdk.types.ReactionEvent.__init__`\n Property getter for `ThreadImpl.channel` has type `(self: ThreadImpl) -> ChannelImpl`, which is not assignable to `(self: ThreadImpl) -> Channel`, the property getter for `Thread.channel`", - "concise_description": "Argument `ThreadImpl` is not assignable to parameter `thread` with type `Thread | None` in function `chat_sdk.types.ReactionEvent.__init__`", - "severity": "error" - }, - { - "line": 1374, - "column": 13, - "stop_line": 1378, - "stop_column": 14, + "line": 1238, + "column": 25, + "stop_line": 1238, + "stop_column": 37, "path": "src/chat_sdk/chat.py", "code": -2, "name": "bad-argument-type", - "description": "Argument `_ChannelImplConfigForChat` is not assignable to parameter `config` with type `_ChannelImplConfigForThread | _ChannelImplConfigLazy | _ChannelImplConfigWithAdapter` in function `chat_sdk.channel.ChannelImpl.__init__`", - "concise_description": "Argument `_ChannelImplConfigForChat` is not assignable to parameter `config` with type `_ChannelImplConfigForThread | _ChannelImplConfigLazy | _ChannelImplConfigWithAdapter` in function `chat_sdk.channel.ChannelImpl.__init__`", - "severity": "error" - }, - { - "line": 1429, - "column": 21, - "stop_line": 1436, - "stop_column": 14, - "path": "src/chat_sdk/chat.py", - "code": -2, - "name": "not-async", - "description": "Type `Literal['channel']` is not awaitable", - "concise_description": "Type `Literal['channel']` is not awaitable", - "severity": "error" - }, - { - "line": 1429, - "column": 21, - "stop_line": 1436, - "stop_column": 14, - "path": "src/chat_sdk/chat.py", - "code": -2, - "name": "not-async", - "description": "Type `Literal['thread']` is not awaitable", - "concise_description": "Type `Literal['thread']` is not awaitable", - "severity": "error" - }, - { - "line": 1793, - "column": 17, - "stop_line": 1793, - "stop_column": 59, - "path": "src/chat_sdk/chat.py", - "code": -2, - "name": "not-async", - "description": "Type `None` is not awaitable", - "concise_description": "Type `None` is not awaitable", - "severity": "error" - }, - { - "line": 1818, - "column": 17, - "stop_line": 1818, - "stop_column": 60, - "path": "src/chat_sdk/chat.py", - "code": -2, - "name": "not-async", - "description": "Type `None` is not awaitable", - "concise_description": "Type `None` is not awaitable", + "description": "Argument `Channel | None` is not assignable to parameter `channel` with type `ChannelImpl | None` in function `Chat._store_modal_context`", + "concise_description": "Argument `Channel | None` is not assignable to parameter `channel` with type `ChannelImpl | None` in function `Chat._store_modal_context`", "severity": "error" }, { @@ -1537,9 +229,9 @@ "severity": "error" }, { - "line": 578, + "line": 593, "column": 27, - "stop_line": 578, + "stop_line": 593, "stop_column": 37, "path": "src/chat_sdk/thread.py", "code": -2, @@ -1549,9 +241,9 @@ "severity": "error" }, { - "line": 1129, + "line": 1144, "column": 23, - "stop_line": 1129, + "stop_line": 1144, "stop_column": 27, "path": "src/chat_sdk/thread.py", "code": -2, diff --git a/src/chat_sdk/adapters/discord/adapter.py b/src/chat_sdk/adapters/discord/adapter.py index aed5d16..9261e3e 100644 --- a/src/chat_sdk/adapters/discord/adapter.py +++ b/src/chat_sdk/adapters/discord/adapter.py @@ -9,12 +9,13 @@ from __future__ import annotations import hmac +import inspect import json import os import re from contextvars import ContextVar from datetime import datetime, timezone -from typing import Any +from typing import Any, Literal, cast from urllib.parse import quote from chat_sdk.adapters.discord.cards import ( @@ -55,6 +56,7 @@ LockScope, Message, MessageMetadata, + PostableRaw, RawMessage, ReactionEvent, SlashCommandEvent, @@ -380,7 +382,11 @@ def _handle_application_command_interaction( self._logger.warn("Chat instance not initialized, ignoring interaction") return - data = interaction.get("data", {}) + # `interaction["data"]` is a union of several TypedDicts (one per + # interaction type). Cast to a plain dict so we can access shared + # fields like `name` and `options` without pyrefly rejecting keys + # that only appear on one variant. + data = cast("dict[str, Any]", interaction.get("data", {})) command_name = data.get("name") if not command_name: self._logger.warn("No command name in application command interaction") @@ -1160,7 +1166,7 @@ async def stream( accumulated += text - postable: AdapterPostableMessage = {"raw": accumulated} + postable: AdapterPostableMessage = PostableRaw(raw=accumulated) if message_id: await self.edit_message(thread_id, message_id, postable) @@ -1257,7 +1263,7 @@ def _parse_discord_message(self, raw: dict[str, Any], thread_id: str) -> Message ], ) - def _get_attachment_type(self, mime_type: str | None) -> str: + def _get_attachment_type(self, mime_type: str | None) -> Literal["audio", "file", "image", "video"]: """Determine attachment type from MIME type.""" if not mime_type: return "file" @@ -1399,20 +1405,26 @@ async def _discord_fetch( async def _get_request_body(self, request: Any) -> str: """Extract the request body as a string.""" - if hasattr(request, "body"): - body = request.body + # `hasattr` narrows `Any` to `object` and kills downstream type info, + # so we use `getattr(..., default)` which preserves `Any`. The + # `await` below is safe because `inspect.isawaitable` guards it. + body = getattr(request, "body", None) + if body is not None: if callable(body): body = body() if hasattr(body, "read"): - raw = await body.read() if hasattr(body.read, "__await__") else body.read() + raw_read = body.read + raw = await raw_read() if inspect.iscoroutinefunction(raw_read) else raw_read() return raw.decode("utf-8") if isinstance(raw, bytes) else raw return body.decode("utf-8") if isinstance(body, bytes) else str(body) - if hasattr(request, "text"): - if callable(request.text): - return await request.text() - return request.text - if hasattr(request, "data"): - data = request.data + text_attr = getattr(request, "text", None) + if text_attr is not None: + if callable(text_attr): + result = text_attr() + return str(await result if inspect.isawaitable(result) else result) + return text_attr + data = getattr(request, "data", None) + if data is not None: return data.decode("utf-8") if isinstance(data, bytes) else str(data) return "" diff --git a/src/chat_sdk/adapters/discord/cards.py b/src/chat_sdk/adapters/discord/cards.py index 223edef..c5114b3 100644 --- a/src/chat_sdk/adapters/discord/cards.py +++ b/src/chat_sdk/adapters/discord/cards.py @@ -7,7 +7,7 @@ from __future__ import annotations -from typing import Any +from typing import Any, cast from chat_sdk.adapters.discord.types import DiscordActionRow, DiscordButton from chat_sdk.cards import ( @@ -113,12 +113,12 @@ def _process_child( elif child_type == "fields": _convert_fields_element(child, fields) # type: ignore[arg-type] elif child_type == "link": - label = child.get("label", "") # type: ignore[union-attr] - url = child.get("url", "") # type: ignore[union-attr] + label = cast("str", child.get("label", "")) + url = cast("str", child.get("url", "")) text_parts.append(f"[{_convert_emoji(label)}]({url})") elif child_type == "table": - headers = child.get("headers", []) # type: ignore[union-attr] - rows = child.get("rows", []) # type: ignore[union-attr] + headers = cast("list[str]", child.get("headers", [])) + rows = cast("list[list[str]]", child.get("rows", [])) text_parts.append("\n".join(render_gfm_table(headers, rows))) else: text = card_child_to_fallback_text(child) @@ -270,8 +270,8 @@ def _child_to_fallback_text(child: CardChild) -> str | None: if (t := _child_to_fallback_text(c)) ) if child_type == "table": - headers = child.get("headers", []) # type: ignore[union-attr] - rows = child.get("rows", []) # type: ignore[union-attr] + headers = cast("list[str]", child.get("headers", [])) + rows = cast("list[list[str]]", child.get("rows", [])) return f"```\n{table_element_to_ascii(headers, rows)}\n```" if child_type == "divider": return "---" diff --git a/src/chat_sdk/adapters/github/adapter.py b/src/chat_sdk/adapters/github/adapter.py index 0e3501b..7095185 100644 --- a/src/chat_sdk/adapters/github/adapter.py +++ b/src/chat_sdk/adapters/github/adapter.py @@ -11,6 +11,7 @@ import asyncio import hashlib import hmac +import inspect import json import os import re @@ -26,6 +27,7 @@ GitHubIssueComment, GitHubRawMessage, GitHubReactionContent, + GitHubRepository, GitHubReviewComment, GitHubThreadId, GitHubUser, @@ -51,6 +53,7 @@ LockScope, Message, MessageMetadata, + PostableMarkdown, RawMessage, StreamChunk, StreamOptions, @@ -365,7 +368,7 @@ def _handle_review_comment( def _parse_issue_comment( self, comment: GitHubIssueComment, - repository: dict[str, Any], + repository: GitHubRepository, pr_number: int, thread_id: str, thread_type: Literal["pr", "issue"] = "pr", @@ -409,7 +412,7 @@ def _parse_issue_comment( def _parse_review_comment( self, comment: GitHubReviewComment, - repository: dict[str, Any], + repository: GitHubRepository, pr_number: int, thread_id: str, ) -> Message: @@ -528,7 +531,7 @@ async def stream( text += chunk elif hasattr(chunk, "type") and chunk.type == "markdown_text": text += chunk.text - return await self.post_message(thread_id, {"markdown": text}) + return await self.post_message(thread_id, PostableMarkdown(markdown=text)) @staticmethod def _build_raw_message(decoded: Any, comment: dict[str, Any]) -> dict[str, Any]: @@ -889,8 +892,11 @@ async def fetch_channel_info(self, channel_id: str) -> ChannelInfo: def parse_message(self, raw: GitHubRawMessage) -> Message: """Parse a raw message into normalized format.""" + # `raw` is a GitHubRawMessage dict with keys of varying value-types + # depending on the event variant; `.get()` unions to `object | str`. + # Narrow via casts where we've runtime-verified via `raw.get("type")`. if raw.get("type") == "issue_comment": - thread_type = raw.get("thread_type", "pr") or "pr" + thread_type = cast("Literal['issue', 'pr']", raw.get("thread_type", "pr") or "pr") thread_id = self.encode_thread_id( GitHubThreadId( owner=raw["repository"]["owner"]["login"], @@ -900,10 +906,17 @@ def parse_message(self, raw: GitHubRawMessage) -> Message: ) ) return self._parse_issue_comment( - raw["comment"], raw["repository"], raw["pr_number"], thread_id, thread_type + cast("GitHubIssueComment", raw["comment"]), + raw["repository"], + raw["pr_number"], + thread_id, + thread_type, ) else: - root_comment_id = raw["comment"].get("in_reply_to_id") or raw["comment"]["id"] + root_comment_id = cast( + "int | None", + raw["comment"].get("in_reply_to_id") or raw["comment"]["id"], + ) thread_id = self.encode_thread_id( GitHubThreadId( owner=raw["repository"]["owner"]["login"], @@ -912,7 +925,12 @@ def parse_message(self, raw: GitHubRawMessage) -> Message: review_comment_id=root_comment_id, ) ) - return self._parse_review_comment(raw["comment"], raw["repository"], raw["pr_number"], thread_id) + return self._parse_review_comment( + cast("GitHubReviewComment", raw["comment"]), + raw["repository"], + raw["pr_number"], + thread_id, + ) def render_formatted(self, content: FormattedContent) -> str: """Render formatted content to GitHub markdown.""" @@ -1069,10 +1087,14 @@ async def _get_installation_id(self, owner: str, repo: str) -> int | None: @staticmethod async def _get_request_body(request: Any) -> str: """Extract body text from a request object.""" - if hasattr(request, "text") and callable(request.text): - return await request.text() - if hasattr(request, "body"): - body = request.body + # `hasattr` narrows `Any` → `object` (which is not awaitable), so + # `getattr(..., None)` keeps `Any` for the framework duck-type path. + text_attr = getattr(request, "text", None) + if text_attr is not None and callable(text_attr): + result = text_attr() + return str(await result if inspect.isawaitable(result) else result) + body = getattr(request, "body", None) + if body is not None: return body.decode("utf-8") if isinstance(body, bytes) else str(body) return "" diff --git a/src/chat_sdk/adapters/github/cards.py b/src/chat_sdk/adapters/github/cards.py index 33c2df2..3790864 100644 --- a/src/chat_sdk/adapters/github/cards.py +++ b/src/chat_sdk/adapters/github/cards.py @@ -6,7 +6,7 @@ from __future__ import annotations -from typing import Any +from typing import Any, cast from chat_sdk.cards import ( CardChild, @@ -92,42 +92,50 @@ def card_to_github_markdown(card: CardElement) -> str: def _render_child(child: CardChild) -> list[str]: - """Render a card child element to markdown lines.""" + """Render a card child element to markdown lines. + + The per-type helpers below accept `dict[str, Any]` because they access + dynamic keys (`content`, `label`, etc.) that are specific to one + variant of the `CardChild` union. The narrowing happens via the + `child_type` check, so casting to `dict[str, Any]` at the call sites + is safe and keeps the helpers simple. + """ child_type = child.get("type", "") if child_type == "text": - return _render_text(child) + return _render_text(cast("dict[str, Any]", child)) if child_type == "fields": - return _render_fields(child) + return _render_fields(cast("dict[str, Any]", child)) if child_type == "actions": - return _render_actions(child) + return _render_actions(cast("dict[str, Any]", child)) if child_type == "section": # Flatten section children result: list[str] = [] - for section_child in child.get("children", []): + section_children = cast("list[CardChild]", child.get("children", [])) + for section_child in section_children: result.extend(_render_child(section_child)) return result if child_type == "image": - alt = child.get("alt", "") - url = child.get("url", "") + alt = cast("str", child.get("alt", "")) + url = cast("str", child.get("url", "")) if alt: return [f"![{_escape_markdown(alt)}]({url})"] return [f"![]({url})"] if child_type == "link": - label = child.get("label", "") - url = child.get("url", "") + label = cast("str", child.get("label", "")) + url = cast("str", child.get("url", "")) return [f"[{_escape_markdown(label)}]({url})"] if child_type == "divider": return ["---"] if child_type == "table": - return _render_table(child) + return _render_table(cast("dict[str, Any]", child)) # Fallback text = card_child_to_fallback_text(child) @@ -233,7 +241,7 @@ def _child_to_plain_text(child: CardChild) -> str | None: return None if child_type == "table": - return "\n".join(_render_table(child)) + return "\n".join(_render_table(cast("dict[str, Any]", child))) if child_type == "section": return "\n".join( diff --git a/src/chat_sdk/adapters/google_chat/adapter.py b/src/chat_sdk/adapters/google_chat/adapter.py index d2ed9cc..be0a117 100644 --- a/src/chat_sdk/adapters/google_chat/adapter.py +++ b/src/chat_sdk/adapters/google_chat/adapter.py @@ -11,13 +11,14 @@ import asyncio import contextlib +import inspect import json import os import re import time from collections.abc import AsyncIterable, Awaitable, Callable from datetime import datetime, timezone -from typing import Any +from typing import Any, NoReturn from chat_sdk.adapters.google_chat.cards import card_to_google_card from chat_sdk.adapters.google_chat.format_converter import GoogleChatFormatConverter @@ -71,6 +72,7 @@ ListThreadsResult, LockScope, Message, + PostableMarkdown, RawMessage, ReactionEvent, StateAdapter, @@ -759,17 +761,22 @@ async def handle_webhook( except Exception: pass - # Parse request body + # Parse request body. `hasattr` narrows `Any` → `object` (not + # awaitable); `getattr(..., None)` preserves `Any` for the + # framework duck-typed path. body: str - if hasattr(request, "text") and callable(request.text): - body = await request.text() - elif hasattr(request, "body"): - raw_body = request.body - body = raw_body.decode("utf-8") if isinstance(raw_body, bytes) else str(raw_body) - elif isinstance(request, dict): - body = json.dumps(request) + text_attr = getattr(request, "text", None) + if text_attr is not None and callable(text_attr): + result = text_attr() + body = str(await result if inspect.isawaitable(result) else result) else: - body = str(request) + raw_body = getattr(request, "body", None) + if raw_body is not None: + body = raw_body.decode("utf-8") if isinstance(raw_body, bytes) else str(raw_body) + elif isinstance(request, dict): + body = json.dumps(request) + else: + body = str(request) self._logger.debug("GChat webhook raw body", {"body": body}) @@ -1595,7 +1602,7 @@ async def stream( accumulated += chunk elif hasattr(chunk, "type") and chunk.type == "markdown_text": accumulated += chunk.text - return await self.post_message(thread_id, {"markdown": accumulated}) + return await self.post_message(thread_id, PostableMarkdown(markdown=accumulated)) # ========================================================================= # Reactions @@ -2645,11 +2652,12 @@ async def _fetch_data() -> bytes: # Error handling # ========================================================================= - def _handle_google_chat_error(self, error: Any, context: str | None = None) -> Any: + def _handle_google_chat_error(self, error: Any, context: str | None = None) -> NoReturn: """Handle Google Chat API errors with proper error classification. - Always re-raises. Returns Never (but typed as Any so callers satisfy - return type without explicit annotation). + Always re-raises — the `NoReturn` annotation lets type checkers skip + the "missing return" warning for callers that rely on this to + propagate out of a `try/except` block. """ error_code = getattr(error, "code", None) error_message = getattr(error, "message", str(error)) diff --git a/src/chat_sdk/adapters/google_chat/cards.py b/src/chat_sdk/adapters/google_chat/cards.py index 36e1070..c8205cf 100644 --- a/src/chat_sdk/adapters/google_chat/cards.py +++ b/src/chat_sdk/adapters/google_chat/cards.py @@ -8,7 +8,7 @@ from __future__ import annotations -from typing import Any +from typing import Any, cast from chat_sdk.cards import ( CardChild, @@ -73,7 +73,7 @@ def card_to_google_card( sections.append({"widgets": current_widgets}) current_widgets = [] # Convert section as its own section - section_widgets = _convert_section_to_widgets(child, opts.get("endpoint_url")) + section_widgets = _convert_section_to_widgets(cast("dict[str, Any]", child), opts.get("endpoint_url")) sections.append({"widgets": section_widgets}) else: # Add to current widgets @@ -108,24 +108,30 @@ def _convert_child_to_widgets( child: CardChild, endpoint_url: str | None = None, ) -> list[dict[str, Any]]: - """Convert a card child element to Google Chat widgets.""" + """Convert a card child element to Google Chat widgets. + + The per-type helpers below accept `dict[str, Any]` because they reach + for variant-specific keys (`content`, `label`, `url`, etc.). The + `child_type` check narrows the union, so the `cast` is safe and lets + the helpers stay simple. + """ child_type = child.get("type") if child_type == "text": - return [_convert_text_to_widget(child)] + return [_convert_text_to_widget(cast("dict[str, Any]", child))] elif child_type == "image": - return [_convert_image_to_widget(child)] + return [_convert_image_to_widget(cast("dict[str, Any]", child))] elif child_type == "divider": return [_convert_divider_to_widget()] elif child_type == "actions": - return [_convert_actions_to_widget(child, endpoint_url)] + return [_convert_actions_to_widget(cast("dict[str, Any]", child), endpoint_url)] elif child_type == "section": - return _convert_section_to_widgets(child, endpoint_url) + return _convert_section_to_widgets(cast("dict[str, Any]", child), endpoint_url) elif child_type == "fields": - return _convert_fields_to_widgets(child) + return _convert_fields_to_widgets(cast("dict[str, Any]", child)) elif child_type == "link": - label = child.get("label", "") - url = child.get("url", "") + label = cast("str", child.get("label", "")) + url = cast("str", child.get("url", "")) return [ { "textParagraph": { @@ -134,7 +140,7 @@ def _convert_child_to_widgets( }, ] elif child_type == "table": - return [_convert_table_to_widget(child)] + return [_convert_table_to_widget(cast("dict[str, Any]", child))] else: text = card_child_to_fallback_text(child) if text: diff --git a/src/chat_sdk/adapters/google_chat/workspace_events.py b/src/chat_sdk/adapters/google_chat/workspace_events.py index a216031..5d29c7a 100644 --- a/src/chat_sdk/adapters/google_chat/workspace_events.py +++ b/src/chat_sdk/adapters/google_chat/workspace_events.py @@ -17,6 +17,7 @@ import asyncio import base64 +import inspect import json from dataclasses import dataclass from typing import Any @@ -340,10 +341,12 @@ async def _get_access_token( elif isinstance(auth, WorkspaceEventsAuthADC): return await _get_adc_token(scopes, http_session) elif isinstance(auth, dict) and "auth" in auth: - # Custom auth - assume it provides a token directly + # Custom auth — `auth["auth"]` can be sync or async. `isawaitable` + # narrows the result so sync callables work without a TypeError. custom_auth = auth["auth"] if callable(custom_auth): - return await custom_auth() + result = custom_auth() + return str(await result if inspect.isawaitable(result) else result) return str(custom_auth) else: raise ValueError("Unsupported auth type for workspace events") diff --git a/src/chat_sdk/adapters/linear/adapter.py b/src/chat_sdk/adapters/linear/adapter.py index dfc91dd..fa886a7 100644 --- a/src/chat_sdk/adapters/linear/adapter.py +++ b/src/chat_sdk/adapters/linear/adapter.py @@ -11,12 +11,13 @@ import asyncio import hashlib import hmac +import inspect import json import os import re import time from datetime import datetime, timezone -from typing import Any +from typing import Any, cast from chat_sdk.adapters.linear.cards import card_to_linear_markdown from chat_sdk.adapters.linear.format_converter import LinearFormatConverter @@ -50,6 +51,7 @@ LockScope, Message, MessageMetadata, + PostableRaw, RawMessage, StreamOptions, ThreadInfo, @@ -299,13 +301,15 @@ async def handle_webhook( ) return self._make_response("Webhook expired", 401) - # Handle events based on type + # Handle events based on type. The payload shape is determined by + # `type` at runtime — cast to the matching TypedDict so each handler + # sees the right variant. payload_type = payload.get("type") if payload_type == "Comment": if payload.get("action") == "create": - self._handle_comment_created(payload, options) + self._handle_comment_created(cast("CommentWebhookPayload", payload), options) elif payload_type == "Reaction": - self._handle_reaction(payload) + self._handle_reaction(cast("ReactionWebhookPayload", payload)) return self._make_response("ok", 200) @@ -338,18 +342,23 @@ def _handle_comment_created( self._logger.warn("Chat instance not initialized, ignoring comment") return - data = payload.get("data", {}) - actor = payload.get("actor", {}) + # TypedDict `.get()` unions every field-type from the union of shapes + # (comment-created payloads vs older camel/snake fallbacks), producing + # `object | str`. Cast to `str` where we've runtime-narrowed via the + # truthy check — the dispatch block already filtered to `Comment` + # events, so these keys are known to be strings. + data = cast("LinearCommentData", payload.get("data", {})) + actor = cast("LinearWebhookActor", payload.get("actor", {})) # Skip non-issue comments - issue_id = data.get("issueId") or data.get("issue_id") + issue_id = cast("str | None", data.get("issueId") or data.get("issue_id")) if not issue_id: self._logger.debug("Ignoring non-issue comment", {"commentId": data.get("id")}) return # Determine thread parent_id = data.get("parentId") or data.get("parent_id") - root_comment_id = parent_id or data.get("id") + root_comment_id = cast("str | None", parent_id or data.get("id")) thread_id = self.encode_thread_id( LinearThreadId( issue_id=issue_id, @@ -393,8 +402,12 @@ def _build_message( thread_id: str, ) -> Message: """Build a Message from a Linear comment and actor.""" - text = comment.get("body", "") - user_id = comment.get("userId") or comment.get("user_id", "") + # `comment.get("body")` unions every value type across the TypedDict + # variants, giving `object | str`. Cast to `str` where the runtime + # shape guarantees a string (Linear webhook `Comment` payloads + # always have `body`, `userId`, `createdAt`, `updatedAt` as strings). + text = cast("str", comment.get("body", "")) + user_id = cast("str", comment.get("userId") or comment.get("user_id", "")) author = Author( user_id=user_id, @@ -406,8 +419,8 @@ def _build_message( formatted = self._format_converter.to_ast(text) - created_at = comment.get("createdAt") or comment.get("created_at", "") - updated_at = comment.get("updatedAt") or comment.get("updated_at", "") + created_at = cast("str", comment.get("createdAt") or comment.get("created_at", "")) + updated_at = cast("str", comment.get("updatedAt") or comment.get("updated_at", "")) return Message( id=comment.get("id", ""), @@ -740,7 +753,7 @@ def _comment_node_to_message( "userId": user_id, "createdAt": node.get("createdAt", ""), "updatedAt": node.get("updatedAt", ""), - "url": node.get("url"), + "url": node.get("url", ""), }, ), author=Author( @@ -833,13 +846,19 @@ def channel_id_from_thread_id(self, thread_id: str) -> str: return f"linear:{decoded.issue_id}" def parse_message(self, raw: LinearRawMessage) -> Message: - """Parse platform message format to normalized format.""" + """Parse platform message format to normalized format. + + TypedDict `.get()` unions every value-type across camel/snake-case + aliases, producing `object | str`. Cast the string fields we know + are strings at runtime so downstream constructors (`Author`, + `_parse_iso`) receive `str` instead of `object`. + """ comment = raw.get("comment", {}) - text = comment.get("body", "") - user_id = comment.get("userId") or comment.get("user_id", "") + text = cast("str", comment.get("body", "")) + user_id = cast("str", comment.get("userId") or comment.get("user_id", "")) - created_at = comment.get("createdAt") or comment.get("created_at", "") - updated_at = comment.get("updatedAt") or comment.get("updated_at", "") + created_at = cast("str", comment.get("createdAt") or comment.get("created_at", "")) + updated_at = cast("str", comment.get("updatedAt") or comment.get("updated_at", "")) return Message( id=comment.get("id", ""), @@ -892,7 +911,7 @@ async def stream( # Post the accumulated text as a single comment if accumulated: - postable: AdapterPostableMessage = {"raw": accumulated} + postable: AdapterPostableMessage = PostableRaw(raw=accumulated) result = await self.post_message(thread_id, postable) message_id = result.id @@ -961,20 +980,26 @@ async def _graphql_query( async def _get_request_body(self, request: Any) -> str: """Extract the request body as a string.""" - if hasattr(request, "body"): - body = request.body + # `hasattr` narrows `Any` → `object` (which is not awaitable); using + # `getattr(..., None)` preserves `Any` so the downstream `await` on + # framework-returned coroutines type-checks. + body = getattr(request, "body", None) + if body is not None: if callable(body): body = body() if hasattr(body, "read"): - raw = await body.read() if hasattr(body.read, "__await__") else body.read() + raw_read = body.read + raw = await raw_read() if inspect.iscoroutinefunction(raw_read) else raw_read() return raw.decode("utf-8") if isinstance(raw, bytes) else raw return body.decode("utf-8") if isinstance(body, bytes) else str(body) - if hasattr(request, "text"): - if callable(request.text): - return await request.text() - return request.text - if hasattr(request, "data"): - data = request.data + text_attr = getattr(request, "text", None) + if text_attr is not None: + if callable(text_attr): + result = text_attr() + return str(await result if inspect.isawaitable(result) else result) + return text_attr + data = getattr(request, "data", None) + if data is not None: return data.decode("utf-8") if isinstance(data, bytes) else str(data) return "" diff --git a/src/chat_sdk/adapters/linear/cards.py b/src/chat_sdk/adapters/linear/cards.py index 7f7d938..0860363 100644 --- a/src/chat_sdk/adapters/linear/cards.py +++ b/src/chat_sdk/adapters/linear/cards.py @@ -9,6 +9,8 @@ from __future__ import annotations +from typing import cast + from chat_sdk.cards import ( ActionsElement, CardChild, @@ -88,14 +90,14 @@ def _render_child(child: CardChild) -> list[str]: result.extend(_render_child(c)) return result if child_type == "image": - alt = child.get("alt", "") # type: ignore[union-attr] - url = child.get("url", "") # type: ignore[union-attr] + alt = cast("str", child.get("alt", "")) + url = cast("str", child.get("url", "")) if alt: return [f"![{_escape_markdown(alt)}]({url})"] return [f"![]({url})"] if child_type == "link": - label = child.get("label", "") # type: ignore[union-attr] - url = child.get("url", "") # type: ignore[union-attr] + label = cast("str", child.get("label", "")) + url = cast("str", child.get("url", "")) return [f"[{_escape_markdown(label)}]({url})"] if child_type == "divider": return ["---"] @@ -185,8 +187,8 @@ def _child_to_plain_text(child: CardChild) -> str | None: # Actions are interactive-only -- exclude from fallback text. return None if child_type == "table": - headers = child.get("headers", []) # type: ignore[union-attr] - rows = child.get("rows", []) # type: ignore[union-attr] + headers = cast("list[str]", child.get("headers", [])) + rows = cast("list[list[str]]", child.get("rows", [])) return "\n".join(render_gfm_table(headers, rows)) if child_type == "section": parts = [ diff --git a/src/chat_sdk/adapters/slack/adapter.py b/src/chat_sdk/adapters/slack/adapter.py index c83c1d7..fea619c 100644 --- a/src/chat_sdk/adapters/slack/adapter.py +++ b/src/chat_sdk/adapters/slack/adapter.py @@ -13,6 +13,7 @@ import contextvars import hashlib import hmac +import inspect import json import os import re @@ -21,7 +22,7 @@ from collections.abc import AsyncIterable, Awaitable, Callable from contextvars import ContextVar from datetime import datetime, timezone -from typing import Any +from typing import Any, NoReturn, cast from urllib.parse import parse_qs from chat_sdk.adapters.slack.cards import ( @@ -51,6 +52,7 @@ ) from chat_sdk.emoji import convert_emoji_placeholders, emoji_to_slack, resolve_emoji_from_slack from chat_sdk.logger import ConsoleLogger, Logger +from chat_sdk.modals import ModalElement from chat_sdk.shared.adapter_utils import extract_card, extract_files from chat_sdk.shared.errors import AdapterRateLimitError, AuthenticationError, ValidationError from chat_sdk.types import ( @@ -434,6 +436,10 @@ async def get_installation(self, team_id: str) -> SlackInstallation | None: bot_user_id = (stored.get("botUserId") or stored.get("bot_user_id") or "") if isinstance(stored, dict) else "" team_name = (stored.get("teamName") or stored.get("team_name") or "") if isinstance(stored, dict) else "" if self._encryption_key and is_encrypted_token_data(bot_token_raw): + # `is_encrypted_token_data` is a runtime type guard but doesn't + # carry TypeGuard narrowing, so pyrefly still sees `None`. Assert + # to collapse the Optional for the field access below. + assert bot_token_raw is not None decrypted = decrypt_token( EncryptedTokenData( iv=bot_token_raw["iv"], @@ -679,16 +685,22 @@ async def handle_webhook(self, request: Any, options: WebhookOptions | None = No Returns a dict with ``body`` and ``status`` keys. """ - # Read the raw body - if hasattr(request, "text") and callable(request.text): - body: str = await request.text() - elif hasattr(request, "body"): - raw = request.body - if asyncio.iscoroutine(raw) or asyncio.isfuture(raw): - raw = await raw - body = raw.decode("utf-8") if isinstance(raw, (bytes, bytearray)) else str(raw) + # Read the raw body. `hasattr` narrows `Any` → `object` (not + # awaitable), so we use `getattr(..., None)` to preserve the + # `Any` type across the duck-typed framework branches. + text_attr = getattr(request, "text", None) + body: str + if text_attr is not None and callable(text_attr): + result = text_attr() + body = str(await result if inspect.isawaitable(result) else result) else: - body = str(request) + raw = getattr(request, "body", None) + if raw is not None: + if asyncio.iscoroutine(raw) or asyncio.isfuture(raw): + raw = await raw + body = raw.decode("utf-8") if isinstance(raw, (bytes, bytearray)) else str(raw) + else: + body = str(request) self._logger.debug("Slack webhook raw body", {"body": body[:500]}) @@ -701,7 +713,7 @@ async def handle_webhook(self, request: Any, options: WebhookOptions | None = No return {"body": "Invalid signature", "status": 401} # Form-urlencoded payloads (interactive + slash commands) - content_type = headers.get("content-type", headers.get("Content-Type", "")) + content_type = headers.get("content-type") or headers.get("Content-Type") or "" if "application/x-www-form-urlencoded" in content_type: params = parse_qs(body, keep_blank_values=True) @@ -1071,7 +1083,7 @@ def _modal_response_to_slack(self, response: ModalResponse, context_id: str | No private_metadata=modal.get("private_metadata"), ) ) - view = modal_to_slack_view(modal, metadata) + view = modal_to_slack_view(cast("ModalElement", modal), metadata) return {"response_action": response.action, "view": view} return {} @@ -2330,7 +2342,7 @@ async def open_modal(self, trigger_id: str, modal: dict[str, Any], context_id: s private_metadata=modal.get("private_metadata"), ) ) - view = modal_to_slack_view(modal, metadata) + view = modal_to_slack_view(cast("ModalElement", modal), metadata) self._logger.debug( "Slack API: views.open", @@ -2347,7 +2359,7 @@ async def open_modal(self, trigger_id: str, modal: dict[str, Any], context_id: s async def update_modal(self, view_id: str, modal: dict[str, Any]) -> dict[str, str]: """Update an existing modal using views.update.""" - view = modal_to_slack_view(modal) + view = modal_to_slack_view(cast("ModalElement", modal)) try: client = self._get_client() @@ -2752,10 +2764,12 @@ def render_formatted(self, content: FormattedContent) -> str: # Error handling # ================================================================== - def _handle_slack_error(self, error: Any) -> None: + def _handle_slack_error(self, error: Any) -> NoReturn: """Re-raise Slack errors with appropriate SDK error types. - Never returns (always raises). + Always raises — the `NoReturn` annotation lets type checkers skip + the "missing return" warning for callers that rely on this to + propagate out of a `try/except` block. """ # slack_sdk's SlackApiError has a .response attribute (SlackResponse) # SlackResponse has a .data dict and an .get() method diff --git a/src/chat_sdk/adapters/teams/adapter.py b/src/chat_sdk/adapters/teams/adapter.py index f915002..bfdc068 100644 --- a/src/chat_sdk/adapters/teams/adapter.py +++ b/src/chat_sdk/adapters/teams/adapter.py @@ -10,6 +10,7 @@ import asyncio import base64 +import inspect import json import os import re @@ -1769,20 +1770,25 @@ async def _verify_bot_framework_token(self, request: Any) -> Any | None: async def _get_request_body(self, request: Any) -> str: """Extract the request body as a string.""" - if hasattr(request, "body"): - body = request.body + # `hasattr` narrows `Any` → `object` (not awaitable); using + # `getattr(..., None)` preserves `Any` for framework duck-typing. + body = getattr(request, "body", None) + if body is not None: if callable(body): body = body() if hasattr(body, "read"): - raw = await body.read() if hasattr(body.read, "__await__") else body.read() + raw_read = body.read + raw = await raw_read() if inspect.iscoroutinefunction(raw_read) else raw_read() return raw.decode("utf-8") if isinstance(raw, bytes) else raw return body.decode("utf-8") if isinstance(body, bytes) else str(body) - if hasattr(request, "text"): - if callable(request.text): - return await request.text() - return request.text - if hasattr(request, "data"): - data = request.data + text_attr = getattr(request, "text", None) + if text_attr is not None: + if callable(text_attr): + result = text_attr() + return str(await result if inspect.isawaitable(result) else result) + return text_attr + data = getattr(request, "data", None) + if data is not None: return data.decode("utf-8") if isinstance(data, bytes) else str(data) return "" diff --git a/src/chat_sdk/adapters/telegram/adapter.py b/src/chat_sdk/adapters/telegram/adapter.py index 0da6f16..26bcf6b 100644 --- a/src/chat_sdk/adapters/telegram/adapter.py +++ b/src/chat_sdk/adapters/telegram/adapter.py @@ -12,6 +12,7 @@ import asyncio import contextlib import hmac +import inspect import json import math import os @@ -1910,12 +1911,17 @@ def _resolve_thread_id(self, value: str) -> TelegramThreadId: @staticmethod async def _get_request_body(request: Any) -> str: """Extract body text from a framework-agnostic request object.""" - if hasattr(request, "text") and callable(request.text): - return await request.text() - if hasattr(request, "body"): - body = request.body + # `hasattr` narrows `Any` → `object` (not awaitable); `getattr(..., None)` + # preserves `Any` for the duck-typed framework paths. + text_attr = getattr(request, "text", None) + if text_attr is not None and callable(text_attr): + result = text_attr() + return str(await result if inspect.isawaitable(result) else result) + body = getattr(request, "body", None) + if body is not None: if callable(body): - body = await body() + result = body() + body = await result if inspect.isawaitable(result) else result if isinstance(body, bytes): return body.decode("utf-8") return str(body) diff --git a/src/chat_sdk/adapters/whatsapp/adapter.py b/src/chat_sdk/adapters/whatsapp/adapter.py index 75024db..534a459 100644 --- a/src/chat_sdk/adapters/whatsapp/adapter.py +++ b/src/chat_sdk/adapters/whatsapp/adapter.py @@ -10,13 +10,14 @@ import hashlib import hmac +import inspect import json import math import os import time from collections.abc import AsyncIterable from datetime import datetime, timezone -from typing import Any +from typing import Any, cast from urllib.parse import parse_qs, urlparse from chat_sdk.adapters.whatsapp.cards import card_to_whatsapp, decode_whatsapp_callback_data @@ -47,6 +48,7 @@ LockScope, Message, MessageMetadata, + PostableMarkdown, RawMessage, ReactionEvent, StreamChunk, @@ -203,12 +205,14 @@ async def handle_webhook( value = change.get("value", {}) - # Process incoming messages + # Process incoming messages. `value["messages"]` is typed as + # `list[dict[str, Any]]` on the webhook TypedDict; cast each + # entry to the more-specific inbound shape for handler dispatch. if value.get("messages"): for message in value["messages"]: try: self._handle_inbound_message( - message, + cast("WhatsAppInboundMessage", message), (value.get("contacts") or [None])[0], value.get("metadata", {}).get("phone_number_id", ""), options, @@ -393,7 +397,7 @@ def _handle_interactive_reply( return decoded = decode_whatsapp_callback_data(raw_id) - action_id = decoded["action_id"] + action_id = decoded["action_id"] or "" value = decoded.get("value") if decoded.get("value") is not None else fallback_value contact_name = (contact or {}).get("profile", {}).get("name", "") or inbound["from"] @@ -841,7 +845,7 @@ async def stream( accumulated += chunk elif hasattr(chunk, "type") and chunk.type == "markdown_text": accumulated += chunk.text - return await self.post_message(thread_id, {"markdown": accumulated}) + return await self.post_message(thread_id, PostableMarkdown(markdown=accumulated)) async def delete_message(self, thread_id: str, message_id: str) -> None: """Delete a message. Not supported by WhatsApp Cloud API.""" @@ -1041,10 +1045,14 @@ def _resolve_emoji(self, emoji: EmojiValue | str) -> str: @staticmethod async def _get_request_body(request: Any) -> str: """Extract body text from a request object.""" - if hasattr(request, "text") and callable(request.text): - return await request.text() - if hasattr(request, "body"): - body = request.body + # `hasattr` narrows `Any` → `object` (not awaitable); using + # `getattr(..., None)` preserves `Any` for framework duck-typing. + text_attr = getattr(request, "text", None) + if text_attr is not None and callable(text_attr): + result = text_attr() + return str(await result if inspect.isawaitable(result) else result) + body = getattr(request, "body", None) + if body is not None: if isinstance(body, bytes): return body.decode("utf-8") return str(body) diff --git a/src/chat_sdk/adapters/whatsapp/types.py b/src/chat_sdk/adapters/whatsapp/types.py index 68092b3..551920d 100644 --- a/src/chat_sdk/adapters/whatsapp/types.py +++ b/src/chat_sdk/adapters/whatsapp/types.py @@ -259,7 +259,7 @@ class WhatsAppRawMessage(TypedDict, total=False): """ # The raw inbound message data - message: dict[str, Any] + message: WhatsAppInboundMessage # Phone number ID that received the message phone_number_id: str # Contact info from the webhook diff --git a/src/chat_sdk/channel.py b/src/chat_sdk/channel.py index 2981137..62d6605 100644 --- a/src/chat_sdk/channel.py +++ b/src/chat_sdk/channel.py @@ -183,11 +183,12 @@ async def get_state(self) -> Any | None: async def set_state( self, - new_state: dict[str, Any], + state: dict[str, Any], *, replace: bool = False, ) -> None: """Set channel state. Merges with existing by default.""" + new_state = state key = f"{CHANNEL_STATE_KEY_PREFIX}{self._id}" if replace: await self._state_adapter.set(key, new_state, THREAD_STATE_TTL_MS) diff --git a/src/chat_sdk/chat.py b/src/chat_sdk/chat.py index 1e29972..9cc5671 100644 --- a/src/chat_sdk/chat.py +++ b/src/chat_sdk/chat.py @@ -11,14 +11,14 @@ import asyncio import contextvars import dataclasses +import inspect import re import uuid from collections.abc import Awaitable, Callable -from dataclasses import dataclass from datetime import datetime, timezone from typing import Any -from chat_sdk.channel import ChannelImpl +from chat_sdk.channel import ChannelImpl, _ChannelImplConfigWithAdapter from chat_sdk.errors import ChatError, LockError from chat_sdk.logger import ConsoleLogger, Logger from chat_sdk.thread import ( @@ -1046,7 +1046,7 @@ async def _handle_slash_command_event(self, event: SlashCommandEvent) -> None: # Create channel for the command channel_id = getattr(event, "channel_id", None) or (event.channel.id if event.channel else "") channel = ChannelImpl( - _ChannelImplConfigForChat( + _ChannelImplConfigWithAdapter( id=channel_id, adapter=event.adapter, state_adapter=self._state_adapter, @@ -1371,7 +1371,7 @@ def channel(self, channel_id: str) -> ChannelImpl: if adapter is None: raise ChatError(f'Adapter "{adapter_name}" not found for channel ID "{channel_id}"') return ChannelImpl( - _ChannelImplConfigForChat( + _ChannelImplConfigWithAdapter( id=channel_id, adapter=adapter, state_adapter=self._state_adapter, @@ -1504,7 +1504,11 @@ async def _get_lock_key(self, adapter: Adapter, thread_id: str) -> str: if hasattr(adapter, "is_dm") and callable(getattr(adapter, "is_dm", None)) else False ) # type: ignore[union-attr] - scope = await self._lock_scope_config( + # The public contract lets callers return either `LockScope` (sync) + # or `Awaitable[LockScope]`. `inspect.isawaitable` narrows so we + # only `await` the coroutine/future branch — doing so + # unconditionally would raise `TypeError` on a sync return. + result = self._lock_scope_config( LockScopeContext( adapter=adapter, channel_id=channel_id, @@ -1512,6 +1516,7 @@ async def _get_lock_key(self, adapter: Adapter, thread_id: str) -> str: thread_id=thread_id, ) ) + scope = await result if inspect.isawaitable(result) else result else: scope = self._lock_scope_config or adapter.lock_scope or "thread" # type: ignore[assignment] @@ -1868,7 +1873,9 @@ async def _dispatch_to_handlers( self._logger.debug("Direct message received - calling handlers", {"thread_id": thread_id}) channel = thread.channel for h in self._direct_message_handlers: - await h(thread, message, channel, context) + result = h(thread, message, channel, context) + if inspect.isawaitable(result): + await result return # Backward compat: DMs without handlers treated as mentions @@ -1893,7 +1900,9 @@ async def _dispatch_to_handlers( if pat.pattern.search(message.text): self._logger.debug("Message matched pattern", {"pattern": pat.pattern.pattern}) matched = True - await pat.handler(thread, message, context) + result = pat.handler(thread, message, context) + if inspect.isawaitable(result): + await result if not matched: self._logger.debug("No handlers matched message", {"thread_id": thread_id}) @@ -2035,7 +2044,12 @@ async def _run_handlers( context: MessageContext | None = None, ) -> None: for h in handlers: - await h(thread, message, context) + result = h(thread, message, context) + # Handlers are typed `Callable[..., Awaitable[None] | None]` — + # sync handlers return None and must NOT be awaited, or we + # raise TypeError at runtime. Narrow with `isawaitable`. + if inspect.isawaitable(result): + await result # --------------------------------------------------------------------------- @@ -2106,21 +2120,3 @@ async def get_messages(self, thread_id: str, limit: int | None = None) -> list[M if limit is not None: messages = messages[-limit:] return messages - - -# --------------------------------------------------------------------------- -# Config helper used by Chat._create_thread internally -# (avoids importing channel config types at module level) -# --------------------------------------------------------------------------- - - -@dataclass -class _ChannelImplConfigForChat: - """Config passed from Chat to ChannelImpl.""" - - id: str - adapter: Adapter - state_adapter: StateAdapter - channel_visibility: ChannelVisibility = "unknown" - is_dm: bool = False - message_history: Any = None diff --git a/src/chat_sdk/thread.py b/src/chat_sdk/thread.py index 461d344..5e0a8c2 100644 --- a/src/chat_sdk/thread.py +++ b/src/chat_sdk/thread.py @@ -12,7 +12,7 @@ from collections.abc import AsyncIterator from dataclasses import dataclass from datetime import datetime, timezone -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, Protocol, runtime_checkable from chat_sdk.errors import ChatNotImplementedError from chat_sdk.logger import Logger @@ -24,6 +24,7 @@ AdapterPostableMessage, Attachment, Author, + Channel, ChannelVisibility, EmojiValue, EphemeralMessage, @@ -56,8 +57,15 @@ _active_chat: contextvars.ContextVar[_ChatSingleton | None] = contextvars.ContextVar("_active_chat", default=None) -class _ChatSingleton: - """Minimal interface for the Chat singleton to avoid circular imports.""" +@runtime_checkable +class _ChatSingleton(Protocol): + """Structural interface for the Chat singleton. + + Declared as a `Protocol` so the concrete `Chat` class structurally + satisfies it without an explicit import/inheritance cycle — both + `thread.py` and `channel.py` need the type but can't directly depend + on `chat.py`. + """ def get_adapter(self, name: str) -> Adapter | None: ... def get_state(self) -> StateAdapter: ... @@ -314,7 +322,7 @@ async def get_state(self) -> Any | None: async def set_state( self, - new_state: dict[str, Any], + state: dict[str, Any], *, replace: bool = False, ) -> None: @@ -322,6 +330,7 @@ async def set_state( State is persisted for 30 days. """ + new_state = state key = f"{THREAD_STATE_KEY_PREFIX}{self._id}" if replace: await self._state_adapter.set(key, new_state, THREAD_STATE_TTL_MS) @@ -333,8 +342,14 @@ async def set_state( # -- Channel ------------------------------------------------------------- @property - def channel(self) -> ChannelImpl: - """Get the Channel containing this thread. Lazy-created and cached.""" + def channel(self) -> Channel: + """Get the Channel containing this thread. Lazy-created and cached. + + Declared as `Channel` (the protocol) rather than `ChannelImpl` so + `ThreadImpl` structurally satisfies the `Thread` protocol — + property getter return types have to match protocol return types + exactly. + """ if self._channel_cache is None: from chat_sdk.channel import ChannelImpl, derive_channel_id diff --git a/src/chat_sdk/types.py b/src/chat_sdk/types.py index 49f3804..67ffdfe 100644 --- a/src/chat_sdk/types.py +++ b/src/chat_sdk/types.py @@ -1338,6 +1338,15 @@ def process_message( message: Message | Callable[[], Awaitable[Message]], options: WebhookOptions | None = None, ) -> None: ... + async def handle_incoming_message( + self, + adapter: Adapter, + thread_id: str, + message: Message, + options: WebhookOptions | None = None, + *, + is_subscribed: bool = False, + ) -> None: ... def process_action(self, event: Any, options: WebhookOptions | None = None) -> None: ... def process_reaction(self, event: Any, options: WebhookOptions | None = None) -> None: ... def process_slash_command(self, event: Any, options: WebhookOptions | None = None) -> None: ... diff --git a/tests/test_coverage_gaps.py b/tests/test_coverage_gaps.py index b148fb7..5fbf098 100644 --- a/tests/test_coverage_gaps.py +++ b/tests/test_coverage_gaps.py @@ -429,7 +429,7 @@ async def text_gen(): adapter.post_message.assert_called_once() call_args = adapter.post_message.call_args assert call_args.args[0] == thread_id - assert call_args.args[1]["raw"] == "hello world" + assert call_args.args[1].raw == "hello world" assert result.id == "comment-1" @pytest.mark.asyncio @@ -463,7 +463,7 @@ async def dict_gen(): await adapter.stream("linear:issue-2", dict_gen()) adapter.post_message.assert_called_once() - assert adapter.post_message.call_args.args[1]["raw"] == "chunk" + assert adapter.post_message.call_args.args[1].raw == "chunk" # =========================================================================== diff --git a/tests/test_critical_fixes.py b/tests/test_critical_fixes.py index b5942d1..9d1baea 100644 --- a/tests/test_critical_fixes.py +++ b/tests/test_critical_fixes.py @@ -323,7 +323,7 @@ async def text_stream(): assert result.id == "msg-1" assert len(posted) == 1 assert posted[0][0] == "gchat:spaces/abc" - assert posted[0][1] == {"markdown": "Hello world"} + assert posted[0][1].markdown == "Hello world" @pytest.mark.asyncio async def test_stream_handles_markdown_text_chunks(self): @@ -349,7 +349,7 @@ async def mixed_stream(): await adapter.stream("gchat:spaces/xyz", mixed_stream()) - assert posted[0][1] == {"markdown": "Start middle end"} + assert posted[0][1].markdown == "Start middle end" # --------------------------------------------------------------------------- @@ -496,7 +496,7 @@ async def text_stream(): assert result.id == "wamid.streamed" assert len(posted) == 1 - assert posted[0][1] == {"markdown": "chunk1 chunk2 chunk3"} + assert posted[0][1].markdown == "chunk1 chunk2 chunk3" # --------------------------------------------------------------------------- From 447dfd007dac63a7618f482197e2065962e69286 Mon Sep 17 00:00:00 2001 From: patrick-chinchill Date: Thu, 23 Apr 2026 11:20:49 -0700 Subject: [PATCH 3/9] chore(types): close remaining 21 pyrefly errors (now 0 / baseline removed) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **Upstream check**: verified each change against vercel/chat TS source. All changes in this PR are now either upstream-aligned or Python-specific internals (request duck-typing, Optional protocol declarations, etc.). **chinchill-api impact**: zero — chinchill-api doesn't import chat_sdk yet (uses slack-bolt directly). No consumer-facing breaking changes. Fixes in this commit: - Batch-cast at TypedDict-union access boundaries (card_utils.py, linear/cards.py, discord/cards.py, base_format_converter.py) - `Attachment.type` Literal typing for Teams / Discord `_get_attachment_type` helpers - Telegram `TelegramReactionType.get(...)` casts (reaction_key + reaction_to_emoji_value) - Telegram `from_user` cast after `.get("from_user") or .get("from")` - WhatsApp `WhatsAppCardResult` union discrimination via cast - WhatsApp adapter / Linear adapter / Google Chat adapter: replaced `{"markdown": ...}` dict literals with `PostableMarkdown(...)` to match the `AdapterPostableMessage` union - GitHub `_dispatch_event`: cast at variant narrowing boundaries - Mock adapter: `post_channel_message` returns channel_id as thread_id (was None, violating RawMessage.thread_id: str) - `_ChannelImplConfigForChat` removed (duplicate of `_ChannelImplConfigWithAdapter`) — matches upstream TS which only has `ChannelImplConfigWithAdapter | ChannelImplConfigLazy` - `thread.py`: `getattr(chunk, "text", "")` for MarkdownTextChunk narrowing (upstream handles via TS literal-union narrowing) **Reverted prior divergences after upstream check:** - `ReactionEvent.thread: Thread` (was `Thread | None`) — upstream has it required. Adapters pass `thread=None` at construction with a `# pyrefly: ignore[bad-argument-type]` + comment; Chat re-wraps with a real Thread before dispatching to handlers (same pattern as upstream TS `Omit`). - `SlashCommandEvent.channel: Channel` (was `Channel | None`) — same pattern, upstream-aligned. - `set_state(new_state=...)` (was renamed to `state`) — upstream is `setState(newState: ...)`, so Python param stays `new_state`. Protocol signature also reverted to `new_state`. Baseline file removed entirely — CI now enforces zero type errors. `.github/workflows/lint.yml` updated to drop baseline references. Validation: - `uv run pyrefly check` → **0 errors** - `uv run ruff check src/ tests/ scripts/` → clean - `uv run ruff format --check src/ tests/ scripts/` → clean - `uv run pytest tests/ --tb=short -q` → 3469 passed, 2 skipped --- .github/workflows/lint.yml | 10 +- .pyrefly-baseline.json | 256 ------------------- pyproject.toml | 5 +- src/chat_sdk/adapters/discord/adapter.py | 6 +- src/chat_sdk/adapters/google_chat/adapter.py | 4 +- src/chat_sdk/adapters/slack/adapter.py | 6 +- src/chat_sdk/adapters/teams/adapter.py | 12 +- src/chat_sdk/adapters/teams/cards.py | 6 +- src/chat_sdk/adapters/telegram/adapter.py | 30 ++- src/chat_sdk/adapters/whatsapp/adapter.py | 22 +- src/chat_sdk/adapters/whatsapp/cards.py | 6 +- src/chat_sdk/channel.py | 3 +- src/chat_sdk/chat.py | 3 +- src/chat_sdk/shared/base_format_converter.py | 8 +- src/chat_sdk/shared/card_utils.py | 24 +- src/chat_sdk/shared/mock_adapter.py | 5 +- src/chat_sdk/thread.py | 20 +- src/chat_sdk/types.py | 31 ++- 18 files changed, 123 insertions(+), 334 deletions(-) delete mode 100644 .pyrefly-baseline.json diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 624f390..d6f0771 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -8,7 +8,6 @@ on: - 'tests/**' - 'scripts/**' - 'pyproject.toml' - - '.pyrefly-baseline.json' - '.github/workflows/lint.yml' pull_request: branches: [main] @@ -18,7 +17,6 @@ on: - 'tests/**' - 'scripts/**' - 'pyproject.toml' - - '.pyrefly-baseline.json' - '.github/workflows/lint.yml' workflow_dispatch: @@ -59,10 +57,10 @@ jobs: continue-on-error: true run: uv run python scripts/audit_test_quality.py - - name: Pyrefly type check (against baseline) + - name: Pyrefly type check id: pyrefly continue-on-error: true - run: uv run pyrefly check --baseline=.pyrefly-baseline.json + run: uv run pyrefly check - name: Minimize uv cache run: uv cache prune --ci @@ -80,9 +78,9 @@ jobs: echo "| Pyrefly | ${{ steps.pyrefly.outcome }} |" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY if [ "${{ steps.pyrefly.outcome }}" = "success" ]; then - echo "No new type issues above the baseline." >> $GITHUB_STEP_SUMMARY + echo "Zero type errors." >> $GITHUB_STEP_SUMMARY else - echo "New type issues above the baseline. Either fix them or refresh the baseline with \`uv run pyrefly check --baseline=.pyrefly-baseline.json --update-baseline\` if intentional." >> $GITHUB_STEP_SUMMARY + echo "Type errors detected — see run output." >> $GITHUB_STEP_SUMMARY fi - name: Fail if any step failed diff --git a/.pyrefly-baseline.json b/.pyrefly-baseline.json deleted file mode 100644 index 0e64351..0000000 --- a/.pyrefly-baseline.json +++ /dev/null @@ -1,256 +0,0 @@ -{ - "errors": [ - { - "line": 584, - "column": 18, - "stop_line": 584, - "stop_column": 26, - "path": "src/chat_sdk/adapters/teams/adapter.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `str` is not assignable to parameter `type` with type `Literal['audio', 'file', 'image', 'video']` in function `chat_sdk.types.Attachment.__init__`", - "concise_description": "Argument `str` is not assignable to parameter `type` with type `Literal['audio', 'file', 'image', 'video']` in function `chat_sdk.types.Attachment.__init__`", - "severity": "error" - }, - { - "line": 138, - "column": 48, - "stop_line": 138, - "stop_column": 53, - "path": "src/chat_sdk/adapters/teams/cards.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `object | str` is not assignable to parameter `text` with type `str` in function `_convert_emoji`", - "concise_description": "Argument `object | str` is not assignable to parameter `text` with type `str` in function `_convert_emoji`", - "severity": "error" - }, - { - "line": 707, - "column": 28, - "stop_line": 707, - "stop_column": 37, - "path": "src/chat_sdk/adapters/telegram/adapter.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `object | TelegramUser` is not assignable to parameter `user` with type `TelegramUser` in function `TelegramAdapter.to_author`", - "concise_description": "Argument `object | TelegramUser` is not assignable to parameter `user` with type `TelegramUser` in function `TelegramAdapter.to_author`", - "severity": "error" - }, - { - "line": 725, - "column": 27, - "stop_line": 725, - "stop_column": 36, - "path": "src/chat_sdk/adapters/telegram/adapter.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `str | None` is not assignable to parameter `action_id` with type `str` in function `chat_sdk.types.ActionEvent.__init__`", - "concise_description": "Argument `str | None` is not assignable to parameter `action_id` with type `str` in function `chat_sdk.types.ActionEvent.__init__`", - "severity": "error" - }, - { - "line": 1241, - "column": 37, - "stop_line": 1241, - "stop_column": 46, - "path": "src/chat_sdk/adapters/telegram/adapter.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `object | TelegramUser` is not assignable to parameter `user` with type `TelegramUser` in function `TelegramAdapter.to_author`", - "concise_description": "Argument `object | TelegramUser` is not assignable to parameter `user` with type `TelegramUser` in function `TelegramAdapter.to_author`", - "severity": "error" - }, - { - "line": 1730, - "column": 20, - "stop_line": 1730, - "stop_column": 45, - "path": "src/chat_sdk/adapters/telegram/adapter.py", - "code": -2, - "name": "bad-return", - "description": "Returned type `object | str` is not assignable to declared return type `str`", - "concise_description": "Returned type `object | str` is not assignable to declared return type `str`", - "severity": "error" - }, - { - "line": 1736, - "column": 30, - "stop_line": 1736, - "stop_column": 55, - "path": "src/chat_sdk/adapters/telegram/adapter.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `object | str` is not assignable to parameter `name` with type `str` in function `chat_sdk.emoji.get_emoji`", - "concise_description": "Argument `object | str` is not assignable to parameter `name` with type `str` in function `chat_sdk.emoji.get_emoji`", - "severity": "error" - }, - { - "line": 718, - "column": 87, - "stop_line": 718, - "stop_column": 100, - "path": "src/chat_sdk/adapters/whatsapp/adapter.py", - "code": -2, - "name": "bad-typed-dict-key", - "description": "TypedDict `WhatsAppCardResultText` does not have key `interactive`", - "concise_description": "TypedDict `WhatsAppCardResultText` does not have key `interactive`", - "severity": "error" - }, - { - "line": 723, - "column": 51, - "stop_line": 723, - "stop_column": 57, - "path": "src/chat_sdk/adapters/whatsapp/adapter.py", - "code": -2, - "name": "bad-typed-dict-key", - "description": "TypedDict `WhatsAppCardResultInteractive` does not have key `text`", - "concise_description": "TypedDict `WhatsAppCardResultInteractive` does not have key `text`", - "severity": "error" - }, - { - "line": 231, - "column": 16, - "stop_line": 231, - "stop_column": 21, - "path": "src/chat_sdk/adapters/whatsapp/cards.py", - "code": -2, - "name": "bad-return", - "description": "Returned type `list[object | str]` is not assignable to declared return type `list[str]`", - "concise_description": "Returned type `list[object | str]` is not assignable to declared return type `list[str]`", - "severity": "error" - }, - { - "line": 1238, - "column": 25, - "stop_line": 1238, - "stop_column": 37, - "path": "src/chat_sdk/chat.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `Channel | None` is not assignable to parameter `channel` with type `ChannelImpl | None` in function `Chat._store_modal_context`", - "concise_description": "Argument `Channel | None` is not assignable to parameter `channel` with type `ChannelImpl | None` in function `Chat._store_modal_context`", - "severity": "error" - }, - { - "line": 194, - "column": 50, - "stop_line": 194, - "stop_column": 57, - "path": "src/chat_sdk/shared/base_format_converter.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `dict[Any, Any]` is not assignable to parameter `card` with type `CardElement` in function `chat_sdk.cards.card_to_fallback_text`", - "concise_description": "Argument `dict[Any, Any]` is not assignable to parameter `card` with type `CardElement` in function `chat_sdk.cards.card_to_fallback_text`", - "severity": "error" - }, - { - "line": 68, - "column": 29, - "stop_line": 68, - "stop_column": 53, - "path": "src/chat_sdk/shared/card_utils.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `object | str` is not assignable to parameter with type `str`", - "concise_description": "Argument `object | str` is not assignable to parameter with type `str`", - "severity": "error" - }, - { - "line": 70, - "column": 32, - "stop_line": 70, - "stop_column": 54, - "path": "src/chat_sdk/shared/card_utils.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `object | str` is not assignable to parameter with type `str`", - "concise_description": "Argument `object | str` is not assignable to parameter with type `str`", - "severity": "error" - }, - { - "line": 72, - "column": 93, - "stop_line": 72, - "stop_column": 118, - "path": "src/chat_sdk/shared/card_utils.py", - "code": -2, - "name": "not-iterable", - "description": "Type `object` is not iterable", - "concise_description": "Type `object` is not iterable", - "severity": "error" - }, - { - "line": 76, - "column": 90, - "stop_line": 76, - "stop_column": 115, - "path": "src/chat_sdk/shared/card_utils.py", - "code": -2, - "name": "not-iterable", - "description": "Type `object` is not iterable", - "concise_description": "Type `object` is not iterable", - "severity": "error" - }, - { - "line": 78, - "column": 39, - "stop_line": 78, - "stop_column": 63, - "path": "src/chat_sdk/shared/card_utils.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `list[str] | object` is not assignable to parameter `headers` with type `list[str]` in function `chat_sdk.cards.table_element_to_ascii`", - "concise_description": "Argument `list[str] | object` is not assignable to parameter `headers` with type `list[str]` in function `chat_sdk.cards.table_element_to_ascii`", - "severity": "error" - }, - { - "line": 78, - "column": 65, - "stop_line": 78, - "stop_column": 86, - "path": "src/chat_sdk/shared/card_utils.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `list[list[str]] | object` is not assignable to parameter `rows` with type `list[list[str]]` in function `chat_sdk.cards.table_element_to_ascii`", - "concise_description": "Argument `list[list[str]] | object` is not assignable to parameter `rows` with type `list[list[str]]` in function `chat_sdk.cards.table_element_to_ascii`", - "severity": "error" - }, - { - "line": 193, - "column": 49, - "stop_line": 193, - "stop_column": 53, - "path": "src/chat_sdk/shared/mock_adapter.py", - "code": -2, - "name": "bad-argument-type", - "description": "Argument `None` is not assignable to parameter `thread_id` with type `str` in function `chat_sdk.types.RawMessage.__init__`", - "concise_description": "Argument `None` is not assignable to parameter `thread_id` with type `str` in function `chat_sdk.types.RawMessage.__init__`", - "severity": "error" - }, - { - "line": 593, - "column": 27, - "stop_line": 593, - "stop_column": 37, - "path": "src/chat_sdk/thread.py", - "code": -2, - "name": "missing-attribute", - "description": "Object of class `PlanUpdateChunk` has no attribute `text`\nObject of class `TaskUpdateChunk` has no attribute `text`", - "concise_description": "Object of class `PlanUpdateChunk` has no attribute `text`\nObject of class `TaskUpdateChunk` has no attribute `text`", - "severity": "error" - }, - { - "line": 1144, - "column": 23, - "stop_line": 1144, - "stop_column": 27, - "path": "src/chat_sdk/thread.py", - "code": -2, - "name": "invalid-yield", - "description": "Yielded type `dict[Any, Any]` is not assignable to declared yield type `MarkdownTextChunk | PlanUpdateChunk | TaskUpdateChunk | str`", - "concise_description": "Yielded type `dict[Any, Any]` is not assignable to declared yield type `MarkdownTextChunk | PlanUpdateChunk | TaskUpdateChunk | str`", - "severity": "error" - } - ] -} \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index dd36b42..1a5eb61 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -80,9 +80,8 @@ dev = [ # --------------------------------------------------------------------------- # Pyrefly type checker configuration # --------------------------------------------------------------------------- -# Baseline workflow: -# Check against baseline: uv run pyrefly check --baseline=.pyrefly-baseline.json -# Refresh baseline: uv run pyrefly check --baseline=.pyrefly-baseline.json --update-baseline +# Target: zero errors. Run: +# uv run pyrefly check # --------------------------------------------------------------------------- [tool.pyrefly] diff --git a/src/chat_sdk/adapters/discord/adapter.py b/src/chat_sdk/adapters/discord/adapter.py index 9261e3e..384c5df 100644 --- a/src/chat_sdk/adapters/discord/adapter.py +++ b/src/chat_sdk/adapters/discord/adapter.py @@ -365,7 +365,7 @@ def _handle_component_interaction( ), message_id=message_id, thread_id=thread_id, - thread=None, + thread=None, # pyrefly: ignore[bad-argument-type] # filled in by Chat adapter=self, raw=interaction, ), @@ -454,7 +454,7 @@ def _handle_application_command_interaction( is_me=user.get("id") == self._application_id, ), adapter=self, - channel=None, + channel=None, # pyrefly: ignore[bad-argument-type] # filled in by Chat raw=interaction, ) event.channel_id = channel_id # type: ignore[attr-defined] @@ -711,7 +711,7 @@ async def _handle_forwarded_reaction( self._chat.process_reaction( ReactionEvent( adapter=self, - thread=None, + thread=None, # pyrefly: ignore[bad-argument-type] # filled in by Chat thread_id=thread_id, message_id=data.get("message_id", ""), emoji=normalized, diff --git a/src/chat_sdk/adapters/google_chat/adapter.py b/src/chat_sdk/adapters/google_chat/adapter.py index be0a117..da22351 100644 --- a/src/chat_sdk/adapters/google_chat/adapter.py +++ b/src/chat_sdk/adapters/google_chat/adapter.py @@ -1030,7 +1030,7 @@ async def build_reaction_event() -> ReactionEvent: reaction_user = reaction.get("user") or {} return ReactionEvent( adapter=self, - thread=None, + thread=None, # pyrefly: ignore[bad-argument-type] # filled in by Chat thread_id=thread_id, message_id=message_name, user=Author( @@ -1198,7 +1198,7 @@ def _handle_card_click( action_event = ActionEvent( adapter=self, - thread=None, + thread=None, # pyrefly: ignore[bad-argument-type] # filled in by Chat thread_id=thread_id, message_id=(message or {}).get("name", ""), user=Author( diff --git a/src/chat_sdk/adapters/slack/adapter.py b/src/chat_sdk/adapters/slack/adapter.py index fea619c..5183a71 100644 --- a/src/chat_sdk/adapters/slack/adapter.py +++ b/src/chat_sdk/adapters/slack/adapter.py @@ -913,7 +913,7 @@ async def _handle_slash_command( is_me=False, ), adapter=self, - channel=None, # chat.py's _handle_slash_command_event creates the ChannelImpl + channel=None, # pyrefly: ignore[bad-argument-type] # filled in by Chat raw={k: v[0] for k, v in params.items()} if params else {}, trigger_id=trigger_id, ) @@ -972,7 +972,7 @@ def _handle_block_actions(self, payload: dict[str, Any], options: WebhookOptions ), message_id=message_id, thread_id=thread_id, - thread=None, + thread=None, # pyrefly: ignore[bad-argument-type] # filled in by Chat adapter=self, raw=payload, trigger_id=payload.get("trigger_id"), @@ -1191,7 +1191,7 @@ async def _resolve_and_process() -> None: ), message_id=message_id, thread_id=thread_id, - thread=None, + thread=None, # pyrefly: ignore[bad-argument-type] # filled in by Chat raw=event, adapter=self, ) diff --git a/src/chat_sdk/adapters/teams/adapter.py b/src/chat_sdk/adapters/teams/adapter.py index bfdc068..578017e 100644 --- a/src/chat_sdk/adapters/teams/adapter.py +++ b/src/chat_sdk/adapters/teams/adapter.py @@ -15,7 +15,7 @@ import os import re from datetime import datetime, timezone -from typing import Any, NoReturn +from typing import Any, Literal, NoReturn from chat_sdk.adapters.teams.cards import card_to_adaptive_card from chat_sdk.adapters.teams.format_converter import TeamsFormatConverter @@ -415,7 +415,7 @@ def _handle_message_action( ), message_id=activity.get("replyToId") or activity.get("id", ""), thread_id=thread_id, - thread=None, + thread=None, # pyrefly: ignore[bad-argument-type] # filled in by Chat adapter=self, raw=activity, ), @@ -458,7 +458,7 @@ async def _handle_adaptive_card_action( ), message_id=activity.get("replyToId") or activity.get("id", ""), thread_id=thread_id, - thread=None, + thread=None, # pyrefly: ignore[bad-argument-type] # filled in by Chat adapter=self, raw=activity, ), @@ -505,7 +505,7 @@ def _handle_reaction_activity( user=user, message_id=message_id, thread_id=thread_id, - thread=None, + thread=None, # pyrefly: ignore[bad-argument-type] # filled in by Chat adapter=self, raw=activity, ), @@ -522,7 +522,7 @@ def _handle_reaction_activity( user=user, message_id=message_id, thread_id=thread_id, - thread=None, + thread=None, # pyrefly: ignore[bad-argument-type] # filled in by Chat adapter=self, raw=activity, ), @@ -572,7 +572,7 @@ def _parse_teams_message( def _create_attachment(self, att: dict[str, Any]) -> Attachment: """Create an Attachment from a Teams attachment dict.""" content_type = att.get("contentType", "") - att_type: str = "file" + att_type: Literal["audio", "file", "image", "video"] = "file" if content_type.startswith("image/"): att_type = "image" elif content_type.startswith("video/"): diff --git a/src/chat_sdk/adapters/teams/cards.py b/src/chat_sdk/adapters/teams/cards.py index d3c42a3..2346504 100644 --- a/src/chat_sdk/adapters/teams/cards.py +++ b/src/chat_sdk/adapters/teams/cards.py @@ -6,7 +6,7 @@ from __future__ import annotations -from typing import Any +from typing import Any, cast from chat_sdk.cards import ( ActionsElement, @@ -142,8 +142,8 @@ def _convert_child_to_adaptive(child: CardChild) -> dict[str, Any]: if child_type == "fields": return {"elements": [_convert_fields_to_element(child)], "actions": []} # type: ignore[arg-type] if child_type == "link": - label = child.get("label", "") # type: ignore[union-attr] - url = child.get("url", "") # type: ignore[union-attr] + label = cast("str", child.get("label", "")) + url = cast("str", child.get("url", "")) return { "elements": [ { diff --git a/src/chat_sdk/adapters/telegram/adapter.py b/src/chat_sdk/adapters/telegram/adapter.py index 26bcf6b..f4a34ac 100644 --- a/src/chat_sdk/adapters/telegram/adapter.py +++ b/src/chat_sdk/adapters/telegram/adapter.py @@ -19,7 +19,7 @@ import re from dataclasses import dataclass from datetime import datetime, timezone -from typing import Any +from typing import Any, cast from chat_sdk.adapters.telegram.cards import ( card_to_telegram_inline_keyboard, @@ -698,11 +698,14 @@ def handle_callback_query( ) decoded = decode_telegram_callback_data(callback_query.get("data")) - action_id = decoded["action_id"] + action_id = decoded["action_id"] or "" value = decoded["value"] # The TS source uses callback_query.from – in our types this is from_user - from_user = callback_query.get("from_user") or callback_query.get("from") # type: ignore[call-overload] + from_user = cast( + "TelegramUser | None", + callback_query.get("from_user") or callback_query.get("from"), # type: ignore[call-overload] + ) user = ( self.to_author(from_user) if from_user @@ -718,7 +721,7 @@ def handle_callback_query( self._chat.process_action( ActionEvent( adapter=self, - thread=None, + thread=None, # pyrefly: ignore[bad-argument-type] # filled in by Chat thread_id=thread_id, message_id=message_id, user=user, @@ -783,7 +786,7 @@ def handle_message_reaction_update( self._chat.process_reaction( ReactionEvent( adapter=self, - thread=None, + thread=None, # pyrefly: ignore[bad-argument-type] # filled in by Chat thread_id=thread_id, message_id=message_id, user=actor, @@ -801,7 +804,7 @@ def handle_message_reaction_update( self._chat.process_reaction( ReactionEvent( adapter=self, - thread=None, + thread=None, # pyrefly: ignore[bad-argument-type] # filled in by Chat thread_id=thread_id, message_id=message_id, user=actor, @@ -1234,7 +1237,10 @@ def parse_telegram_message( text = apply_telegram_entities(plain_text, entities) # Determine author -- Telegram uses 'from' key which is a reserved word - from_user = raw.get("from_user") or raw.get("from") # type: ignore[call-overload] + from_user = cast( + "TelegramUser | None", + raw.get("from_user") or raw.get("from"), # type: ignore[call-overload] + ) sender_chat = raw.get("sender_chat") if from_user: @@ -1726,15 +1732,17 @@ def to_telegram_reaction(self, emoji: EmojiValue | str) -> TelegramReactionType: def reaction_key(self, reaction: TelegramReactionType) -> str: """Compute a unique key for a Telegram reaction.""" + # `TelegramReactionType` is a TypedDict union; `.get()` returns the + # union of all value-types, so narrow the strings we know are strings. if reaction.get("type") == "emoji": - return reaction.get("emoji", "") - return f"custom:{reaction.get('custom_emoji_id', '')}" + return cast("str", reaction.get("emoji", "")) + return f"custom:{cast('str', reaction.get('custom_emoji_id', ''))}" def reaction_to_emoji_value(self, reaction: TelegramReactionType) -> EmojiValue: """Convert a Telegram reaction to an :class:`EmojiValue`.""" if reaction.get("type") == "emoji": - return get_emoji(reaction.get("emoji", "")) - return get_emoji(f"custom:{reaction.get('custom_emoji_id', '')}") + return get_emoji(cast("str", reaction.get("emoji", ""))) + return get_emoji(f"custom:{cast('str', reaction.get('custom_emoji_id', ''))}") # -- Telegram API -------------------------------------------------------- diff --git a/src/chat_sdk/adapters/whatsapp/adapter.py b/src/chat_sdk/adapters/whatsapp/adapter.py index 534a459..15b4e98 100644 --- a/src/chat_sdk/adapters/whatsapp/adapter.py +++ b/src/chat_sdk/adapters/whatsapp/adapter.py @@ -20,7 +20,12 @@ from typing import Any, cast from urllib.parse import parse_qs, urlparse -from chat_sdk.adapters.whatsapp.cards import card_to_whatsapp, decode_whatsapp_callback_data +from chat_sdk.adapters.whatsapp.cards import ( + WhatsAppCardResultInteractive, + WhatsAppCardResultText, + card_to_whatsapp, + decode_whatsapp_callback_data, +) from chat_sdk.adapters.whatsapp.format_converter import WhatsAppFormatConverter from chat_sdk.adapters.whatsapp.types import ( WhatsAppAdapterConfig, @@ -353,7 +358,7 @@ def _handle_reaction( self._chat.process_reaction( ReactionEvent( adapter=self, - thread=None, + thread=None, # pyrefly: ignore[bad-argument-type] # filled in by Chat thread_id=thread_id, message_id=inbound["reaction"]["message_id"], user=user, @@ -404,7 +409,7 @@ def _handle_interactive_reply( self._chat.process_action( ActionEvent( adapter=self, - thread=None, + thread=None, # pyrefly: ignore[bad-argument-type] # filled in by Chat thread_id=thread_id, message_id=inbound["id"], user=Author( @@ -443,7 +448,7 @@ def _handle_button_response( self._chat.process_action( ActionEvent( adapter=self, - thread=None, + thread=None, # pyrefly: ignore[bad-argument-type] # filled in by Chat thread_id=thread_id, message_id=inbound["id"], user=Author( @@ -713,14 +718,19 @@ async def post_message( # Check if this is a card with interactive buttons card = extract_card(message) if card: + # `card_to_whatsapp` returns a `WhatsAppCardResultInteractive` + # or `WhatsAppCardResultText` union; the `type` check narrows + # at runtime but pyrefly doesn't propagate TypedDict `type`-tag + # discrimination, so cast the field access on each branch. result = card_to_whatsapp(card) if result.get("type") == "interactive": - interactive = json.loads(convert_emoji_placeholders(json.dumps(result["interactive"]), "whatsapp")) + interactive_raw = cast("WhatsAppCardResultInteractive", result)["interactive"] + interactive = json.loads(convert_emoji_placeholders(json.dumps(interactive_raw), "whatsapp")) return await self._send_interactive_message(thread_id, user_wa_id, interactive) return await self._send_text_message( thread_id, user_wa_id, - convert_emoji_placeholders(result["text"], "whatsapp"), + convert_emoji_placeholders(cast("WhatsAppCardResultText", result)["text"], "whatsapp"), ) # Regular text message diff --git a/src/chat_sdk/adapters/whatsapp/cards.py b/src/chat_sdk/adapters/whatsapp/cards.py index 2c1fd94..971157e 100644 --- a/src/chat_sdk/adapters/whatsapp/cards.py +++ b/src/chat_sdk/adapters/whatsapp/cards.py @@ -12,7 +12,7 @@ from __future__ import annotations import json -from typing import Any, Literal, TypedDict +from typing import Any, Literal, TypedDict, cast from chat_sdk.adapters.whatsapp.types import WhatsAppInteractiveMessage from chat_sdk.cards import ( @@ -224,8 +224,8 @@ def _render_child(child: CardChild) -> list[str]: return result if child_type == "image": - alt = child.get("alt", "") # type: ignore[union-attr] - url = child.get("url", "") # type: ignore[union-attr] + alt = cast("str", child.get("alt", "")) + url = cast("str", child.get("url", "")) if alt: return [f"{alt}: {url}"] return [url] diff --git a/src/chat_sdk/channel.py b/src/chat_sdk/channel.py index 62d6605..2981137 100644 --- a/src/chat_sdk/channel.py +++ b/src/chat_sdk/channel.py @@ -183,12 +183,11 @@ async def get_state(self) -> Any | None: async def set_state( self, - state: dict[str, Any], + new_state: dict[str, Any], *, replace: bool = False, ) -> None: """Set channel state. Merges with existing by default.""" - new_state = state key = f"{CHANNEL_STATE_KEY_PREFIX}{self._id}" if replace: await self._state_adapter.set(key, new_state, THREAD_STATE_TTL_MS) diff --git a/src/chat_sdk/chat.py b/src/chat_sdk/chat.py index 9cc5671..78c133e 100644 --- a/src/chat_sdk/chat.py +++ b/src/chat_sdk/chat.py @@ -36,6 +36,7 @@ AssistantContextChangedEvent, AssistantThreadStartedEvent, Author, + Channel, ChannelVisibility, ChatConfig, ConcurrencyStrategy, @@ -1096,7 +1097,7 @@ def _store_modal_context( context_id: str, thread: ThreadImpl | None = None, message: Message | None = None, - channel: ChannelImpl | None = None, + channel: Channel | None = None, ) -> None: key = f"modal-context:{adapter_name}:{context_id}" context = { diff --git a/src/chat_sdk/shared/base_format_converter.py b/src/chat_sdk/shared/base_format_converter.py index 31f3805..0f6f7e4 100644 --- a/src/chat_sdk/shared/base_format_converter.py +++ b/src/chat_sdk/shared/base_format_converter.py @@ -15,7 +15,7 @@ from abc import ABC, abstractmethod from collections.abc import Callable -from typing import Any +from typing import Any, cast from chat_sdk.shared.markdown_parser import ( Content, @@ -186,12 +186,14 @@ def render_postable(self, message: PostableMessageInput) -> str: return card_to_fallback_text(message["card"]) if message.get("type") == "card": - from chat_sdk.cards import is_card_element + from chat_sdk.cards import CardElement, is_card_element if is_card_element(message): from chat_sdk.cards import card_to_fallback_text - return card_to_fallback_text(message) + # `is_card_element` isn't a TypeGuard, so pyrefly still + # sees `dict[Any, Any]`. Cast to match the signature. + return card_to_fallback_text(cast("CardElement", message)) return "" return str(message) diff --git a/src/chat_sdk/shared/card_utils.py b/src/chat_sdk/shared/card_utils.py index 2552da8..397b0e9 100644 --- a/src/chat_sdk/shared/card_utils.py +++ b/src/chat_sdk/shared/card_utils.py @@ -3,7 +3,7 @@ from __future__ import annotations from collections.abc import Callable -from typing import Literal +from typing import Literal, cast from chat_sdk.cards import CardChild, CardElement, card_child_to_fallback_text, table_element_to_ascii from chat_sdk.emoji import convert_emoji_placeholders @@ -62,20 +62,30 @@ def card_to_fallback_text( def _child_to_fallback_text(child: CardChild, convert_text: Callable[[str], str]) -> str | None: - """Convert a card child element to fallback text.""" + """Convert a card child element to fallback text. + + `child` is a `CardChild` union of TypedDicts; the `child_type` check + narrows at runtime but pyrefly returns `object | str` from `.get()` + on the union, so we `cast` on each branch to the specific value type. + """ child_type = child.get("type", "") if child_type == "text": - return convert_text(child.get("content", "")) + return convert_text(cast("str", child.get("content", ""))) if child_type == "link": - return f"{convert_text(child.get('label', ''))} ({child.get('url', '')})" + return f"{convert_text(cast('str', child.get('label', '')))} ({cast('str', child.get('url', ''))})" if child_type == "fields": - return "\n".join(f"{convert_text(f['label'])}: {convert_text(f['value'])}" for f in child.get("children", [])) + children = cast("list[dict[str, str]]", child.get("children", [])) + return "\n".join(f"{convert_text(f['label'])}: {convert_text(f['value'])}" for f in children) if child_type == "actions": return None if child_type == "section": - return "\n".join(filter(None, (_child_to_fallback_text(c, convert_text) for c in child.get("children", [])))) + section_children = cast("list[CardChild]", child.get("children", [])) + return "\n".join(filter(None, (_child_to_fallback_text(c, convert_text) for c in section_children))) if child_type == "table": - return table_element_to_ascii(child.get("headers", []), child.get("rows", [])) + return table_element_to_ascii( + cast("list[str]", child.get("headers", [])), + cast("list[list[str]]", child.get("rows", [])), + ) if child_type == "divider": return "---" return card_child_to_fallback_text(child) diff --git a/src/chat_sdk/shared/mock_adapter.py b/src/chat_sdk/shared/mock_adapter.py index bc29b59..ba75254 100644 --- a/src/chat_sdk/shared/mock_adapter.py +++ b/src/chat_sdk/shared/mock_adapter.py @@ -190,7 +190,10 @@ async def fetch_channel_info(self, channel_id: str) -> ChannelInfo: ) async def post_channel_message(self, channel_id: str, message: AdapterPostableMessage) -> RawMessage: - return RawMessage(id="msg-1", thread_id=None, raw={}) + # Channel-scoped posts don't have a thread — use the channel id as + # the identifier so downstream code that expects a non-null + # `thread_id` can still route by string key. + return RawMessage(id="msg-1", thread_id=channel_id, raw={}) # --------------------------------------------------------------------------- diff --git a/src/chat_sdk/thread.py b/src/chat_sdk/thread.py index 5e0a8c2..7702fa1 100644 --- a/src/chat_sdk/thread.py +++ b/src/chat_sdk/thread.py @@ -12,7 +12,7 @@ from collections.abc import AsyncIterator from dataclasses import dataclass from datetime import datetime, timezone -from typing import TYPE_CHECKING, Any, Protocol, runtime_checkable +from typing import TYPE_CHECKING, Any, Protocol, cast, runtime_checkable from chat_sdk.errors import ChatNotImplementedError from chat_sdk.logger import Logger @@ -30,8 +30,10 @@ EphemeralMessage, FetchOptions, FormattedContent, + MarkdownTextChunk, Message, MessageMetadata, + PlanUpdateChunk, PostableCard, PostableMarkdown, PostableMessage, @@ -43,6 +45,7 @@ StateAdapter, StreamChunk, StreamOptions, + TaskUpdateChunk, ) if TYPE_CHECKING: @@ -322,7 +325,7 @@ async def get_state(self) -> Any | None: async def set_state( self, - state: dict[str, Any], + new_state: dict[str, Any], *, replace: bool = False, ) -> None: @@ -330,7 +333,6 @@ async def set_state( State is persisted for 30 days. """ - new_state = state key = f"{THREAD_STATE_KEY_PREFIX}{self._id}" if replace: await self._state_adapter.set(key, new_state, THREAD_STATE_TTL_MS) @@ -590,7 +592,10 @@ async def _text_only_stream() -> AsyncIterator[str]: elif isinstance(chunk, dict) and chunk.get("type") == "markdown_text": yield chunk.get("text", "") elif hasattr(chunk, "type") and getattr(chunk, "type", None) == "markdown_text": - yield chunk.text + # Runtime-narrowed to a MarkdownTextChunk via the `type` + # tag; only that variant has `.text`. Pyrefly doesn't do + # tag-based union narrowing, so read via `getattr`. + yield getattr(chunk, "text", "") # Skip non-text chunks in fallback mode return await self._fallback_stream(_text_only_stream(), options) @@ -1139,9 +1144,12 @@ async def _from_full_stream(raw_stream: Any) -> AsyncIterator[str | StreamChunk] elif isinstance(item, dict): t = item.get("type") - # Pass through known StreamChunk dict types + # Pass through known StreamChunk dict types. The `t` check + # narrows at runtime to one of the three StreamChunk TypedDicts, + # but pyrefly doesn't narrow through tag-string comparisons, so + # cast to the declared yield union. if t in ("markdown_text", "task_update", "plan_update"): - yield item + yield cast("MarkdownTextChunk | PlanUpdateChunk | TaskUpdateChunk", item) continue if t == "text-delta": diff --git a/src/chat_sdk/types.py b/src/chat_sdk/types.py index 67ffdfe..d9aa856 100644 --- a/src/chat_sdk/types.py +++ b/src/chat_sdk/types.py @@ -946,14 +946,16 @@ class PostEphemeralOptions: class ReactionEvent: """Reaction event data. - `thread` is optional because some platforms deliver reactions without a - usable thread context (e.g. Slack reactions on a top-level channel - message; Teams reactions where the message lookup failed). Consumers - should `if event.thread is None: return` before using it. + Matches upstream TS `ReactionEvent` shape: `thread` is required here + because handlers receive the fully-populated event after `Chat` + re-wraps any partial event from an adapter. Adapters dispatch via + `chat.process_reaction(...)` with a partial event (`thread=None` at + construction); `Chat` resolves the real thread before invoking + handlers, so this field is never `None` at handler time. """ adapter: Adapter - thread: Thread | None + thread: Thread thread_id: str message_id: str user: Author @@ -1029,14 +1031,15 @@ class ModalResponse: class SlashCommandEvent: """Slash command event data. - `channel` is optional because some platforms (notably Discord) fire - slash commands without a channel context (DM slash commands), and - because adapters may not always resolve the channel before dispatch. - Consumers should `if event.channel is None: return` before using it. + Matches upstream TS `SlashCommandEvent`: `channel` is required here + because handlers receive the fully-populated event after `Chat` + re-wraps the partial event from an adapter. Adapters pass + `channel=None` at construction; `Chat` constructs a real `Channel` + before invoking handlers, so this is never `None` at handler time. """ adapter: Adapter - channel: Channel | None + channel: Channel user: Author command: str text: str @@ -1416,11 +1419,15 @@ async def schedule( async def set_state( self, - state: dict[str, Any], + new_state: dict[str, Any], *, replace: bool = False, ) -> None: - """Set the state. Merges with existing state by default.""" + """Set the state. Merges with existing state by default. + + Parameter is named `new_state` to match upstream TS + `setState(newState)` and preserve call-site kwarg compatibility. + """ ... async def start_typing(self, status: str | None = None) -> None: From 13f0e7b33d9d73c6027415999cc731444bcd35c1 Mon Sep 17 00:00:00 2001 From: patrick-chinchill Date: Thu, 23 Apr 2026 13:32:13 -0700 Subject: [PATCH 4/9] address review comments on PR #49 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses CodeRabbit critical/major findings + github-code-quality nits: **Critical: `request.body` duck-typing symmetry** (5 adapters). Previously only `request.text` got the `getattr + isawaitable` treatment; `request.body` was still vulnerable to the same async-method case (Flask, FastAPI body-as-coroutine, etc.). Would return `` stringified, breaking JSON parsing and HMAC verification. Applied to: - src/chat_sdk/adapters/linear/adapter.py - src/chat_sdk/adapters/discord/adapter.py - src/chat_sdk/adapters/teams/adapter.py - src/chat_sdk/adapters/slack/adapter.py - src/chat_sdk/adapters/github/adapter.py - src/chat_sdk/adapters/google_chat/adapter.py - src/chat_sdk/adapters/whatsapp/adapter.py (Telegram already had the correct pattern.) **Major: sync-handler support was partial.** Previously `_run_handlers`, `_direct_message_handlers`, and pattern handlers used `inspect.isawaitable` guards, but `_handle_action_event`, `_handle_slash_command_event`, `_handle_reaction_event`, `process_modal_submit`, `process_modal_close`, and the assistant/app-home/member-joined handler loops still `await`ed unconditionally. A sync handler on any of those paths would crash with `TypeError`. Fix: added `Chat._invoke_handler(handler, *args)` static helper that calls the handler, narrows with `inspect.isawaitable`, and returns the result (so modal-submit handlers that return a `ModalResponse` still work). Routed all 11 remaining handler-invocation sites through it. **Minor: Unused import false positives** (CodeQL). `ModalElement` in slack/adapter.py and `WhatsAppCardResultInteractive` / `WhatsAppCardResultText` in whatsapp/adapter.py were used only in string-literal `cast("ModalElement", ...)` forms. Switched to direct `cast(ModalElement, ...)` so the import is syntactically used — pyrefly still resolves the cast and CodeQL no longer flags. **Minor: Protocol signature**: dropped extra `options` / `is_subscribed` params from `ChatInstance.handle_incoming_message` protocol stub — matches `Chat.handle_incoming_message` implementation exactly. Validation: - `uv run pyrefly check` — 0 errors - `uv run ruff check src/ tests/ scripts/` — clean - `uv run pytest tests/ --tb=short -q` — 3469 passed, 2 skipped --- src/chat_sdk/adapters/discord/adapter.py | 4 ++ src/chat_sdk/adapters/github/adapter.py | 6 +++ src/chat_sdk/adapters/google_chat/adapter.py | 6 +++ src/chat_sdk/adapters/linear/adapter.py | 4 ++ src/chat_sdk/adapters/slack/adapter.py | 14 ++++-- src/chat_sdk/adapters/teams/adapter.py | 4 ++ src/chat_sdk/adapters/whatsapp/adapter.py | 10 +++- src/chat_sdk/chat.py | 52 +++++++++++++------- src/chat_sdk/types.py | 3 -- 9 files changed, 76 insertions(+), 27 deletions(-) diff --git a/src/chat_sdk/adapters/discord/adapter.py b/src/chat_sdk/adapters/discord/adapter.py index 384c5df..a031f45 100644 --- a/src/chat_sdk/adapters/discord/adapter.py +++ b/src/chat_sdk/adapters/discord/adapter.py @@ -1412,6 +1412,10 @@ async def _get_request_body(self, request: Any) -> str: if body is not None: if callable(body): body = body() + # Some frameworks expose `body` as an async method; if calling it + # produced a coroutine, await it before treating as bytes/str. + if inspect.isawaitable(body): + body = await body if hasattr(body, "read"): raw_read = body.read raw = await raw_read() if inspect.iscoroutinefunction(raw_read) else raw_read() diff --git a/src/chat_sdk/adapters/github/adapter.py b/src/chat_sdk/adapters/github/adapter.py index 7095185..972670e 100644 --- a/src/chat_sdk/adapters/github/adapter.py +++ b/src/chat_sdk/adapters/github/adapter.py @@ -1095,6 +1095,12 @@ async def _get_request_body(request: Any) -> str: return str(await result if inspect.isawaitable(result) else result) body = getattr(request, "body", None) if body is not None: + # Some frameworks expose `body` as an async method; if calling it + # produced a coroutine, await it before treating as bytes/str. + if callable(body): + body = body() + if inspect.isawaitable(body): + body = await body return body.decode("utf-8") if isinstance(body, bytes) else str(body) return "" diff --git a/src/chat_sdk/adapters/google_chat/adapter.py b/src/chat_sdk/adapters/google_chat/adapter.py index da22351..9083f8f 100644 --- a/src/chat_sdk/adapters/google_chat/adapter.py +++ b/src/chat_sdk/adapters/google_chat/adapter.py @@ -772,6 +772,12 @@ async def handle_webhook( else: raw_body = getattr(request, "body", None) if raw_body is not None: + # Some frameworks expose `body` as an async method; call and + # await if needed before treating as bytes/str. + if callable(raw_body): + raw_body = raw_body() + if inspect.isawaitable(raw_body): + raw_body = await raw_body body = raw_body.decode("utf-8") if isinstance(raw_body, bytes) else str(raw_body) elif isinstance(request, dict): body = json.dumps(request) diff --git a/src/chat_sdk/adapters/linear/adapter.py b/src/chat_sdk/adapters/linear/adapter.py index fa886a7..c42b7d3 100644 --- a/src/chat_sdk/adapters/linear/adapter.py +++ b/src/chat_sdk/adapters/linear/adapter.py @@ -987,6 +987,10 @@ async def _get_request_body(self, request: Any) -> str: if body is not None: if callable(body): body = body() + # Some frameworks expose `body` as an async method; if calling it + # produced a coroutine, await it before treating as bytes/str. + if inspect.isawaitable(body): + body = await body if hasattr(body, "read"): raw_read = body.read raw = await raw_read() if inspect.iscoroutinefunction(raw_read) else raw_read() diff --git a/src/chat_sdk/adapters/slack/adapter.py b/src/chat_sdk/adapters/slack/adapter.py index 5183a71..b87495b 100644 --- a/src/chat_sdk/adapters/slack/adapter.py +++ b/src/chat_sdk/adapters/slack/adapter.py @@ -696,7 +696,13 @@ async def handle_webhook(self, request: Any, options: WebhookOptions | None = No else: raw = getattr(request, "body", None) if raw is not None: - if asyncio.iscoroutine(raw) or asyncio.isfuture(raw): + # Some frameworks expose `body` as an async method (e.g. + # `async def body(self)`) — call it, then await if the + # result is awaitable. Previously we only handled the + # coroutine-as-attribute case, not the async-method case. + if callable(raw): + raw = raw() + if asyncio.iscoroutine(raw) or asyncio.isfuture(raw) or inspect.isawaitable(raw): raw = await raw body = raw.decode("utf-8") if isinstance(raw, (bytes, bytearray)) else str(raw) else: @@ -1083,7 +1089,7 @@ def _modal_response_to_slack(self, response: ModalResponse, context_id: str | No private_metadata=modal.get("private_metadata"), ) ) - view = modal_to_slack_view(cast("ModalElement", modal), metadata) + view = modal_to_slack_view(cast(ModalElement, modal), metadata) return {"response_action": response.action, "view": view} return {} @@ -2342,7 +2348,7 @@ async def open_modal(self, trigger_id: str, modal: dict[str, Any], context_id: s private_metadata=modal.get("private_metadata"), ) ) - view = modal_to_slack_view(cast("ModalElement", modal), metadata) + view = modal_to_slack_view(cast(ModalElement, modal), metadata) self._logger.debug( "Slack API: views.open", @@ -2359,7 +2365,7 @@ async def open_modal(self, trigger_id: str, modal: dict[str, Any], context_id: s async def update_modal(self, view_id: str, modal: dict[str, Any]) -> dict[str, str]: """Update an existing modal using views.update.""" - view = modal_to_slack_view(cast("ModalElement", modal)) + view = modal_to_slack_view(cast(ModalElement, modal)) try: client = self._get_client() diff --git a/src/chat_sdk/adapters/teams/adapter.py b/src/chat_sdk/adapters/teams/adapter.py index 578017e..e4b7418 100644 --- a/src/chat_sdk/adapters/teams/adapter.py +++ b/src/chat_sdk/adapters/teams/adapter.py @@ -1776,6 +1776,10 @@ async def _get_request_body(self, request: Any) -> str: if body is not None: if callable(body): body = body() + # Some frameworks expose `body` as an async method; if calling it + # produced a coroutine, await it before treating as bytes/str. + if inspect.isawaitable(body): + body = await body if hasattr(body, "read"): raw_read = body.read raw = await raw_read() if inspect.iscoroutinefunction(raw_read) else raw_read() diff --git a/src/chat_sdk/adapters/whatsapp/adapter.py b/src/chat_sdk/adapters/whatsapp/adapter.py index 15b4e98..4b7ad09 100644 --- a/src/chat_sdk/adapters/whatsapp/adapter.py +++ b/src/chat_sdk/adapters/whatsapp/adapter.py @@ -724,13 +724,13 @@ async def post_message( # discrimination, so cast the field access on each branch. result = card_to_whatsapp(card) if result.get("type") == "interactive": - interactive_raw = cast("WhatsAppCardResultInteractive", result)["interactive"] + interactive_raw = cast(WhatsAppCardResultInteractive, result)["interactive"] interactive = json.loads(convert_emoji_placeholders(json.dumps(interactive_raw), "whatsapp")) return await self._send_interactive_message(thread_id, user_wa_id, interactive) return await self._send_text_message( thread_id, user_wa_id, - convert_emoji_placeholders(cast("WhatsAppCardResultText", result)["text"], "whatsapp"), + convert_emoji_placeholders(cast(WhatsAppCardResultText, result)["text"], "whatsapp"), ) # Regular text message @@ -1063,6 +1063,12 @@ async def _get_request_body(request: Any) -> str: return str(await result if inspect.isawaitable(result) else result) body = getattr(request, "body", None) if body is not None: + # Some frameworks expose `body` as an async method; call and + # await if needed before treating as bytes/str. + if callable(body): + body = body() + if inspect.isawaitable(body): + body = await body if isinstance(body, bytes): return body.decode("utf-8") return str(body) diff --git a/src/chat_sdk/chat.py b/src/chat_sdk/chat.py index 78c133e..3d5c10d 100644 --- a/src/chat_sdk/chat.py +++ b/src/chat_sdk/chat.py @@ -874,7 +874,7 @@ async def process_modal_submit( for pat in self._modal_submit_handlers: if not pat.callback_ids or event.callback_id in pat.callback_ids: try: - response = await pat.handler(full_event) + response = await self._invoke_handler(pat.handler, full_event) if response is not None: return response except Exception as exc: @@ -909,7 +909,7 @@ async def _task() -> None: for pat in self._modal_close_handlers: if not pat.callback_ids or event.callback_id in pat.callback_ids: - await pat.handler(full_event) + await self._invoke_handler(pat.handler, full_event) task = _create_task(_task(), self._active_tasks) if task is not None: @@ -948,7 +948,7 @@ def process_assistant_thread_started( ) -> None: async def _task() -> None: for h in self._assistant_thread_started_handlers: - await h(event) + await self._invoke_handler(h, event) task = _create_task(_task(), self._active_tasks) if task is not None: @@ -969,7 +969,7 @@ def process_assistant_context_changed( ) -> None: async def _task() -> None: for h in self._assistant_context_changed_handlers: - await h(event) + await self._invoke_handler(h, event) task = _create_task(_task(), self._active_tasks) if task is not None: @@ -990,7 +990,7 @@ def process_app_home_opened( ) -> None: async def _task() -> None: for h in self._app_home_opened_handlers: - await h(event) + await self._invoke_handler(h, event) task = _create_task(_task(), self._active_tasks) if task is not None: @@ -1011,7 +1011,7 @@ def process_member_joined_channel( ) -> None: async def _task() -> None: for h in self._member_joined_channel_handlers: - await h(event) + await self._invoke_handler(h, event) task = _create_task(_task(), self._active_tasks) if task is not None: @@ -1081,11 +1081,11 @@ async def _open_modal(modal: Any) -> dict[str, str] | None: for pat in self._slash_command_handlers: if not pat.commands: self._logger.debug("Running catch-all slash command handler") - await pat.handler(full_event) + await self._invoke_handler(pat.handler, full_event) continue if event.command in pat.commands: self._logger.debug("Running matched slash command handler", {"command": event.command}) - await pat.handler(full_event) + await self._invoke_handler(pat.handler, full_event) # ======================================================================== # Modal context persistence @@ -1256,11 +1256,11 @@ async def _open_modal(modal: Any) -> dict[str, str] | None: for pat in self._action_handlers: if not pat.action_ids: self._logger.debug("Running catch-all action handler") - await pat.handler(full_event) + await self._invoke_handler(pat.handler, full_event) continue if event.action_id in pat.action_ids: self._logger.debug("Running matched action handler", {"action_id": event.action_id}) - await pat.handler(full_event) + await self._invoke_handler(pat.handler, full_event) # ======================================================================== # Reaction handling @@ -1318,7 +1318,7 @@ async def _handle_reaction_event(self, event: ReactionEvent) -> None: for pat in self._reaction_handlers: if not pat.emoji: self._logger.debug("Running catch-all reaction handler") - await pat.handler(full_event) + await self._invoke_handler(pat.handler, full_event) continue matches = any( @@ -1334,7 +1334,7 @@ async def _handle_reaction_event(self, event: ReactionEvent) -> None: ) if matches: self._logger.debug("Running matched reaction handler") - await pat.handler(full_event) + await self._invoke_handler(pat.handler, full_event) # ======================================================================== # openDM / channel @@ -2045,12 +2045,28 @@ async def _run_handlers( context: MessageContext | None = None, ) -> None: for h in handlers: - result = h(thread, message, context) - # Handlers are typed `Callable[..., Awaitable[None] | None]` — - # sync handlers return None and must NOT be awaited, or we - # raise TypeError at runtime. Narrow with `isawaitable`. - if inspect.isawaitable(result): - await result + await self._invoke_handler(h, thread, message, context) + + @staticmethod + async def _invoke_handler(handler: Any, /, *args: Any, **kwargs: Any) -> Any: + """Invoke a handler and await the result only if awaitable. + + All Chat handler types (message, reaction, action, slash, modal, + options-load, assistant, home, member-joined) are declared as + `Callable[..., Awaitable[T] | T]` — i.e. users may register either + a sync or an async callable. Awaiting the return value + unconditionally raises `TypeError: object NoneType can't be used + in 'await' expression` for sync handlers, so this helper narrows + with `inspect.isawaitable`. + + Returns whatever the handler returned (post-await for async) so + callers that need the value (modal submit → `ModalResponse`) can + still capture it. + """ + result = handler(*args, **kwargs) + if inspect.isawaitable(result): + return await result + return result # --------------------------------------------------------------------------- diff --git a/src/chat_sdk/types.py b/src/chat_sdk/types.py index d9aa856..86e8468 100644 --- a/src/chat_sdk/types.py +++ b/src/chat_sdk/types.py @@ -1346,9 +1346,6 @@ async def handle_incoming_message( adapter: Adapter, thread_id: str, message: Message, - options: WebhookOptions | None = None, - *, - is_subscribed: bool = False, ) -> None: ... def process_action(self, event: Any, options: WebhookOptions | None = None) -> None: ... def process_reaction(self, event: Any, options: WebhookOptions | None = None) -> None: ... From f973cfcf8d4e1c881f770c2b58b12986cf8f6c08 Mon Sep 17 00:00:00 2001 From: patrick-chinchill Date: Thu, 23 Apr 2026 13:59:50 -0700 Subject: [PATCH 5/9] address new review comments on PR #49 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CodeRabbit MAJOR (google_chat/adapter.py:785): non-callable `request.text` was skipped by the `and callable(text_attr)` gate, falling through to the body/str(request) path and turning valid string-text attributes into unparseable output. The bug affects 5 adapters: github, google_chat, whatsapp, slack, telegram. Discord, Linear, and Teams already had the correct pattern. Fix: move the callable check INSIDE the `if text_attr is not None` branch so populated string/bytes attributes are consumed. ```python # Before (buggy): if text_attr is not None and callable(text_attr): result = text_attr() body = str(await result if inspect.isawaitable(result) else result) else: # Fall through to body — wrong if text_attr was a string! ... # After (correct): if text_attr is not None: if callable(text_attr): result = text_attr() text_attr = await result if inspect.isawaitable(result) else result body = text_attr.decode("utf-8") if isinstance(text_attr, bytes) else str(text_attr) else: # body fallback only when text is actually absent ... ``` Also addresses github-code-quality "Statement has no effect" (types.py:1349): collapse the multi-line `) -> None: ...` stub for `ChatInstance.handle_incoming_message` to single-line to match the other protocol stubs and silence CodeQL's "naked ellipsis" false positive. Validation: - 3469 tests pass - ruff check + format clean - pyrefly 0 errors --- src/chat_sdk/adapters/github/adapter.py | 11 ++++++++--- src/chat_sdk/adapters/google_chat/adapter.py | 13 ++++++++++--- src/chat_sdk/adapters/slack/adapter.py | 11 ++++++++--- src/chat_sdk/adapters/telegram/adapter.py | 10 +++++++--- src/chat_sdk/adapters/whatsapp/adapter.py | 10 +++++++--- src/chat_sdk/types.py | 7 +------ 6 files changed, 41 insertions(+), 21 deletions(-) diff --git a/src/chat_sdk/adapters/github/adapter.py b/src/chat_sdk/adapters/github/adapter.py index 972670e..1d5d03d 100644 --- a/src/chat_sdk/adapters/github/adapter.py +++ b/src/chat_sdk/adapters/github/adapter.py @@ -1089,10 +1089,15 @@ async def _get_request_body(request: Any) -> str: """Extract body text from a request object.""" # `hasattr` narrows `Any` → `object` (which is not awaitable), so # `getattr(..., None)` keeps `Any` for the framework duck-type path. + # Handle both callable (`async def text(self)`) and non-callable + # (`text: str` property) cases — gating entry on callability + # would silently drop valid string attributes. text_attr = getattr(request, "text", None) - if text_attr is not None and callable(text_attr): - result = text_attr() - return str(await result if inspect.isawaitable(result) else result) + if text_attr is not None: + if callable(text_attr): + result = text_attr() + text_attr = await result if inspect.isawaitable(result) else result + return text_attr.decode("utf-8") if isinstance(text_attr, bytes) else str(text_attr) body = getattr(request, "body", None) if body is not None: # Some frameworks expose `body` as an async method; if calling it diff --git a/src/chat_sdk/adapters/google_chat/adapter.py b/src/chat_sdk/adapters/google_chat/adapter.py index 9083f8f..859611d 100644 --- a/src/chat_sdk/adapters/google_chat/adapter.py +++ b/src/chat_sdk/adapters/google_chat/adapter.py @@ -764,11 +764,18 @@ async def handle_webhook( # Parse request body. `hasattr` narrows `Any` → `object` (not # awaitable); `getattr(..., None)` preserves `Any` for the # framework duck-typed path. + # `request.text` may be either an async method (aiohttp, FastAPI) + # or a populated string/bytes attribute (Django raw HttpRequest, + # some mock objects). Handle both — testing callability inside + # the non-None branch, not gating entry on it, so non-callable + # text attributes aren't silently dropped to the body branch. body: str text_attr = getattr(request, "text", None) - if text_attr is not None and callable(text_attr): - result = text_attr() - body = str(await result if inspect.isawaitable(result) else result) + if text_attr is not None: + if callable(text_attr): + result = text_attr() + text_attr = await result if inspect.isawaitable(result) else result + body = text_attr.decode("utf-8") if isinstance(text_attr, bytes) else str(text_attr) else: raw_body = getattr(request, "body", None) if raw_body is not None: diff --git a/src/chat_sdk/adapters/slack/adapter.py b/src/chat_sdk/adapters/slack/adapter.py index b87495b..85d12e2 100644 --- a/src/chat_sdk/adapters/slack/adapter.py +++ b/src/chat_sdk/adapters/slack/adapter.py @@ -688,11 +688,16 @@ async def handle_webhook(self, request: Any, options: WebhookOptions | None = No # Read the raw body. `hasattr` narrows `Any` → `object` (not # awaitable), so we use `getattr(..., None)` to preserve the # `Any` type across the duck-typed framework branches. + # Handle both callable (`async def text(self)`) and non-callable + # (`text: str` attribute) forms of `request.text`. Gating entry + # on callability would drop populated string attributes. text_attr = getattr(request, "text", None) body: str - if text_attr is not None and callable(text_attr): - result = text_attr() - body = str(await result if inspect.isawaitable(result) else result) + if text_attr is not None: + if callable(text_attr): + result = text_attr() + text_attr = await result if inspect.isawaitable(result) else result + body = text_attr.decode("utf-8") if isinstance(text_attr, (bytes, bytearray)) else str(text_attr) else: raw = getattr(request, "body", None) if raw is not None: diff --git a/src/chat_sdk/adapters/telegram/adapter.py b/src/chat_sdk/adapters/telegram/adapter.py index f4a34ac..ac994c5 100644 --- a/src/chat_sdk/adapters/telegram/adapter.py +++ b/src/chat_sdk/adapters/telegram/adapter.py @@ -1921,10 +1921,14 @@ async def _get_request_body(request: Any) -> str: """Extract body text from a framework-agnostic request object.""" # `hasattr` narrows `Any` → `object` (not awaitable); `getattr(..., None)` # preserves `Any` for the duck-typed framework paths. + # Handle both callable and non-callable `request.text` forms. + # Gating entry on callability would drop populated string attrs. text_attr = getattr(request, "text", None) - if text_attr is not None and callable(text_attr): - result = text_attr() - return str(await result if inspect.isawaitable(result) else result) + if text_attr is not None: + if callable(text_attr): + result = text_attr() + text_attr = await result if inspect.isawaitable(result) else result + return text_attr.decode("utf-8") if isinstance(text_attr, bytes) else str(text_attr) body = getattr(request, "body", None) if body is not None: if callable(body): diff --git a/src/chat_sdk/adapters/whatsapp/adapter.py b/src/chat_sdk/adapters/whatsapp/adapter.py index 4b7ad09..e9d9f0c 100644 --- a/src/chat_sdk/adapters/whatsapp/adapter.py +++ b/src/chat_sdk/adapters/whatsapp/adapter.py @@ -1057,10 +1057,14 @@ async def _get_request_body(request: Any) -> str: """Extract body text from a request object.""" # `hasattr` narrows `Any` → `object` (not awaitable); using # `getattr(..., None)` preserves `Any` for framework duck-typing. + # Handle both callable and non-callable `request.text`. Gating + # entry on callability would drop populated string attributes. text_attr = getattr(request, "text", None) - if text_attr is not None and callable(text_attr): - result = text_attr() - return str(await result if inspect.isawaitable(result) else result) + if text_attr is not None: + if callable(text_attr): + result = text_attr() + text_attr = await result if inspect.isawaitable(result) else result + return text_attr.decode("utf-8") if isinstance(text_attr, bytes) else str(text_attr) body = getattr(request, "body", None) if body is not None: # Some frameworks expose `body` as an async method; call and diff --git a/src/chat_sdk/types.py b/src/chat_sdk/types.py index 86e8468..439d7e2 100644 --- a/src/chat_sdk/types.py +++ b/src/chat_sdk/types.py @@ -1341,12 +1341,7 @@ def process_message( message: Message | Callable[[], Awaitable[Message]], options: WebhookOptions | None = None, ) -> None: ... - async def handle_incoming_message( - self, - adapter: Adapter, - thread_id: str, - message: Message, - ) -> None: ... + async def handle_incoming_message(self, adapter: Adapter, thread_id: str, message: Message) -> None: ... def process_action(self, event: Any, options: WebhookOptions | None = None) -> None: ... def process_reaction(self, event: Any, options: WebhookOptions | None = None) -> None: ... def process_slash_command(self, event: Any, options: WebhookOptions | None = None) -> None: ... From 8e1baa0c5dff33bf76001398fa244798b0bcd275 Mon Sep 17 00:00:00 2001 From: patrick-chinchill Date: Thu, 23 Apr 2026 14:10:05 -0700 Subject: [PATCH 6/9] add regression tests + bytearray consistency for request body extraction MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Self-review findings on PR #49: 1. **bytearray inconsistency**: github / google_chat / whatsapp / telegram text-attr path used `isinstance(text_attr, bytes)`, missing bytearray. Slack's already had `(bytes, bytearray)`. Unified all four to match. 2. **Zero regression coverage** for the non-callable `request.text` string-attribute path — the fix's raison d'être shipped untested. Added `tests/test_request_body_extraction.py` with 7 cases × 3 static-method adapters (github, telegram, whatsapp) = 21 tests. Cases covered per adapter: - `text` as plain string attribute (Django-style) - `text` as bytes attribute (decoded UTF-8) - `text` as bytearray attribute (bytes/bytearray symmetry) - `text` as sync method (Flask-style) - `text` as async method (aiohttp/FastAPI) - `body` as async method (FastAPI `async def body(self)`) - `body` as bytes property (Django-style) Discord/Teams/Slack/Linear/Google Chat have instance methods (not static) — their extractor requires adapter construction which needs real platform config. Their paths are exercised via existing integration tests; the 3 static adapters cover the same patterns with less setup overhead and catch the specific historical bugs (non-callable text gate, sync text() crash, async body() coroutine stringification, bytes/bytearray asymmetry). Validation: - 3490 tests pass (21 new) - ruff check + format clean - pyrefly 0 errors --- src/chat_sdk/adapters/github/adapter.py | 2 +- src/chat_sdk/adapters/google_chat/adapter.py | 2 +- src/chat_sdk/adapters/telegram/adapter.py | 2 +- src/chat_sdk/adapters/whatsapp/adapter.py | 2 +- tests/test_request_body_extraction.py | 214 +++++++++++++++++++ 5 files changed, 218 insertions(+), 4 deletions(-) create mode 100644 tests/test_request_body_extraction.py diff --git a/src/chat_sdk/adapters/github/adapter.py b/src/chat_sdk/adapters/github/adapter.py index 1d5d03d..3351ec2 100644 --- a/src/chat_sdk/adapters/github/adapter.py +++ b/src/chat_sdk/adapters/github/adapter.py @@ -1097,7 +1097,7 @@ async def _get_request_body(request: Any) -> str: if callable(text_attr): result = text_attr() text_attr = await result if inspect.isawaitable(result) else result - return text_attr.decode("utf-8") if isinstance(text_attr, bytes) else str(text_attr) + return text_attr.decode("utf-8") if isinstance(text_attr, (bytes, bytearray)) else str(text_attr) body = getattr(request, "body", None) if body is not None: # Some frameworks expose `body` as an async method; if calling it diff --git a/src/chat_sdk/adapters/google_chat/adapter.py b/src/chat_sdk/adapters/google_chat/adapter.py index 859611d..559aa15 100644 --- a/src/chat_sdk/adapters/google_chat/adapter.py +++ b/src/chat_sdk/adapters/google_chat/adapter.py @@ -775,7 +775,7 @@ async def handle_webhook( if callable(text_attr): result = text_attr() text_attr = await result if inspect.isawaitable(result) else result - body = text_attr.decode("utf-8") if isinstance(text_attr, bytes) else str(text_attr) + body = text_attr.decode("utf-8") if isinstance(text_attr, (bytes, bytearray)) else str(text_attr) else: raw_body = getattr(request, "body", None) if raw_body is not None: diff --git a/src/chat_sdk/adapters/telegram/adapter.py b/src/chat_sdk/adapters/telegram/adapter.py index ac994c5..1af64af 100644 --- a/src/chat_sdk/adapters/telegram/adapter.py +++ b/src/chat_sdk/adapters/telegram/adapter.py @@ -1928,7 +1928,7 @@ async def _get_request_body(request: Any) -> str: if callable(text_attr): result = text_attr() text_attr = await result if inspect.isawaitable(result) else result - return text_attr.decode("utf-8") if isinstance(text_attr, bytes) else str(text_attr) + return text_attr.decode("utf-8") if isinstance(text_attr, (bytes, bytearray)) else str(text_attr) body = getattr(request, "body", None) if body is not None: if callable(body): diff --git a/src/chat_sdk/adapters/whatsapp/adapter.py b/src/chat_sdk/adapters/whatsapp/adapter.py index e9d9f0c..69622bc 100644 --- a/src/chat_sdk/adapters/whatsapp/adapter.py +++ b/src/chat_sdk/adapters/whatsapp/adapter.py @@ -1064,7 +1064,7 @@ async def _get_request_body(request: Any) -> str: if callable(text_attr): result = text_attr() text_attr = await result if inspect.isawaitable(result) else result - return text_attr.decode("utf-8") if isinstance(text_attr, bytes) else str(text_attr) + return text_attr.decode("utf-8") if isinstance(text_attr, (bytes, bytearray)) else str(text_attr) body = getattr(request, "body", None) if body is not None: # Some frameworks expose `body` as an async method; call and diff --git a/tests/test_request_body_extraction.py b/tests/test_request_body_extraction.py new file mode 100644 index 0000000..5686bbd --- /dev/null +++ b/tests/test_request_body_extraction.py @@ -0,0 +1,214 @@ +"""Regression tests for `_get_request_body` body-extraction across adapters. + +Each adapter's webhook handler normalizes the incoming request object via a +framework-agnostic `_get_request_body` helper. The helper supports multiple +frameworks by duck-typing on `request.text` and `request.body` — each of which +may be: + +- a plain string/bytes attribute (Django raw HttpRequest, some mocks) +- a synchronous method (older frameworks, test helpers) +- an async method / coroutine-returning callable (aiohttp, FastAPI, Starlette) + +Historical bugs in this code path have been: + +1. `hasattr(request, "text") and callable(request.text)` gate silently dropped + non-callable `request.text` string attributes, falling through to `body`. + Fixed across 5 adapters by restructuring the branch. +2. `await request.text()` without `isawaitable` narrowing crashed on Flask-style + sync `text` methods with `TypeError: object is not awaitable`. Fixed by + guarding with `inspect.isawaitable`. +3. `request.body` exposed as an async method (`async def body(self)`) returned + a coroutine that was stringified instead of awaited. Fixed by adding the + same callable + isawaitable dance for `body`. +4. `isinstance(body, bytes)` missed `bytearray`. Fixed by using + `(bytes, bytearray)` consistently. + +Each test below locks in one of those cases per adapter. +""" + +from __future__ import annotations + +from typing import Any + +import pytest + +pytestmark = pytest.mark.asyncio + + +# --------------------------------------------------------------------------- +# Fake request objects covering the shapes we need to support. +# --------------------------------------------------------------------------- + + +class _StringTextRequest: + """`request.text` is a populated string attribute (Django-style).""" + + def __init__(self, body: str) -> None: + self.text = body + + +class _BytesTextRequest: + """`request.text` is populated bytes.""" + + def __init__(self, body: bytes) -> None: + self.text = body + + +class _BytearrayTextRequest: + """`request.text` is a bytearray.""" + + def __init__(self, body: bytearray) -> None: + self.text = body + + +class _SyncCallableTextRequest: + """`request.text` is a sync method (Flask-style).""" + + def __init__(self, body: str) -> None: + self._body = body + + def text(self) -> str: # pragma: no cover - called + return self._body + + +class _AsyncCallableTextRequest: + """`request.text` is an async method (aiohttp/FastAPI).""" + + def __init__(self, body: str) -> None: + self._body = body + + async def text(self) -> str: + return self._body + + +class _AsyncCallableBodyRequest: + """`request.body` is an async method returning bytes.""" + + def __init__(self, body: bytes) -> None: + self._body = body + + async def body(self) -> bytes: + return self._body + + +class _SyncCallableBodyRequest: + """`request.body` is a sync method returning bytes.""" + + def __init__(self, body: bytes) -> None: + self._body = body + + def body(self) -> bytes: # pragma: no cover - called + return self._body + + +class _PropertyBodyRequest: + """`request.body` is a bytes property.""" + + def __init__(self, body: bytes) -> None: + self.body = body + + +class _BytearrayBodyRequest: + """`request.body` is a bytearray.""" + + def __init__(self, body: bytearray) -> None: + self.body = body + + +# --------------------------------------------------------------------------- +# Per-adapter matrix — each adapter exposes `_get_request_body` (static or +# instance). We run the same 7 cases through each to lock in the contract. +# --------------------------------------------------------------------------- + + +def _adapters() -> list[tuple[str, Any]]: + """Return (name, extractor) pairs for each adapter's body extraction. + + Lazy imports because some adapters require platform deps (cryptography, + pynacl). If a dep isn't installed, that adapter is skipped. + """ + result: list[tuple[str, Any]] = [] + + # GitHub (static method) + try: + from chat_sdk.adapters.github.adapter import GitHubAdapter + + result.append(("github", GitHubAdapter._get_request_body)) + except ImportError: + pass + + # Telegram (static method) + try: + from chat_sdk.adapters.telegram.adapter import TelegramAdapter + + result.append(("telegram", TelegramAdapter._get_request_body)) + except ImportError: + pass + + # WhatsApp (static method) + try: + from chat_sdk.adapters.whatsapp.adapter import WhatsAppAdapter + + result.append(("whatsapp", WhatsAppAdapter._get_request_body)) + except ImportError: + pass + + return result + + +@pytest.mark.parametrize("name,extractor", _adapters()) +async def test_handles_string_text_attribute(name: str, extractor: Any) -> None: + """Non-callable `request.text` string attribute is consumed — the + historical bug was silently falling through to `body`, which + produced ``-style stringified objects for valid webhooks. + """ + result = await extractor(_StringTextRequest('{"ok": true}')) + assert result == '{"ok": true}', f"{name} failed string text attr" + + +@pytest.mark.parametrize("name,extractor", _adapters()) +async def test_handles_bytes_text_attribute(name: str, extractor: Any) -> None: + """Bytes `request.text` is decoded as UTF-8.""" + result = await extractor(_BytesTextRequest(b'{"ok": true}')) + assert result == '{"ok": true}', f"{name} failed bytes text attr" + + +@pytest.mark.parametrize("name,extractor", _adapters()) +async def test_handles_bytearray_text_attribute(name: str, extractor: Any) -> None: + """Bytearray `request.text` is also decoded — bytes/bytearray symmetry.""" + result = await extractor(_BytearrayTextRequest(bytearray(b'{"ok": true}'))) + assert result == '{"ok": true}', f"{name} failed bytearray text attr" + + +@pytest.mark.parametrize("name,extractor", _adapters()) +async def test_handles_async_callable_text(name: str, extractor: Any) -> None: + """`async def text(self)` returning a coroutine is awaited.""" + result = await extractor(_AsyncCallableTextRequest('{"ok": true}')) + assert result == '{"ok": true}', f"{name} failed async text()" + + +@pytest.mark.parametrize("name,extractor", _adapters()) +async def test_handles_sync_callable_text(name: str, extractor: Any) -> None: + """Sync `def text(self)` returns the body directly — the + historical bug was `await request.text()` crashing on this path. + """ + result = await extractor(_SyncCallableTextRequest('{"ok": true}')) + assert result == '{"ok": true}', f"{name} failed sync text()" + + +@pytest.mark.parametrize("name,extractor", _adapters()) +async def test_handles_async_callable_body(name: str, extractor: Any) -> None: + """Falls back to `request.body` when no `text`. Historical bug: + `async def body(self)` returning a coroutine was stringified as + `` instead of awaited, breaking webhook + signature verification on FastAPI/Starlette. + """ + result = await extractor(_AsyncCallableBodyRequest(b'{"ok": true}')) + assert result == '{"ok": true}', f"{name} failed async body()" + + +@pytest.mark.parametrize("name,extractor", _adapters()) +async def test_handles_bytes_body_attribute(name: str, extractor: Any) -> None: + """`request.body` as a bytes property is decoded as UTF-8.""" + result = await extractor(_PropertyBodyRequest(b'{"ok": true}')) + assert result == '{"ok": true}', f"{name} failed bytes body attr" From e477c2c240f49b2ba4f64e39852699a09c6ed0e2 Mon Sep 17 00:00:00 2001 From: patrick-chinchill Date: Thu, 23 Apr 2026 14:16:53 -0700 Subject: [PATCH 7/9] extend bytearray consistency to body-path + add regression test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Self-review caught the asymmetry: previous commit unified bytearray handling on the text-path only. All 7 adapters still used `isinstance(body, bytes)` on the body-path — a bytearray body would fall through to `str(bytearray(...))` producing the repr string `"bytearray(b'...')"` instead of the decoded content. Applied `(bytes, bytearray)` symmetrically across all 7 adapters (discord, linear, google_chat, github, whatsapp, telegram, teams). Also added a `test_handles_bytearray_body_attribute` regression test covering the new path — parameterized across the 3 static-method adapters that the test matrix reaches. --- src/chat_sdk/adapters/discord/adapter.py | 4 ++-- src/chat_sdk/adapters/github/adapter.py | 2 +- src/chat_sdk/adapters/google_chat/adapter.py | 2 +- src/chat_sdk/adapters/linear/adapter.py | 4 ++-- src/chat_sdk/adapters/teams/adapter.py | 4 ++-- src/chat_sdk/adapters/telegram/adapter.py | 2 +- src/chat_sdk/adapters/whatsapp/adapter.py | 2 +- tests/test_request_body_extraction.py | 10 ++++++++++ 8 files changed, 20 insertions(+), 10 deletions(-) diff --git a/src/chat_sdk/adapters/discord/adapter.py b/src/chat_sdk/adapters/discord/adapter.py index a031f45..92e2ab6 100644 --- a/src/chat_sdk/adapters/discord/adapter.py +++ b/src/chat_sdk/adapters/discord/adapter.py @@ -1419,8 +1419,8 @@ async def _get_request_body(self, request: Any) -> str: if hasattr(body, "read"): raw_read = body.read raw = await raw_read() if inspect.iscoroutinefunction(raw_read) else raw_read() - return raw.decode("utf-8") if isinstance(raw, bytes) else raw - return body.decode("utf-8") if isinstance(body, bytes) else str(body) + return raw.decode("utf-8") if isinstance(raw, (bytes, bytearray)) else raw + return body.decode("utf-8") if isinstance(body, (bytes, bytearray)) else str(body) text_attr = getattr(request, "text", None) if text_attr is not None: if callable(text_attr): diff --git a/src/chat_sdk/adapters/github/adapter.py b/src/chat_sdk/adapters/github/adapter.py index 3351ec2..1f11849 100644 --- a/src/chat_sdk/adapters/github/adapter.py +++ b/src/chat_sdk/adapters/github/adapter.py @@ -1106,7 +1106,7 @@ async def _get_request_body(request: Any) -> str: body = body() if inspect.isawaitable(body): body = await body - return body.decode("utf-8") if isinstance(body, bytes) else str(body) + return body.decode("utf-8") if isinstance(body, (bytes, bytearray)) else str(body) return "" @staticmethod diff --git a/src/chat_sdk/adapters/google_chat/adapter.py b/src/chat_sdk/adapters/google_chat/adapter.py index 559aa15..143f68e 100644 --- a/src/chat_sdk/adapters/google_chat/adapter.py +++ b/src/chat_sdk/adapters/google_chat/adapter.py @@ -785,7 +785,7 @@ async def handle_webhook( raw_body = raw_body() if inspect.isawaitable(raw_body): raw_body = await raw_body - body = raw_body.decode("utf-8") if isinstance(raw_body, bytes) else str(raw_body) + body = raw_body.decode("utf-8") if isinstance(raw_body, (bytes, bytearray)) else str(raw_body) elif isinstance(request, dict): body = json.dumps(request) else: diff --git a/src/chat_sdk/adapters/linear/adapter.py b/src/chat_sdk/adapters/linear/adapter.py index c42b7d3..c8d5ebc 100644 --- a/src/chat_sdk/adapters/linear/adapter.py +++ b/src/chat_sdk/adapters/linear/adapter.py @@ -994,8 +994,8 @@ async def _get_request_body(self, request: Any) -> str: if hasattr(body, "read"): raw_read = body.read raw = await raw_read() if inspect.iscoroutinefunction(raw_read) else raw_read() - return raw.decode("utf-8") if isinstance(raw, bytes) else raw - return body.decode("utf-8") if isinstance(body, bytes) else str(body) + return raw.decode("utf-8") if isinstance(raw, (bytes, bytearray)) else raw + return body.decode("utf-8") if isinstance(body, (bytes, bytearray)) else str(body) text_attr = getattr(request, "text", None) if text_attr is not None: if callable(text_attr): diff --git a/src/chat_sdk/adapters/teams/adapter.py b/src/chat_sdk/adapters/teams/adapter.py index e4b7418..0bc88aa 100644 --- a/src/chat_sdk/adapters/teams/adapter.py +++ b/src/chat_sdk/adapters/teams/adapter.py @@ -1783,8 +1783,8 @@ async def _get_request_body(self, request: Any) -> str: if hasattr(body, "read"): raw_read = body.read raw = await raw_read() if inspect.iscoroutinefunction(raw_read) else raw_read() - return raw.decode("utf-8") if isinstance(raw, bytes) else raw - return body.decode("utf-8") if isinstance(body, bytes) else str(body) + return raw.decode("utf-8") if isinstance(raw, (bytes, bytearray)) else raw + return body.decode("utf-8") if isinstance(body, (bytes, bytearray)) else str(body) text_attr = getattr(request, "text", None) if text_attr is not None: if callable(text_attr): diff --git a/src/chat_sdk/adapters/telegram/adapter.py b/src/chat_sdk/adapters/telegram/adapter.py index 1af64af..6721751 100644 --- a/src/chat_sdk/adapters/telegram/adapter.py +++ b/src/chat_sdk/adapters/telegram/adapter.py @@ -1934,7 +1934,7 @@ async def _get_request_body(request: Any) -> str: if callable(body): result = body() body = await result if inspect.isawaitable(result) else result - if isinstance(body, bytes): + if isinstance(body, (bytes, bytearray)): return body.decode("utf-8") return str(body) return "" diff --git a/src/chat_sdk/adapters/whatsapp/adapter.py b/src/chat_sdk/adapters/whatsapp/adapter.py index 69622bc..983bfb0 100644 --- a/src/chat_sdk/adapters/whatsapp/adapter.py +++ b/src/chat_sdk/adapters/whatsapp/adapter.py @@ -1073,7 +1073,7 @@ async def _get_request_body(request: Any) -> str: body = body() if inspect.isawaitable(body): body = await body - if isinstance(body, bytes): + if isinstance(body, (bytes, bytearray)): return body.decode("utf-8") return str(body) return "" diff --git a/tests/test_request_body_extraction.py b/tests/test_request_body_extraction.py index 5686bbd..11d8ac3 100644 --- a/tests/test_request_body_extraction.py +++ b/tests/test_request_body_extraction.py @@ -212,3 +212,13 @@ async def test_handles_bytes_body_attribute(name: str, extractor: Any) -> None: """`request.body` as a bytes property is decoded as UTF-8.""" result = await extractor(_PropertyBodyRequest(b'{"ok": true}')) assert result == '{"ok": true}', f"{name} failed bytes body attr" + + +@pytest.mark.parametrize("name,extractor", _adapters()) +async def test_handles_bytearray_body_attribute(name: str, extractor: Any) -> None: + """`request.body` as bytearray — bytes/bytearray symmetry on the body + path, catching the asymmetry where we fixed text-path for bytearray + but left body-path checking only `isinstance(body, bytes)`. + """ + result = await extractor(_BytearrayBodyRequest(bytearray(b'{"ok": true}'))) + assert result == '{"ok": true}', f"{name} failed bytearray body attr" From 72e96996dcc74f289234d59f24eae4dcd1e904c0 Mon Sep 17 00:00:00 2001 From: patrick-chinchill Date: Thu, 23 Apr 2026 14:30:18 -0700 Subject: [PATCH 8/9] address review nits: pyrefly comment wording + empty-except docs - pyproject.toml: drop stale baseline wording; state zero-error CI policy directly (coderabbit). - test_request_body_extraction: annotate `except ImportError: pass` as skipping adapters whose optional deps aren't installed (github-code- quality empty-except flag). Co-Authored-By: Claude Opus 4.7 (1M context) --- pyproject.toml | 3 +-- tests/test_request_body_extraction.py | 6 +++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 1a5eb61..36b34b6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -121,5 +121,4 @@ replace-imports-with-any = [ ] [tool.pyrefly.errors] -# Default severity. Known issues tracked in .pyrefly-baseline.json — -# new errors fail CI, existing ones are allowed. +# CI policy: zero pyrefly errors. diff --git a/tests/test_request_body_extraction.py b/tests/test_request_body_extraction.py index 11d8ac3..f73b1a1 100644 --- a/tests/test_request_body_extraction.py +++ b/tests/test_request_body_extraction.py @@ -135,7 +135,7 @@ def _adapters() -> list[tuple[str, Any]]: result.append(("github", GitHubAdapter._get_request_body)) except ImportError: - pass + pass # Optional platform dep missing — skip this adapter's cases. # Telegram (static method) try: @@ -143,7 +143,7 @@ def _adapters() -> list[tuple[str, Any]]: result.append(("telegram", TelegramAdapter._get_request_body)) except ImportError: - pass + pass # Optional platform dep missing — skip this adapter's cases. # WhatsApp (static method) try: @@ -151,7 +151,7 @@ def _adapters() -> list[tuple[str, Any]]: result.append(("whatsapp", WhatsAppAdapter._get_request_body)) except ImportError: - pass + pass # Optional platform dep missing — skip this adapter's cases. return result From a422389a1602455f32f3aa56df45297674c5cb58 Mon Sep 17 00:00:00 2001 From: patrick-chinchill Date: Thu, 23 Apr 2026 14:57:19 -0700 Subject: [PATCH 9/9] fix(adapters): align discord/linear/teams body extraction with working pattern MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CodeRabbit flagged three real bugs in the discord/linear/teams `_get_request_body` implementations that the github/telegram/whatsapp fix didn't cover: - `str(await result if ... else result)` wrong-stringifies bytes returned from a callable `request.text()` (e.g. `str(b"...") -> "b'...'"`). - Non-callable `request.text` holding `bytes`/`bytearray` returns raw bytes to a `-> str` signature, typecheck lie + caller breaks. - `data` fallback missed `bytearray` (matches the earlier text/body bytes-vs-bytearray fix). - `read()` was only awaited via `iscoroutinefunction(raw_read)`, which misses wrapped/decorated methods that return an awaitable. Switched to `inspect.isawaitable(raw_result)` on the call result. Restructured all three to match the text-first shape used by the static adapters (github/telegram/whatsapp) and marked them `@staticmethod` — `self` was unused and consistency avoids future regressions. Tests: extended `test_request_body_extraction.py` matrix from 3 adapters to 6 and added `test_handles_sync_callable_body` for the previously unexercised sync body() branch. Dropped the broad `except ImportError: pass` guard — every adapter lazy-imports platform deps inside methods, so direct imports work and a broken adapter module now fails the suite instead of silently skipping (matches the "every test must fail when the code is wrong" rule). 54 parametrized cases (9 shapes × 6 adapters) — all passing. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/chat_sdk/adapters/discord/adapter.py | 30 ++++++------ src/chat_sdk/adapters/linear/adapter.py | 30 ++++++------ src/chat_sdk/adapters/teams/adapter.py | 25 +++++----- tests/test_request_body_extraction.py | 59 ++++++++++++------------ 4 files changed, 76 insertions(+), 68 deletions(-) diff --git a/src/chat_sdk/adapters/discord/adapter.py b/src/chat_sdk/adapters/discord/adapter.py index 92e2ab6..3fd9b6a 100644 --- a/src/chat_sdk/adapters/discord/adapter.py +++ b/src/chat_sdk/adapters/discord/adapter.py @@ -1403,11 +1403,19 @@ async def _discord_fetch( # Request/Response helpers (framework-agnostic) # ========================================================================= - async def _get_request_body(self, request: Any) -> str: + @staticmethod + async def _get_request_body(request: Any) -> str: """Extract the request body as a string.""" - # `hasattr` narrows `Any` to `object` and kills downstream type info, - # so we use `getattr(..., default)` which preserves `Any`. The - # `await` below is safe because `inspect.isawaitable` guards it. + # `hasattr` narrows `Any` → `object` (not awaitable); using + # `getattr(..., None)` preserves `Any` for framework duck-typing. + # Handle both callable and non-callable `request.text`. Gating + # entry on callability would drop populated string attributes. + text_attr = getattr(request, "text", None) + if text_attr is not None: + if callable(text_attr): + result = text_attr() + text_attr = await result if inspect.isawaitable(result) else result + return text_attr.decode("utf-8") if isinstance(text_attr, (bytes, bytearray)) else str(text_attr) body = getattr(request, "body", None) if body is not None: if callable(body): @@ -1417,19 +1425,13 @@ async def _get_request_body(self, request: Any) -> str: if inspect.isawaitable(body): body = await body if hasattr(body, "read"): - raw_read = body.read - raw = await raw_read() if inspect.iscoroutinefunction(raw_read) else raw_read() - return raw.decode("utf-8") if isinstance(raw, (bytes, bytearray)) else raw + raw_result = body.read() + raw = await raw_result if inspect.isawaitable(raw_result) else raw_result + return raw.decode("utf-8") if isinstance(raw, (bytes, bytearray)) else str(raw) return body.decode("utf-8") if isinstance(body, (bytes, bytearray)) else str(body) - text_attr = getattr(request, "text", None) - if text_attr is not None: - if callable(text_attr): - result = text_attr() - return str(await result if inspect.isawaitable(result) else result) - return text_attr data = getattr(request, "data", None) if data is not None: - return data.decode("utf-8") if isinstance(data, bytes) else str(data) + return data.decode("utf-8") if isinstance(data, (bytes, bytearray)) else str(data) return "" def _get_header(self, request: Any, name: str) -> str | None: diff --git a/src/chat_sdk/adapters/linear/adapter.py b/src/chat_sdk/adapters/linear/adapter.py index c8d5ebc..ea724f6 100644 --- a/src/chat_sdk/adapters/linear/adapter.py +++ b/src/chat_sdk/adapters/linear/adapter.py @@ -978,11 +978,19 @@ async def _graphql_query( # Request/Response helpers (framework-agnostic) # ========================================================================= - async def _get_request_body(self, request: Any) -> str: + @staticmethod + async def _get_request_body(request: Any) -> str: """Extract the request body as a string.""" - # `hasattr` narrows `Any` → `object` (which is not awaitable); using - # `getattr(..., None)` preserves `Any` so the downstream `await` on - # framework-returned coroutines type-checks. + # `hasattr` narrows `Any` → `object` (not awaitable); using + # `getattr(..., None)` preserves `Any` for framework duck-typing. + # Handle both callable and non-callable `request.text`. Gating + # entry on callability would drop populated string attributes. + text_attr = getattr(request, "text", None) + if text_attr is not None: + if callable(text_attr): + result = text_attr() + text_attr = await result if inspect.isawaitable(result) else result + return text_attr.decode("utf-8") if isinstance(text_attr, (bytes, bytearray)) else str(text_attr) body = getattr(request, "body", None) if body is not None: if callable(body): @@ -992,19 +1000,13 @@ async def _get_request_body(self, request: Any) -> str: if inspect.isawaitable(body): body = await body if hasattr(body, "read"): - raw_read = body.read - raw = await raw_read() if inspect.iscoroutinefunction(raw_read) else raw_read() - return raw.decode("utf-8") if isinstance(raw, (bytes, bytearray)) else raw + raw_result = body.read() + raw = await raw_result if inspect.isawaitable(raw_result) else raw_result + return raw.decode("utf-8") if isinstance(raw, (bytes, bytearray)) else str(raw) return body.decode("utf-8") if isinstance(body, (bytes, bytearray)) else str(body) - text_attr = getattr(request, "text", None) - if text_attr is not None: - if callable(text_attr): - result = text_attr() - return str(await result if inspect.isawaitable(result) else result) - return text_attr data = getattr(request, "data", None) if data is not None: - return data.decode("utf-8") if isinstance(data, bytes) else str(data) + return data.decode("utf-8") if isinstance(data, (bytes, bytearray)) else str(data) return "" def _get_header(self, request: Any, name: str) -> str | None: diff --git a/src/chat_sdk/adapters/teams/adapter.py b/src/chat_sdk/adapters/teams/adapter.py index 0bc88aa..9439709 100644 --- a/src/chat_sdk/adapters/teams/adapter.py +++ b/src/chat_sdk/adapters/teams/adapter.py @@ -1768,10 +1768,19 @@ async def _verify_bot_framework_token(self, request: Any) -> Any | None: # Request/Response helpers (framework-agnostic) # ========================================================================= - async def _get_request_body(self, request: Any) -> str: + @staticmethod + async def _get_request_body(request: Any) -> str: """Extract the request body as a string.""" # `hasattr` narrows `Any` → `object` (not awaitable); using # `getattr(..., None)` preserves `Any` for framework duck-typing. + # Handle both callable and non-callable `request.text`. Gating + # entry on callability would drop populated string attributes. + text_attr = getattr(request, "text", None) + if text_attr is not None: + if callable(text_attr): + result = text_attr() + text_attr = await result if inspect.isawaitable(result) else result + return text_attr.decode("utf-8") if isinstance(text_attr, (bytes, bytearray)) else str(text_attr) body = getattr(request, "body", None) if body is not None: if callable(body): @@ -1781,19 +1790,13 @@ async def _get_request_body(self, request: Any) -> str: if inspect.isawaitable(body): body = await body if hasattr(body, "read"): - raw_read = body.read - raw = await raw_read() if inspect.iscoroutinefunction(raw_read) else raw_read() - return raw.decode("utf-8") if isinstance(raw, (bytes, bytearray)) else raw + raw_result = body.read() + raw = await raw_result if inspect.isawaitable(raw_result) else raw_result + return raw.decode("utf-8") if isinstance(raw, (bytes, bytearray)) else str(raw) return body.decode("utf-8") if isinstance(body, (bytes, bytearray)) else str(body) - text_attr = getattr(request, "text", None) - if text_attr is not None: - if callable(text_attr): - result = text_attr() - return str(await result if inspect.isawaitable(result) else result) - return text_attr data = getattr(request, "data", None) if data is not None: - return data.decode("utf-8") if isinstance(data, bytes) else str(data) + return data.decode("utf-8") if isinstance(data, (bytes, bytearray)) else str(data) return "" def _get_header(self, request: Any, name: str) -> str | None: diff --git a/tests/test_request_body_extraction.py b/tests/test_request_body_extraction.py index f73b1a1..c8c0454 100644 --- a/tests/test_request_body_extraction.py +++ b/tests/test_request_body_extraction.py @@ -124,36 +124,26 @@ def __init__(self, body: bytearray) -> None: def _adapters() -> list[tuple[str, Any]]: """Return (name, extractor) pairs for each adapter's body extraction. - Lazy imports because some adapters require platform deps (cryptography, - pynacl). If a dep isn't installed, that adapter is skipped. + Imported directly: every adapter lazy-imports its platform deps inside + methods, so module-level import works in any environment the test runs + in. A broken adapter module fails the suite — matching the repo rule + "every test must fail when the code is wrong." """ - result: list[tuple[str, Any]] = [] - - # GitHub (static method) - try: - from chat_sdk.adapters.github.adapter import GitHubAdapter - - result.append(("github", GitHubAdapter._get_request_body)) - except ImportError: - pass # Optional platform dep missing — skip this adapter's cases. - - # Telegram (static method) - try: - from chat_sdk.adapters.telegram.adapter import TelegramAdapter - - result.append(("telegram", TelegramAdapter._get_request_body)) - except ImportError: - pass # Optional platform dep missing — skip this adapter's cases. - - # WhatsApp (static method) - try: - from chat_sdk.adapters.whatsapp.adapter import WhatsAppAdapter - - result.append(("whatsapp", WhatsAppAdapter._get_request_body)) - except ImportError: - pass # Optional platform dep missing — skip this adapter's cases. - - return result + from chat_sdk.adapters.discord.adapter import DiscordAdapter + from chat_sdk.adapters.github.adapter import GitHubAdapter + from chat_sdk.adapters.linear.adapter import LinearAdapter + from chat_sdk.adapters.teams.adapter import TeamsAdapter + from chat_sdk.adapters.telegram.adapter import TelegramAdapter + from chat_sdk.adapters.whatsapp.adapter import WhatsAppAdapter + + return [ + ("github", GitHubAdapter._get_request_body), + ("telegram", TelegramAdapter._get_request_body), + ("whatsapp", WhatsAppAdapter._get_request_body), + ("discord", DiscordAdapter._get_request_body), + ("linear", LinearAdapter._get_request_body), + ("teams", TeamsAdapter._get_request_body), + ] @pytest.mark.parametrize("name,extractor", _adapters()) @@ -207,6 +197,17 @@ async def test_handles_async_callable_body(name: str, extractor: Any) -> None: assert result == '{"ok": true}', f"{name} failed async body()" +@pytest.mark.parametrize("name,extractor", _adapters()) +async def test_handles_sync_callable_body(name: str, extractor: Any) -> None: + """Sync `def body(self)` returning bytes is consumed directly — + covers the callable-but-non-awaitable body branch. Without this, + a regression that always awaits `body()` would only show up in + async-framework use and silently pass the suite. + """ + result = await extractor(_SyncCallableBodyRequest(b'{"ok": true}')) + assert result == '{"ok": true}', f"{name} failed sync body()" + + @pytest.mark.parametrize("name,extractor", _adapters()) async def test_handles_bytes_body_attribute(name: str, extractor: Any) -> None: """`request.body` as a bytes property is decoded as UTF-8."""