Skip to content

Commit 43c2cf0

Browse files
chore(types): reduce pyrefly errors from 213 → 130 (38% drop)
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
1 parent 6292436 commit 43c2cf0

13 files changed

Lines changed: 296 additions & 1240 deletions

File tree

.pyrefly-baseline.json

Lines changed: 169 additions & 1165 deletions
Large diffs are not rendered by default.

pyproject.toml

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -99,19 +99,26 @@ python-platform = "linux"
9999
# Optional adapter deps that aren't in the default install. Treating them as
100100
# Any keeps pyrefly from flagging missing-import every time we lazy-load one.
101101
replace-imports-with-any = [
102-
# Slack
102+
# Slack — covers top-level + submodule imports like slack_sdk.web.async_client
103103
"slack_sdk",
104-
# Discord
104+
"slack_sdk.*",
105+
# Discord — signed-request verification uses pynacl's nacl.signing
105106
"nacl",
106-
# Google Chat
107+
"nacl.*",
108+
# Google Chat — auth uses google.auth.* subpackages
107109
"google",
110+
"google.*",
108111
# State backends
109112
"redis",
113+
"redis.*",
110114
"asyncpg",
115+
"asyncpg.*",
111116
# HTTP clients used in lazy paths
112117
"httpx",
118+
"httpx.*",
113119
# GitHub App auth
114120
"jwt",
121+
"jwt.*",
115122
]
116123

117124
[tool.pyrefly.errors]

src/chat_sdk/adapters/discord/adapter.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
FetchResult,
5353
FileUpload,
5454
FormattedContent,
55+
LockScope,
5556
Message,
5657
MessageMetadata,
5758
RawMessage,
@@ -160,7 +161,7 @@ def bot_user_id(self) -> str | None:
160161
return self._bot_user_id
161162

162163
@property
163-
def lock_scope(self) -> str | None:
164+
def lock_scope(self) -> LockScope | None:
164165
return None
165166

166167
@property
@@ -953,7 +954,7 @@ async def remove_reaction(
953954
"DELETE",
954955
)
955956

956-
async def start_typing(self, thread_id: str, _status: str | None = None) -> None:
957+
async def start_typing(self, thread_id: str, status: str | None = None) -> None:
957958
"""Start typing indicator in a Discord channel or thread."""
958959
decoded = self.decode_thread_id(thread_id)
959960
target_channel_id = decoded.thread_id or decoded.channel_id

src/chat_sdk/adapters/github/adapter.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
import time
1818
from collections.abc import AsyncIterable
1919
from datetime import datetime, timezone
20-
from typing import Any, Literal
20+
from typing import Any, Literal, cast
2121

