refactor: replace try-except blocks with contextlib.suppress for cleaner error handling across multiple files

This commit is contained in:
Jack Lu
2026-05-01 19:30:11 +08:00
committed by Xubin Ren
parent 1c24f10236
commit d9800ecdd2
27 changed files with 98 additions and 195 deletions
+4 -9
View File
@@ -5,6 +5,7 @@ from __future__ import annotations
import asyncio
import importlib.util
import time
from contextlib import suppress
from dataclasses import dataclass
from pathlib import Path
from typing import TYPE_CHECKING, Any, Literal
@@ -564,10 +565,8 @@ class DiscordChannel(BaseChannel):
# Delayed working indicator (cosmetic — not tied to subagent lifecycle)
async def _delayed_working_emoji() -> None:
await asyncio.sleep(self.config.working_emoji_delay)
try:
with suppress(Exception):
await message.add_reaction(self.config.working_emoji)
except Exception:
pass
self._working_emoji_tasks[channel_id] = asyncio.create_task(_delayed_working_emoji())
@@ -771,10 +770,8 @@ class DiscordChannel(BaseChannel):
if task is None:
return
task.cancel()
try:
with suppress(asyncio.CancelledError):
await task
except asyncio.CancelledError:
pass
async def _clear_reactions(self, chat_id: str) -> None:
"""Remove all pending reactions after bot replies."""
@@ -788,10 +785,8 @@ class DiscordChannel(BaseChannel):
return
bot_user = self._client.user if self._client else None
for emoji in (self.config.read_receipt_emoji, self.config.working_emoji):
try:
with suppress(Exception):
await msg_obj.remove_reaction(emoji, bot_user)
except Exception:
pass
async def _cancel_all_typing(self) -> None:
"""Stop all typing tasks."""
+2 -3
View File
@@ -6,6 +6,7 @@ import imaplib
import re
import smtplib
import ssl
from contextlib import suppress
from datetime import date
from email import policy
from email.header import decode_header, make_header
@@ -460,10 +461,8 @@ class EmailChannel(BaseChannel):
if mark_seen:
client.store(imap_id, "+FLAGS", "\\Seen")
finally:
try:
with suppress(Exception):
client.logout()
except Exception:
pass
def _collect_self_addresses(self) -> set[str]:
"""Return normalized email addresses owned by this channel instance."""
+3 -3
View File
@@ -9,6 +9,7 @@ import threading
import time
import uuid
from collections import OrderedDict
from contextlib import suppress
from dataclasses import dataclass
from typing import Any, Literal
@@ -612,12 +613,11 @@ class FeishuChannel(BaseChannel):
"""Callback: store reaction_id after background add-reaction completes."""
if task.cancelled():
return
try:
# Failures already logged by _on_background_task_done.
with suppress(Exception):
reaction_id = task.result()
if reaction_id:
self._reaction_ids[message_id] = reaction_id
except Exception:
pass # already logged by _on_background_task_done
# Trim cache to prevent unbounded growth
if len(self._reaction_ids) > 500:
self._reaction_ids.pop(next(iter(self._reaction_ids)))
+2 -3
View File
@@ -3,6 +3,7 @@
from __future__ import annotations
import asyncio
from contextlib import suppress
from pathlib import Path
from typing import TYPE_CHECKING, Any
@@ -220,10 +221,8 @@ class ChannelManager:
# Stop dispatcher
if self._dispatch_task:
self._dispatch_task.cancel()
try:
with suppress(asyncio.CancelledError):
await self._dispatch_task
except asyncio.CancelledError:
pass
# Stop all channels
for name, channel in self.channels.items():
+5 -12
View File
@@ -5,6 +5,7 @@ import json
import logging
import mimetypes
import time
from contextlib import suppress
from dataclasses import dataclass
from pathlib import Path
from typing import Any, Literal, TypeAlias
@@ -341,10 +342,8 @@ class MatrixChannel(BaseChannel):
timeout=self.config.sync_stop_grace_seconds)
except (asyncio.TimeoutError, asyncio.CancelledError):
self._sync_task.cancel()
try:
with suppress(asyncio.CancelledError):
await self._sync_task
except asyncio.CancelledError:
pass
if self.client:
await self.client.close()
@@ -609,13 +608,11 @@ class MatrixChannel(BaseChannel):
"""Best-effort typing indicator update."""
if not self.client:
return
try:
with suppress(Exception):
response = await self.client.room_typing(room_id=room_id, typing_state=typing,
timeout=TYPING_NOTICE_TIMEOUT_MS)
if isinstance(response, RoomTypingError):
logger.debug("Matrix typing failed for {}: {}", room_id, response)
except Exception:
pass
async def _start_typing_keepalive(self, room_id: str) -> None:
"""Start periodic typing refresh (spec-recommended keepalive)."""
@@ -625,22 +622,18 @@ class MatrixChannel(BaseChannel):
return
async def loop() -> None:
try:
with suppress(asyncio.CancelledError):
while self._running:
await asyncio.sleep(TYPING_KEEPALIVE_INTERVAL_MS / 1000)
await self._set_typing(room_id, True)
except asyncio.CancelledError:
pass
self._typing_tasks[room_id] = asyncio.create_task(loop())
async def _stop_typing_keepalive(self, room_id: str, *, clear_typing: bool) -> None:
if task := self._typing_tasks.pop(room_id, None):
task.cancel()
try:
with suppress(asyncio.CancelledError):
await task
except asyncio.CancelledError:
pass
if clear_typing:
await self._set_typing(room_id, False)
+3 -6
View File
@@ -5,6 +5,7 @@ from __future__ import annotations
import asyncio
import json
from collections import deque
from contextlib import suppress
from dataclasses import dataclass, field
from datetime import datetime
from typing import Any
@@ -330,10 +331,8 @@ class MochatChannel(BaseChannel):
await self._cancel_delay_timers()
if self._socket:
try:
with suppress(Exception):
await self._socket.disconnect()
except Exception:
pass
self._socket = None
if self._cursor_save_task:
@@ -460,10 +459,8 @@ class MochatChannel(BaseChannel):
return True
except Exception as e:
logger.error("Failed to connect Mochat websocket: {}", e)
try:
with suppress(Exception):
await client.disconnect()
except Exception:
pass
self._socket = None
return False
+2 -4
View File
@@ -20,7 +20,7 @@ import re
import tempfile
import threading
import time
from contextlib import contextmanager
from contextlib import contextmanager, suppress
from dataclasses import dataclass
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from typing import TYPE_CHECKING, Any
@@ -712,10 +712,8 @@ class MSTeamsChannel(BaseChannel):
os.replace(tmp_path, path)
finally:
if tmp_path and os.path.exists(tmp_path):
try:
with suppress(OSError):
os.unlink(tmp_path)
except OSError:
pass
def _save_refs_locked(self, *, prune: bool = True) -> None:
"""Persist conversation references (caller must hold _refs_guard)."""
+4 -9
View File
@@ -25,6 +25,7 @@ import os
import re
import time
from collections import deque
from contextlib import suppress
from pathlib import Path
from typing import TYPE_CHECKING, Any, Literal
from urllib.parse import unquote, urlparse
@@ -221,17 +222,13 @@ class QQChannel(BaseChannel):
"""Stop bot and cleanup resources."""
self._running = False
if self._client:
try:
with suppress(Exception):
await self._client.close()
except Exception:
pass
self._client = None
if self._http:
try:
with suppress(Exception):
await self._http.close()
except Exception:
pass
self._http = None
logger.info("QQ bot stopped")
@@ -683,7 +680,5 @@ class QQChannel(BaseChannel):
finally:
# Cleanup partial file
if tmp_path is not None:
try:
with suppress(Exception):
tmp_path.unlink(missing_ok=True)
except Exception:
pass
+8 -14
View File
@@ -6,6 +6,7 @@ import asyncio
import re
import time
import unicodedata
from contextlib import suppress
from dataclasses import dataclass
from pathlib import Path
from typing import Any, Literal
@@ -462,10 +463,8 @@ class TelegramChannel(BaseChannel):
if not msg.metadata.get("_progress", False):
self._stop_typing(msg.chat_id)
if reply_to_message_id := msg.metadata.get("message_id"):
try:
with suppress(ValueError):
await self._remove_reaction(msg.chat_id, int(reply_to_message_id))
except ValueError:
pass
try:
chat_id = int(msg.chat_id)
@@ -642,10 +641,8 @@ class TelegramChannel(BaseChannel):
return
self._stop_typing(chat_id)
if reply_to_message_id := meta.get("message_id"):
try:
with suppress(ValueError):
await self._remove_reaction(chat_id, int(reply_to_message_id))
except ValueError:
pass
thread_kwargs = {}
if message_thread_id := meta.get("message_thread_id"):
thread_kwargs["message_thread_id"] = message_thread_id
@@ -1162,11 +1159,10 @@ class TelegramChannel(BaseChannel):
async def _typing_loop(self, chat_id: str) -> None:
"""Repeatedly send 'typing' action until cancelled."""
try:
while self._app:
await self._app.bot.send_chat_action(chat_id=int(chat_id), action="typing")
await asyncio.sleep(4)
except asyncio.CancelledError:
pass
with suppress(asyncio.CancelledError):
while self._app:
await self._app.bot.send_chat_action(chat_id=int(chat_id), action="typing")
await asyncio.sleep(4)
except Exception as e:
logger.debug("Typing indicator stopped for {}: {}", chat_id, e)
@@ -1265,10 +1261,8 @@ class TelegramChannel(BaseChannel):
button_label = query.data or ""
await query.answer()
if query.message:
try:
with suppress(Exception):
await query.message.edit_reply_markup(reply_markup=None)
except Exception:
pass
logger.debug("Inline button tap from {}: {}", sender_id, button_label)
self._start_typing(str(chat_id))
await self._handle_message(
+12 -33
View File
@@ -19,6 +19,7 @@ import re
import time
import uuid
from collections import OrderedDict
from contextlib import suppress
from pathlib import Path
from typing import Any
from urllib.parse import quote
@@ -211,7 +212,7 @@ class WeixinChannel(BaseChannel):
def _save_state(self) -> None:
state_file = self._get_state_dir() / "account.json"
try:
with suppress(Exception):
data = {
"token": self._token,
"get_updates_buf": self._get_updates_buf,
@@ -220,8 +221,6 @@ class WeixinChannel(BaseChannel):
"base_url": self.config.base_url,
}
state_file.write_text(json.dumps(data, ensure_ascii=False))
except Exception:
pass
# ------------------------------------------------------------------
# HTTP helpers (matches api.ts buildHeaders / apiFetch)
@@ -576,10 +575,8 @@ class WeixinChannel(BaseChannel):
# Process messages (WeixinMessage[] from types.ts)
msgs: list[dict] = data.get("msgs", []) or []
for msg in msgs:
try:
with suppress(Exception):
await self._process_message(msg)
except Exception:
pass
# ------------------------------------------------------------------
# Inbound message processing (matches inbound.ts + process-message.ts)
@@ -932,10 +929,8 @@ class WeixinChannel(BaseChannel):
await asyncio.sleep(TYPING_KEEPALIVE_INTERVAL_S)
if stop_event.is_set():
break
try:
with suppress(Exception):
await self._send_typing(user_id, typing_ticket, TYPING_STATUS_TYPING)
except Exception:
pass
finally:
pass
@@ -962,16 +957,12 @@ class WeixinChannel(BaseChannel):
return
typing_ticket = ""
try:
with suppress(Exception):
typing_ticket = await self._get_typing_ticket(msg.chat_id, ctx_token)
except Exception:
typing_ticket = ""
if typing_ticket:
try:
with suppress(Exception):
await self._send_typing(msg.chat_id, typing_ticket, TYPING_STATUS_TYPING)
except Exception:
pass
typing_keepalive_stop = asyncio.Event()
typing_keepalive_task: asyncio.Task | None = None
@@ -1043,16 +1034,12 @@ class WeixinChannel(BaseChannel):
if typing_keepalive_task:
typing_keepalive_stop.set()
typing_keepalive_task.cancel()
try:
with suppress(asyncio.CancelledError):
await typing_keepalive_task
except asyncio.CancelledError:
pass
if typing_ticket and not is_progress:
try:
with suppress(Exception):
await self._send_typing(msg.chat_id, typing_ticket, TYPING_STATUS_CANCEL)
except Exception:
pass
async def _start_typing(self, chat_id: str, context_token: str = "") -> None:
"""Start typing indicator immediately when a message is received."""
@@ -1076,10 +1063,8 @@ class WeixinChannel(BaseChannel):
await asyncio.sleep(TYPING_KEEPALIVE_INTERVAL_S)
if stop_event.is_set():
break
try:
with suppress(Exception):
await self._send_typing(chat_id, ticket, TYPING_STATUS_TYPING)
except Exception:
pass
finally:
pass
@@ -1095,10 +1080,8 @@ class WeixinChannel(BaseChannel):
if stop_event:
stop_event.set()
task.cancel()
try:
with suppress(asyncio.CancelledError):
await task
except asyncio.CancelledError:
pass
if not clear_remote:
return
entry = self._typing_tickets.get(chat_id)
@@ -1339,13 +1322,11 @@ def _encrypt_aes_ecb(data: bytes, aes_key_b64: str) -> bytes:
pad_len = 16 - len(data) % 16
padded = data + bytes([pad_len] * pad_len)
try:
with suppress(ImportError):
from Crypto.Cipher import AES
cipher = AES.new(key, AES.MODE_ECB)
return cipher.encrypt(padded)
except ImportError:
pass
try:
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
@@ -1371,13 +1352,11 @@ def _decrypt_aes_ecb(data: bytes, aes_key_b64: str) -> bytes:
decrypted: bytes | None = None
try:
with suppress(ImportError):
from Crypto.Cipher import AES
cipher = AES.new(key, AES.MODE_ECB)
decrypted = cipher.decrypt(data)
except ImportError:
pass
if decrypted is None:
try:
+2 -3
View File
@@ -8,6 +8,7 @@ import os
import secrets
import shutil
import subprocess
from contextlib import suppress
from collections import OrderedDict
from pathlib import Path
from typing import Any, Literal
@@ -47,10 +48,8 @@ def _load_or_create_bridge_token(path: Path) -> str:
path.parent.mkdir(parents=True, exist_ok=True)
token = secrets.token_urlsafe(32)
path.write_text(token, encoding="utf-8")
try:
with suppress(OSError):
path.chmod(0o600)
except OSError:
pass
return token