2222
from chat_sdk.adapters.github.cards import card_to_github_markdown
2323
from chat_sdk.adapters.github.format_converter import GitHubFormatConverter
@@ -31,6 +31,7 @@
3131
GitHubUser,
3232
IssueCommentWebhookPayload,
3333
PullRequestReviewCommentWebhookPayload,
34+
_GitHubAdapterConfigInternal,
3435
)
3536
from chat_sdk.emoji import convert_emoji_placeholders
3637
from chat_sdk.logger import ConsoleLogger, Logger
@@ -47,6 +48,7 @@
4748
FormattedContent,
4849
ListThreadsOptions,
4950
ListThreadsResult,
51+
LockScope,
5052
Message,
5153
MessageMetadata,
5254
RawMessage,
@@ -89,7 +91,10 @@ class GitHubAdapter:
8991
"""
9092

9193
def __init__(self, config: GitHubAdapterConfig | None = None) -> None:
92-
config = config or {}
94+
# The public API surface is the auth-mode-specific TypedDict union
95+
# above; internally we duck-type with `.get()` so narrow to the
96+
# superset TypedDict that admits every possible key.
97+
config = cast(_GitHubAdapterConfigInternal, config or {})
9398
self._name = "github"
9499

95100
webhook_secret = config.get("webhook_secret") or os.environ.get("GITHUB_WEBHOOK_SECRET")
@@ -177,7 +182,7 @@ def bot_user_id(self) -> str | None:
177182
return str(self._bot_user_id) if self._bot_user_id else None
178183

179184
@property
180-
def lock_scope(self) -> str | None:
185+
def lock_scope(self) -> LockScope | None:
181186
return None
182187

183188
@property

src/chat_sdk/adapters/github/types.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,23 @@ class GitHubAdapterAutoConfig(GitHubAdapterBaseConfig, total=False):
8080
"""Configuration with no auth fields - will auto-detect from env vars."""
8181

8282

83-
# Union of all configuration types
83+
class _GitHubAdapterConfigInternal(GitHubAdapterBaseConfig, total=False):
84+
"""Internal superset used for duck-typed `config.get(...)` auth detection.
85+
86+
The public API surface is the auth-mode-specific TypedDicts above — they
87+
document which fields are valid for each auth mode. Inside `__init__` we
88+
do mode detection via `.get("token")`, `.get("app_id")`, etc., which
89+
requires a type that admits every possible key. This type is never
90+
exposed to consumers.
91+
"""
92+
93+
token: str
94+
app_id: str
95+
installation_id: int
96+
private_key: str
97+
98+
99+
# Union of all public configuration types
84100
GitHubAdapterConfig = (
85101
GitHubAdapterPATConfig | GitHubAdapterAppConfig | GitHubAdapterMultiTenantAppConfig | GitHubAdapterAutoConfig
86102
)

src/chat_sdk/adapters/google_chat/adapter.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@
6969
FormattedContent,
7070
ListThreadsOptions,
7171
ListThreadsResult,
72+
LockScope,
7273
Message,
7374
RawMessage,
7475
ReactionEvent,
@@ -130,7 +131,7 @@ def __init__(self, config: GoogleChatAdapterConfig | None = None) -> None:
130131
config = GoogleChatAdapterConfig()
131132

132133
self._name = "gchat"
133-
self._lock_scope: str | None = None
134+
self._lock_scope: LockScope | None = None
134135
self._persist_message_history: bool | None = None
135136
self._logger: Logger = config.logger or ConsoleLogger("info").child("gchat")
136137
self._user_name = config.user_name or "bot"
@@ -211,7 +212,7 @@ def bot_user_id(self) -> str | None:
211212
return self._bot_user_id
212213

213214
@property
214-
def lock_scope(self) -> str | None:
215+
def lock_scope(self) -> LockScope | None:
215216
return self._lock_scope
216217

217218
@property

src/chat_sdk/adapters/linear/adapter.py

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
FetchOptions,
4848
FetchResult,
4949
FormattedContent,
50+
LockScope,
5051
Message,
5152
MessageMetadata,
5253
RawMessage,
@@ -169,7 +170,7 @@ def bot_user_id(self) -> str | None:
169170
return self._bot_user_id
170171

171172
@property
172-
def lock_scope(self) -> str | None:
173+
def lock_scope(self) -> LockScope | None:
173174
return None
174175

175176
@property
@@ -537,7 +538,7 @@ async def edit_message(
537538
),
538539
)
539540

540-
async def delete_message(self, _thread_id: str, message_id: str) -> None:
541+
async def delete_message(self, thread_id: str, message_id: str) -> None:
541542
"""Delete a message (delete a comment)."""
542543
await self._ensure_valid_token()
543544

@@ -554,7 +555,7 @@ async def delete_message(self, _thread_id: str, message_id: str) -> None:
554555

555556
async def add_reaction(
556557
self,
557-
_thread_id: str,
558+
thread_id: str,
558559
message_id: str,
559560
emoji: EmojiValue | str,
560561
) -> None:
@@ -575,14 +576,14 @@ async def add_reaction(
575576

576577
async def remove_reaction(
577578
self,
578-
_thread_id: str,
579-
_message_id: str,
580-
_emoji: EmojiValue | str,
579+
thread_id: str,
580+
message_id: str,
581+
emoji: EmojiValue | str,
581582
) -> None:
582583
"""Remove a reaction from a comment (limited support)."""
583584
self._logger.warn("removeReaction is not fully supported on Linear - reaction ID lookup would be required")
584585

585-
async def start_typing(self, _thread_id: str, _status: str | None = None) -> None:
586+
async def start_typing(self, thread_id: str, status: str | None = None) -> None:
586587
"""Start typing indicator. Not supported by Linear."""
587588
pass
588589

src/chat_sdk/adapters/slack/adapter.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@
7373
LinkPreview,
7474
ListThreadsOptions,
7575
ListThreadsResult,
76+
LockScope,
7677
MemberJoinedChannelEvent,
7778
Message,
7879
MessageMetadata,
@@ -203,7 +204,7 @@ def __init__(self, config: SlackAdapterConfig | None = None) -> None:
203204
self._bot_id: str | None = None # Bot app ID (B_xxx)
204205
self._chat: ChatInstance | None = None
205206
self._format_converter = SlackFormatConverter()
206-
self._lock_scope = "thread"
207+
self._lock_scope: LockScope = "thread"
207208
self._persist_message_history = False
208209

209210
# Channel external/shared cache
@@ -245,7 +246,7 @@ def bot_user_id(self) -> str | None:
245246
return self._bot_user_id
246247

247248
@property
248-
def lock_scope(self) -> str:
249+
def lock_scope(self) -> LockScope:
249250
return self._lock_scope
250251

251252
@property

src/chat_sdk/adapters/teams/adapter.py

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
FetchOptions,
4646
FetchResult,
4747
FormattedContent,
48+
LockScope,
4849
Message,
4950
MessageMetadata,
5051
RawMessage,
@@ -195,7 +196,7 @@ def bot_user_id(self) -> str | None:
195196
return self._bot_user_id
196197

197198
@property
198-
def lock_scope(self) -> str | None:
199+
def lock_scope(self) -> LockScope | None:
199200
return None
200201

201202
@property
@@ -767,23 +768,23 @@ async def delete_message(self, thread_id: str, message_id: str) -> None:
767768

768769
async def add_reaction(
769770
self,
770-
_thread_id: str,
771-
_message_id: str,
772-
_emoji: EmojiValue | str,
771+
thread_id: str,
772+
message_id: str,
773+
emoji: EmojiValue | str,
773774
) -> None:
774775
"""Add a reaction (not supported by Teams Bot Framework API)."""
775776
self._logger.warn("addReaction is not supported by the Teams Bot Framework API")
776777

777778
async def remove_reaction(
778779
self,
779-
_thread_id: str,
780-
_message_id: str,
781-
_emoji: EmojiValue | str,
780+
thread_id: str,
781+
message_id: str,
782+
emoji: EmojiValue | str,
782783
) -> None:
783784
"""Remove a reaction (not supported by Teams Bot Framework API)."""
784785
self._logger.warn("removeReaction is not supported by the Teams Bot Framework API")
785786

786-
async def start_typing(self, thread_id: str, _status: str | None = None) -> None:
787+
async def start_typing(self, thread_id: str, status: str | None = None) -> None:
787788
"""Send typing indicator to a Teams conversation."""
788789
decoded = self.decode_thread_id(thread_id)
789790

src/chat_sdk/adapters/telegram/adapter.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@
6868
FetchOptions,
6969
FetchResult,
7070
FormattedContent,
71+
LockScope,
7172
Message,
7273
MessageMetadata,
7374
RawMessage,
@@ -257,7 +258,7 @@ def __init__(self, config: TelegramAdapterConfig | None = None) -> None:
257258
)
258259

259260
self._name: str = "telegram"
260-
self._lock_scope: str = "channel"
261+
self._lock_scope: LockScope = "channel"
261262
self._persist_message_history: bool = True
262263

263264
self._bot_token: str = bot_token
@@ -300,7 +301,7 @@ def name(self) -> str:
300301
return self._name
301302

302303
@property
303-
def lock_scope(self) -> str:
304+
def lock_scope(self) -> LockScope:
304305
return self._lock_scope
305306

306307
@property
@@ -1018,7 +1019,7 @@ async def remove_reaction(
10181019
self,
10191020
thread_id: str,
10201021
message_id: str,
1021-
_emoji: EmojiValue | str,
1022+
emoji: EmojiValue | str,
10221023
) -> None:
10231024
"""Remove a reaction from a Telegram message."""
10241025
parsed_thread = self._resolve_thread_id(thread_id)

0 commit comments

Comments
 (0)