chore: ruff auto-fix PLR6201 — tuple → set in membership tests (#23937)

Replace  with  for all literal-tuple
membership tests. Set lookup is O(1) vs O(n) for tuple — consistent
micro-optimization across the codebase.

608 instances fixed via `ruff --fix --unsafe-fixes`, 0 remaining.
133 files, +626/-626 (net zero).
This commit is contained in:
kshitij 2026-05-11 11:13:25 -07:00 committed by GitHub
parent 8c11710314
commit 2ec8d2b42f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
133 changed files with 626 additions and 626 deletions

View file

@ -449,7 +449,7 @@ if AIOHTTP_AVAILABLE:
@web.middleware
async def body_limit_middleware(request, handler):
"""Reject overly large request bodies early based on Content-Length."""
if request.method in ("POST", "PUT", "PATCH"):
if request.method in {"POST", "PUT", "PATCH"}:
cl = request.headers.get("Content-Length")
if cl is not None:
try:
@ -646,7 +646,7 @@ class APIServerAdapter(BasePlatformAdapter):
try:
from hermes_cli.profiles import get_active_profile_name
profile = get_active_profile_name()
if profile and profile not in ("default", "custom"):
if profile and profile not in {"default", "custom"}:
return profile
except Exception:
pass
@ -1003,7 +1003,7 @@ class APIServerAdapter(BasePlatformAdapter):
system_prompt = content
else:
system_prompt = system_prompt + "\n" + content
elif role in ("user", "assistant"):
elif role in {"user", "assistant"}:
try:
content = _normalize_multimodal_content(raw_content)
except ValueError as exc:
@ -2381,7 +2381,7 @@ class APIServerAdapter(BasePlatformAdapter):
if cron_err:
return cron_err
try:
include_disabled = request.query.get("include_disabled", "").lower() in ("true", "1")
include_disabled = request.query.get("include_disabled", "").lower() in {"true", "1"}
jobs = _cron_list(include_disabled=include_disabled)
return web.json_response({"jobs": jobs})
except Exception as e:

View file

@ -560,7 +560,7 @@ def _looks_like_image(data: bytes) -> bool:
return True
if data[:3] == b"\xff\xd8\xff":
return True
if data[:6] in (b"GIF87a", b"GIF89a"):
if data[:6] in {b"GIF87a", b"GIF89a"}:
return True
if data[:2] == b"BM":
return True
@ -859,7 +859,7 @@ def cache_document_from_bytes(data: bytes, filename: str) -> str:
# Sanitize: strip directory components, null bytes, and control characters
safe_name = Path(filename).name if filename else "document"
safe_name = safe_name.replace("\x00", "").strip()
if not safe_name or safe_name in (".", ".."):
if not safe_name or safe_name in {".", ".."}:
safe_name = "document"
cached_name = f"doc_{uuid.uuid4().hex[:12]}_{safe_name}"
filepath = cache_dir / cached_name
@ -2793,7 +2793,7 @@ class BasePlatformAdapter(ABC):
# and preserve ordering of queued follow-ups. Route those
# through the dedicated handoff path that serializes
# cancellation + runner response + pending drain.
if cmd in ("stop", "new", "reset"):
if cmd in {"stop", "new", "reset"}:
try:
await self._dispatch_active_session_command(event, session_key, cmd)
except Exception as e:

View file

@ -223,7 +223,7 @@ class BlueBubblesAdapter(BasePlatformAdapter):
def _webhook_url(self) -> str:
"""Compute the external webhook URL for BlueBubbles registration."""
host = self.webhook_host
if host in ("0.0.0.0", "127.0.0.1", "localhost", "::"):
if host in {"0.0.0.0", "127.0.0.1", "localhost", "::"}:
host = "localhost"
return f"http://{host}:{self.webhook_port}{self.webhook_path}"

View file

@ -353,9 +353,9 @@ class DingTalkAdapter(BasePlatformAdapter):
configured = self.config.extra.get("require_mention")
if configured is not None:
if isinstance(configured, str):
return configured.lower() in ("true", "1", "yes", "on")
return configured.lower() in {"true", "1", "yes", "on"}
return bool(configured)
return os.getenv("DINGTALK_REQUIRE_MENTION", "false").lower() in ("true", "1", "yes", "on")
return os.getenv("DINGTALK_REQUIRE_MENTION", "false").lower() in {"true", "1", "yes", "on"}
def _dingtalk_free_response_chats(self) -> Set[str]:
raw = self.config.extra.get("free_response_chats")

View file

@ -115,7 +115,7 @@ def _build_allowed_mentions():
raw = os.getenv(name, "").strip().lower()
if not raw:
return default
return raw in ("true", "1", "yes", "on")
return raw in {"true", "1", "yes", "on"}
return discord.AllowedMentions(
everyone=_b("DISCORD_ALLOW_MENTION_EVERYONE", False),
@ -708,7 +708,7 @@ class DiscordAdapter(BasePlatformAdapter):
# Ignore Discord system messages (thread renames, pins, member joins, etc.)
# Allow both default and reply types — replies have a distinct MessageType.
if message.type not in (discord.MessageType.default, discord.MessageType.reply):
if message.type not in {discord.MessageType.default, discord.MessageType.reply}:
return
# Bot message filtering (DISCORD_ALLOW_BOTS):
@ -769,7 +769,7 @@ class DiscordAdapter(BasePlatformAdapter):
# answer regardless of who is mentioned.
_ignore_no_mention = os.getenv(
"DISCORD_IGNORE_NO_MENTION", "true"
).lower() in ("true", "1", "yes")
).lower() in {"true", "1", "yes"}
if _ignore_no_mention and not _self_mentioned and not _other_bots_mentioned:
_channel_id = str(message.channel.id)
_parent_id = None
@ -1317,7 +1317,7 @@ class DiscordAdapter(BasePlatformAdapter):
def _reactions_enabled(self) -> bool:
"""Check if message reactions are enabled via config/env."""
return os.getenv("DISCORD_REACTIONS", "true").lower() not in ("false", "0", "no")
return os.getenv("DISCORD_REACTIONS", "true").lower() not in {"false", "0", "no"}
async def on_processing_start(self, event: MessageEvent) -> None:
"""Add an in-progress reaction for normal Discord message events."""
@ -3137,9 +3137,9 @@ class DiscordAdapter(BasePlatformAdapter):
# UX so users don't see commands they can't invoke. Off by default
# to preserve the slash UX for deployments that intentionally allow
# everyone in the guild.
if os.getenv("DISCORD_HIDE_SLASH_COMMANDS", "false").strip().lower() in (
if os.getenv("DISCORD_HIDE_SLASH_COMMANDS", "false").strip().lower() in {
"true", "1", "yes", "on",
):
}:
self._apply_owner_only_visibility(tree)
def _apply_owner_only_visibility(self, tree) -> None:
@ -3526,9 +3526,9 @@ class DiscordAdapter(BasePlatformAdapter):
configured = self.config.extra.get("require_mention")
if configured is not None:
if isinstance(configured, str):
return configured.lower() not in ("false", "0", "no", "off")
return configured.lower() not in {"false", "0", "no", "off"}
return bool(configured)
return os.getenv("DISCORD_REQUIRE_MENTION", "true").lower() not in ("false", "0", "no", "off")
return os.getenv("DISCORD_REQUIRE_MENTION", "true").lower() not in {"false", "0", "no", "off"}
def _discord_free_response_channels(self) -> set:
"""Return Discord channel IDs where no bot mention is required.
@ -4200,7 +4200,7 @@ class DiscordAdapter(BasePlatformAdapter):
no_thread_channels_raw = os.getenv("DISCORD_NO_THREAD_CHANNELS", "")
no_thread_channels = {ch.strip() for ch in no_thread_channels_raw.split(",") if ch.strip()}
skip_thread = bool(channel_ids & no_thread_channels)
auto_thread = os.getenv("DISCORD_AUTO_THREAD", "true").lower() in ("true", "1", "yes")
auto_thread = os.getenv("DISCORD_AUTO_THREAD", "true").lower() in {"true", "1", "yes"}
is_reply_message = getattr(message, "type", None) == discord.MessageType.reply
if auto_thread and not skip_thread and not is_voice_linked_channel and not is_reply_message:
thread = await self._auto_create_thread(message)
@ -4282,7 +4282,7 @@ class DiscordAdapter(BasePlatformAdapter):
try:
# Determine extension from content type (image/png -> .png)
ext = "." + content_type.split("/")[-1].split(";")[0]
if ext not in (".jpg", ".jpeg", ".png", ".gif", ".webp"):
if ext not in {".jpg", ".jpeg", ".png", ".gif", ".webp"}:
ext = ".jpg"
cached_path = await self._cache_discord_image(att, ext)
media_urls.append(cached_path)
@ -4296,7 +4296,7 @@ class DiscordAdapter(BasePlatformAdapter):
elif content_type.startswith("audio/"):
try:
ext = "." + content_type.split("/")[-1].split(";")[0]
if ext not in (".ogg", ".mp3", ".wav", ".webm", ".m4a"):
if ext not in {".ogg", ".mp3", ".wav", ".webm", ".m4a"}:
ext = ".ogg"
cached_path = await self._cache_discord_audio(att, ext)
media_urls.append(cached_path)
@ -4339,7 +4339,7 @@ class DiscordAdapter(BasePlatformAdapter):
logger.info("[Discord] Cached user document: %s", cached_path)
# Inject text content for plain-text documents (capped at 100 KB)
MAX_TEXT_INJECT_BYTES = 100 * 1024
if ext in (".md", ".txt", ".log") and len(raw_bytes) <= MAX_TEXT_INJECT_BYTES:
if ext in {".md", ".txt", ".log"} and len(raw_bytes) <= MAX_TEXT_INJECT_BYTES:
try:
text_content = raw_bytes.decode("utf-8")
display_name = att.filename or f"document{ext}"

View file

@ -54,7 +54,7 @@ _NOREPLY_PATTERNS = (
# RFC headers that indicate bulk/automated mail
_AUTOMATED_HEADERS = {
"Auto-Submitted": lambda v: v.lower() != "no",
"Precedence": lambda v: v.lower() in ("bulk", "list", "junk"),
"Precedence": lambda v: v.lower() in {"bulk", "list", "junk"},
"X-Auto-Response-Suppress": lambda v: bool(v),
"List-Unsubscribe": lambda v: bool(v),
}
@ -203,7 +203,7 @@ def _extract_attachments(
continue
# Skip text/plain and text/html body parts
content_type = part.get_content_type()
if content_type in ("text/plain", "text/html") and "attachment" not in disposition:
if content_type in {"text/plain", "text/html"} and "attachment" not in disposition:
continue
filename = part.get_filename()

View file

@ -428,7 +428,7 @@ RejectReason = Literal[
def _is_bot_sender(sender: Any) -> bool:
# receive_v1 docs say {user, bot}; accept "app" defensively.
return getattr(sender, "sender_type", "") in ("bot", "app")
return getattr(sender, "sender_type", "") in {"bot", "app"}
def _sender_identity(sender: Any) -> frozenset:
@ -1443,7 +1443,7 @@ class FeishuAdapter(BasePlatformAdapter):
# Env-only so adapter and gateway auth bypass share one source; yaml
# feishu.allow_bots is bridged to this env var at config load.
allow_bots = os.getenv("FEISHU_ALLOW_BOTS", "none").strip().lower()
if allow_bots not in ("none", "mentions", "all"):
if allow_bots not in {"none", "mentions", "all"}:
logger.warning(
"[Feishu] Unknown allow_bots=%r, falling back to 'none'. Valid: none, mentions, all.",
allow_bots,
@ -2752,7 +2752,7 @@ class FeishuAdapter(BasePlatformAdapter):
# =========================================================================
def _reactions_enabled(self) -> bool:
return os.getenv("FEISHU_REACTIONS", "true").strip().lower() not in ("false", "0", "no")
return os.getenv("FEISHU_REACTIONS", "true").strip().lower() not in {"false", "0", "no"}
async def _add_reaction(self, message_id: str, emoji_type: str) -> Optional[str]:
"""Return the reaction_id on success, else None. The id is needed later for deletion."""
@ -3219,7 +3219,7 @@ class FeishuAdapter(BasePlatformAdapter):
self._on_bot_added_to_chat(data)
elif event_type == "im.chat.member.bot.deleted_v1":
self._on_bot_removed_from_chat(data)
elif event_type in ("im.message.reaction.created_v1", "im.message.reaction.deleted_v1"):
elif event_type in {"im.message.reaction.created_v1", "im.message.reaction.deleted_v1"}:
self._on_reaction_event(event_type, data)
elif event_type == "card.action.trigger":
self._on_card_action_trigger(data)
@ -4815,7 +4815,7 @@ def _poll_registration(
# Terminal errors
error = res.get("error", "")
if error in ("access_denied", "expired_token"):
if error in {"access_denied", "expired_token"}:
if poll_count > 0:
print()
logger.warning("[Feishu onboard] Registration %s", error)

View file

@ -690,7 +690,7 @@ def _extract_docs_links(replies: List[Dict[str, Any]]) -> List[Dict[str, str]]:
except (json.JSONDecodeError, TypeError):
continue
for elem in content.get("elements", []):
if elem.get("type") not in ("docs_link", "link"):
if elem.get("type") not in {"docs_link", "link"}:
continue
link_data = elem.get("docs_link") or elem.get("link") or {}
url = link_data.get("url", "")
@ -1031,7 +1031,7 @@ def _save_session_history(key: str, messages: List[Dict[str, Any]]) -> None:
# Only keep user/assistant messages (strip system messages and tool internals)
cleaned = [
m for m in messages
if m.get("role") in ("user", "assistant") and m.get("content")
if m.get("role") in {"user", "assistant"} and m.get("content")
]
# Keep last N
if len(cleaned) > _SESSION_MAX_MESSAGES:
@ -1170,7 +1170,7 @@ async def handle_drive_comment_event(
rule = resolve_rule(comments_cfg, file_type, file_token)
# If no exact match and config has wiki keys, try reverse-lookup
if rule.match_source in ("wildcard", "top") and has_wiki_keys(comments_cfg):
if rule.match_source in {"wildcard", "top"} and has_wiki_keys(comments_cfg):
wiki_token = await _reverse_lookup_wiki_token(client, file_type, file_token)
if wiki_token:
rule = resolve_rule(comments_cfg, file_type, file_token, wiki_token=wiki_token)

View file

@ -256,7 +256,7 @@ class HomeAssistantAdapter(BasePlatformAdapter):
await self._handle_ha_event(data.get("event", {}))
except json.JSONDecodeError:
logger.debug("Invalid JSON from HA WS: %s", ws_msg.data[:200])
elif ws_msg.type in (aiohttp.WSMsgType.CLOSED, aiohttp.WSMsgType.ERROR):
elif ws_msg.type in {aiohttp.WSMsgType.CLOSED, aiohttp.WSMsgType.ERROR}:
break
async def _handle_ha_event(self, event: Dict[str, Any]) -> None:
@ -361,7 +361,7 @@ class HomeAssistantAdapter(BasePlatformAdapter):
f"(was {'triggered' if old_val == 'on' else 'cleared'})"
)
if domain in ("light", "switch", "fan"):
if domain in {"light", "switch", "fan"}:
return (
f"[Home Assistant] {friendly_name}: turned "
f"{'on' if new_val == 'on' else 'off'}"

View file

@ -245,11 +245,11 @@ def check_matrix_requirements() -> bool:
# If encryption is requested, verify E2EE deps are available at startup
# rather than silently degrading to plaintext-only at connect time.
encryption_requested = os.getenv("MATRIX_ENCRYPTION", "").lower() in (
encryption_requested = os.getenv("MATRIX_ENCRYPTION", "").lower() in {
"true",
"1",
"yes",
)
}
if encryption_requested and not _check_e2ee_deps():
logger.error(
"Matrix: MATRIX_ENCRYPTION=true but E2EE dependencies are missing. %s. "
@ -312,7 +312,7 @@ class MatrixAdapter(BasePlatformAdapter):
)
self._encryption: bool = config.extra.get(
"encryption",
os.getenv("MATRIX_ENCRYPTION", "").lower() in ("true", "1", "yes"),
os.getenv("MATRIX_ENCRYPTION", "").lower() in {"true", "1", "yes"},
)
self._device_id: str = config.extra.get("device_id", "") or os.getenv(
"MATRIX_DEVICE_ID", ""
@ -343,7 +343,7 @@ class MatrixAdapter(BasePlatformAdapter):
# Mention/thread gating — parsed once from env vars.
self._require_mention: bool = os.getenv(
"MATRIX_REQUIRE_MENTION", "true"
).lower() not in ("false", "0", "no")
).lower() not in {"false", "0", "no"}
free_rooms_raw = config.extra.get("free_response_rooms")
if free_rooms_raw is None:
free_rooms_raw = os.getenv("MATRIX_FREE_RESPONSE_ROOMS", "")
@ -367,22 +367,22 @@ class MatrixAdapter(BasePlatformAdapter):
self._allowed_rooms: Set[str] = {
r.strip() for r in str(allowed_rooms_raw).split(",") if r.strip()
}
self._auto_thread: bool = os.getenv("MATRIX_AUTO_THREAD", "true").lower() in (
self._auto_thread: bool = os.getenv("MATRIX_AUTO_THREAD", "true").lower() in {
"true",
"1",
"yes",
)
}
self._dm_auto_thread: bool = os.getenv(
"MATRIX_DM_AUTO_THREAD", "false"
).lower() in ("true", "1", "yes")
).lower() in {"true", "1", "yes"}
self._dm_mention_threads: bool = os.getenv(
"MATRIX_DM_MENTION_THREADS", "false"
).lower() in ("true", "1", "yes")
).lower() in {"true", "1", "yes"}
# Reactions: configurable via MATRIX_REACTIONS (default: true).
self._reactions_enabled: bool = os.getenv(
"MATRIX_REACTIONS", "true"
).lower() not in ("false", "0", "no")
).lower() not in {"false", "0", "no"}
self._pending_reactions: dict[tuple[str, str], str] = {}
# Delay before redacting reactions so Matrix homeservers have time to
# deliver the final message event without tripping "missing event"
@ -1771,9 +1771,9 @@ class MatrixAdapter(BasePlatformAdapter):
# Cache media locally when downstream tools need a real file path.
cached_path = None
should_cache_locally = msg_type in (
should_cache_locally = msg_type in {
MessageType.PHOTO, MessageType.AUDIO, MessageType.VIDEO, MessageType.DOCUMENT,
) or is_voice_message or is_encrypted_media
} or is_voice_message or is_encrypted_media
if should_cache_locally and url:
try:
file_bytes = await self._client.download_media(ContentURI(url))
@ -1834,7 +1834,7 @@ class MatrixAdapter(BasePlatformAdapter):
ext = ext_map.get(media_type, ".jpg")
cached_path = cache_image_from_bytes(file_bytes, ext=ext)
logger.info("[Matrix] Cached user image at %s", cached_path)
elif msg_type in (MessageType.AUDIO, MessageType.VOICE):
elif msg_type in {MessageType.AUDIO, MessageType.VOICE}:
ext = (
Path(
body
@ -2602,7 +2602,7 @@ class MatrixAdapter(BasePlatformAdapter):
"""Sanitize a URL for use in an href attribute."""
stripped = url.strip()
scheme = stripped.split(":", 1)[0].lower().strip() if ":" in stripped else ""
if scheme in ("javascript", "data", "vbscript"):
if scheme in {"javascript", "data", "vbscript"}:
return ""
return stripped.replace('"', "&quot;")

View file

@ -611,7 +611,7 @@ class MattermostAdapter(BasePlatformAdapter):
# succeed on retry — stop reconnecting instead of looping forever.
import aiohttp
err_str = str(exc).lower()
if isinstance(exc, aiohttp.WSServerHandshakeError) and exc.status in (401, 403):
if isinstance(exc, aiohttp.WSServerHandshakeError) and exc.status in {401, 403}:
logger.error("Mattermost WS auth failed (HTTP %d) — stopping reconnect", exc.status)
return
if "401" in err_str or "403" in err_str or "unauthorized" in err_str:
@ -649,21 +649,21 @@ class MattermostAdapter(BasePlatformAdapter):
if self._closing:
return
if raw_msg.type in (
if raw_msg.type in {
raw_msg.type.TEXT,
raw_msg.type.BINARY,
):
}:
try:
event = json.loads(raw_msg.data)
except (json.JSONDecodeError, TypeError):
continue
await self._handle_ws_event(event)
elif raw_msg.type in (
elif raw_msg.type in {
raw_msg.type.ERROR,
raw_msg.type.CLOSE,
raw_msg.type.CLOSING,
raw_msg.type.CLOSED,
):
}:
logger.info("Mattermost: WebSocket closed (%s)", raw_msg.type)
break
@ -732,7 +732,7 @@ class MattermostAdapter(BasePlatformAdapter):
require_mention = os.getenv(
"MATTERMOST_REQUIRE_MENTION", "true"
).lower() not in ("false", "0", "no")
).lower() not in {"false", "0", "no"}
free_channels_raw = os.getenv("MATTERMOST_FREE_RESPONSE_CHANNELS", "")
free_channels = {ch.strip() for ch in free_channels_raw.split(",") if ch.strip()}

View file

@ -513,7 +513,7 @@ class QQAdapter(BasePlatformAdapter):
self._fail_pending("Connection closed")
# Stop reconnecting for fatal codes
if code in (4914, 4915):
if code in {4914, 4915}:
desc = "offline/sandbox-only" if code == 4914 else "banned"
logger.error(
"[%s] Bot is %s. Check QQ Open Platform.", self._log_tag, desc
@ -550,7 +550,7 @@ class QQAdapter(BasePlatformAdapter):
self._token_expires_at = 0.0
# Session invalid → clear session, will re-identify on next Hello
if code in (
if code in {
4006,
4007,
4009,
@ -568,7 +568,7 @@ class QQAdapter(BasePlatformAdapter):
4911,
4912,
4913,
):
}:
logger.info(
"[%s] Session error (%d), clearing session for re-identify",
self._log_tag,
@ -637,12 +637,12 @@ class QQAdapter(BasePlatformAdapter):
payload = self._parse_json(msg.data)
if payload:
self._dispatch_payload(payload)
elif msg.type in (aiohttp.WSMsgType.PING,):
elif msg.type in {aiohttp.WSMsgType.PING,}:
# aiohttp auto-replies with PONG
pass
elif msg.type == aiohttp.WSMsgType.CLOSE:
raise QQCloseError(msg.data, msg.extra)
elif msg.type in (aiohttp.WSMsgType.CLOSED, aiohttp.WSMsgType.ERROR):
elif msg.type in {aiohttp.WSMsgType.CLOSED, aiohttp.WSMsgType.ERROR}:
raise RuntimeError("WebSocket closed")
async def _heartbeat_loop(self) -> None:
@ -783,13 +783,13 @@ class QQAdapter(BasePlatformAdapter):
self._handle_ready(d)
elif t == "RESUMED":
logger.info("[%s] Session resumed", self._log_tag)
elif t in (
elif t in {
"C2C_MESSAGE_CREATE",
"GROUP_AT_MESSAGE_CREATE",
"DIRECT_MESSAGE_CREATE",
"GUILD_MESSAGE_CREATE",
"GUILD_AT_MESSAGE_CREATE",
):
}:
asyncio.create_task(self._on_message(t, d))
elif t == "INTERACTION_CREATE":
self._create_task(self._on_interaction(d))
@ -859,9 +859,9 @@ class QQAdapter(BasePlatformAdapter):
# Route by event type
if event_type == "C2C_MESSAGE_CREATE":
await self._handle_c2c_message(d, msg_id, content, author, timestamp)
elif event_type in ("GROUP_AT_MESSAGE_CREATE",):
elif event_type in {"GROUP_AT_MESSAGE_CREATE",}:
await self._handle_group_message(d, msg_id, content, author, timestamp)
elif event_type in ("GUILD_MESSAGE_CREATE", "GUILD_AT_MESSAGE_CREATE"):
elif event_type in {"GUILD_MESSAGE_CREATE", "GUILD_AT_MESSAGE_CREATE"}:
await self._handle_guild_message(d, msg_id, content, author, timestamp)
elif event_type == "DIRECT_MESSAGE_CREATE":
await self._handle_dm_message(d, msg_id, content, author, timestamp)
@ -1864,7 +1864,7 @@ class QQAdapter(BasePlatformAdapter):
return ".wav"
if data[:4] == b"fLaC":
return ".flac"
if data[:2] in (b"\xff\xfb", b"\xff\xf3", b"\xff\xf2"):
if data[:2] in {b"\xff\xfb", b"\xff\xf3", b"\xff\xf2"}:
return ".mp3"
if data[:4] == b"\x30\x26\xb2\x75" or data[:4] == b"\x4f\x67\x67\x53":
return ".ogg"
@ -2033,7 +2033,7 @@ class QQAdapter(BasePlatformAdapter):
"base_url": base_url,
"api_key": api_key,
"model": model
or ("glm-asr" if provider in ("zai", "glm") else "whisper-1"),
or ("glm-asr" if provider in {"zai", "glm"} else "whisper-1"),
}
# 2. QQ-specific env vars (set by `hermes setup gateway` / `hermes gateway`)
@ -2115,7 +2115,7 @@ class QQAdapter(BasePlatformAdapter):
if urlparse(source_url).path
else ""
)
if not ext or ext not in (
if not ext or ext not in {
".silk",
".amr",
".mp3",
@ -2124,7 +2124,7 @@ class QQAdapter(BasePlatformAdapter):
".m4a",
".aac",
".flac",
):
}:
ext = self._guess_ext_from_data(audio_data)
with tempfile.NamedTemporaryFile(suffix=ext, delete=False) as tmp_src:
@ -2870,7 +2870,7 @@ class QQAdapter(BasePlatformAdapter):
raise ValueError("Media source is required")
parsed = urlparse(source)
if parsed.scheme in ("http", "https"):
if parsed.scheme in {"http", "https"}:
# For URLs, pass through directly to the upload API
content_type = mimetypes.guess_type(source)[0] or "application/octet-stream"
resolved_name = file_name or Path(parsed.path).name or "media"
@ -2966,7 +2966,7 @@ class QQAdapter(BasePlatformAdapter):
chat_type = self._guess_chat_type(chat_id)
return {
"name": chat_id,
"type": "group" if chat_type in ("group", "guild") else "dm",
"type": "group" if chat_type in {"group", "guild"} else "dm",
}
# ------------------------------------------------------------------
@ -2975,7 +2975,7 @@ class QQAdapter(BasePlatformAdapter):
@staticmethod
def _is_url(source: str) -> bool:
return urlparse(str(source)).scheme in ("http", "https")
return urlparse(str(source)).scheme in {"http", "https"}
def _guess_chat_type(self, chat_id: str) -> str:
"""Determine chat type from stored inbound metadata, fallback to 'c2c'."""

View file

@ -239,7 +239,7 @@ class ChunkedUploader:
:raises UploadFileTooLargeError: When the file exceeds the platform limit.
:raises RuntimeError: On other API or I/O failures.
"""
if chat_type not in ("c2c", "group"):
if chat_type not in {"c2c", "group"}:
raise ValueError(
f"ChunkedUploader: unsupported chat_type {chat_type!r}"
)

View file

@ -99,11 +99,11 @@ def _guess_extension(data: bytes) -> str:
def _is_image_ext(ext: str) -> bool:
return ext.lower() in (".jpg", ".jpeg", ".png", ".gif", ".webp")
return ext.lower() in {".jpg", ".jpeg", ".png", ".gif", ".webp"}
def _is_audio_ext(ext: str) -> bool:
return ext.lower() in (".mp3", ".wav", ".ogg", ".m4a", ".aac")
return ext.lower() in {".mp3", ".wav", ".ogg", ".m4a", ".aac"}
_EXT_TO_MIME = {
@ -1449,7 +1449,7 @@ class SignalAdapter(BasePlatformAdapter):
contacts from seeing the 👀 reaction (which fires before run.py's
auth gate and would otherwise reveal that a bot is listening).
"""
if os.getenv("SIGNAL_REACTIONS", "true").lower() in ("false", "0", "no"):
if os.getenv("SIGNAL_REACTIONS", "true").lower() in {"false", "0", "no"}:
return False
if event is not None:
sender = getattr(getattr(event, "source", None), "user_id", None)

View file

@ -935,7 +935,7 @@ class SlackAdapter(BasePlatformAdapter):
raw = self.config.extra.get("dm_top_level_threads_as_sessions")
if raw is None:
return True # default: each DM thread is its own session
return str(raw).strip().lower() in ("1", "true", "yes", "on")
return str(raw).strip().lower() in {"1", "true", "yes", "on"}
def _resolve_thread_ts(
self,
@ -1300,7 +1300,7 @@ class SlackAdapter(BasePlatformAdapter):
def _reactions_enabled(self) -> bool:
"""Check if message reactions are enabled via config/env."""
return os.getenv("SLACK_REACTIONS", "true").lower() not in ("false", "0", "no")
return os.getenv("SLACK_REACTIONS", "true").lower() not in {"false", "0", "no"}
async def on_processing_start(self, event: MessageEvent) -> None:
"""Add an in-progress reaction when message processing begins."""
@ -1773,7 +1773,7 @@ class SlackAdapter(BasePlatformAdapter):
# Ignore message edits and deletions
subtype = event.get("subtype")
if subtype in ("message_changed", "message_deleted"):
if subtype in {"message_changed", "message_deleted"}:
return
original_text = event.get("text", "")
@ -1892,7 +1892,7 @@ class SlackAdapter(BasePlatformAdapter):
channel_type = event.get("channel_type", "")
if not channel_type and channel_id.startswith("D"):
channel_type = "im"
is_dm = channel_type in ("im", "mpim") # Both 1:1 and group DMs
is_dm = channel_type in {"im", "mpim"} # Both 1:1 and group DMs
# Build thread_ts for session keying.
# In channels: fall back to ts so each top-level @mention starts a
@ -2033,7 +2033,7 @@ class SlackAdapter(BasePlatformAdapter):
if mimetype.startswith("image/") and url:
try:
ext = "." + mimetype.split("/")[-1].split(";")[0]
if ext not in (".jpg", ".jpeg", ".png", ".gif", ".webp"):
if ext not in {".jpg", ".jpeg", ".png", ".gif", ".webp"}:
ext = ".jpg"
# Slack private URLs require the bot token as auth header
cached = await self._download_slack_file(url, ext, team_id=team_id)
@ -2049,7 +2049,7 @@ class SlackAdapter(BasePlatformAdapter):
elif mimetype.startswith("audio/") and url:
try:
ext = "." + mimetype.split("/")[-1].split(";")[0]
if ext not in (".ogg", ".mp3", ".wav", ".webm", ".m4a"):
if ext not in {".ogg", ".mp3", ".wav", ".webm", ".m4a"}:
ext = ".ogg"
cached = await self._download_slack_file(url, ext, audio=True, team_id=team_id)
media_urls.append(cached)
@ -2737,7 +2737,7 @@ class SlackAdapter(BasePlatformAdapter):
if team_id and channel_id:
self._channel_team[channel_id] = team_id
if slash_name in ("hermes", ""):
if slash_name in {"hermes", ""}:
# Legacy /hermes <subcommand> [args] routing + free-form questions.
# Empty slash_name falls into this branch for backward compat
# with any caller that didn't populate command["command"].
@ -2932,9 +2932,9 @@ class SlackAdapter(BasePlatformAdapter):
configured = self.config.extra.get("require_mention")
if configured is not None:
if isinstance(configured, str):
return configured.lower() not in ("false", "0", "no", "off")
return configured.lower() not in {"false", "0", "no", "off"}
return bool(configured)
return os.getenv("SLACK_REQUIRE_MENTION", "true").lower() not in ("false", "0", "no", "off")
return os.getenv("SLACK_REQUIRE_MENTION", "true").lower() not in {"false", "0", "no", "off"}
def _slack_strict_mention(self) -> bool:
"""When true, channel threads require an explicit @-mention on every
@ -2944,9 +2944,9 @@ class SlackAdapter(BasePlatformAdapter):
configured = self.config.extra.get("strict_mention")
if configured is not None:
if isinstance(configured, str):
return configured.lower() in ("true", "1", "yes", "on")
return configured.lower() in {"true", "1", "yes", "on"}
return bool(configured)
return os.getenv("SLACK_STRICT_MENTION", "false").lower() in ("true", "1", "yes", "on")
return os.getenv("SLACK_STRICT_MENTION", "false").lower() in {"true", "1", "yes", "on"}
def _slack_free_response_channels(self) -> set:
"""Return channel IDs where no @mention is required."""

View file

@ -616,7 +616,7 @@ class TelegramAdapter(BasePlatformAdapter):
def _looks_like_network_error(error: Exception) -> bool:
"""Return True for transient network errors that warrant a reconnect attempt."""
name = error.__class__.__name__.lower()
if name in ("networkerror", "timedout", "connectionerror"):
if name in {"networkerror", "timedout", "connectionerror"}:
return True
try:
from telegram.error import NetworkError, TimedOut
@ -632,9 +632,9 @@ class TelegramAdapter(BasePlatformAdapter):
return default
if isinstance(value, str):
lowered = value.strip().lower()
if lowered in ("true", "1", "yes", "on"):
if lowered in {"true", "1", "yes", "on"}:
return True
if lowered in ("false", "0", "no", "off"):
if lowered in {"false", "0", "no", "off"}:
return False
return default
return bool(value)
@ -1171,7 +1171,7 @@ class TelegramAdapter(BasePlatformAdapter):
"write_timeout": _env_float("HERMES_TELEGRAM_HTTP_WRITE_TIMEOUT", 20.0),
}
disable_fallback = (os.getenv("HERMES_TELEGRAM_DISABLE_FALLBACK_IPS", "").strip().lower() in ("1", "true", "yes", "on"))
disable_fallback = (os.getenv("HERMES_TELEGRAM_DISABLE_FALLBACK_IPS", "").strip().lower() in {"1", "true", "yes", "on"})
fallback_ips = self._fallback_ips()
if not fallback_ips:
fallback_ips = await discover_fallback_ips()
@ -1917,7 +1917,7 @@ class TelegramAdapter(BasePlatformAdapter):
"""
if not self._bot or not hasattr(self._bot, "send_message_draft"):
return False
return (chat_type or "").lower() in ("dm", "private")
return (chat_type or "").lower() in {"dm", "private"}
async def send_draft(
self,
@ -2723,7 +2723,7 @@ class TelegramAdapter(BasePlatformAdapter):
with open(audio_path, "rb") as audio_file:
ext = os.path.splitext(audio_path)[1].lower()
# .ogg / .opus files -> send as voice (round playable bubble)
if ext in (".ogg", ".opus"):
if ext in {".ogg", ".opus"}:
_voice_thread = self._metadata_thread_id(metadata)
reply_to_id = self._reply_to_message_id_for_send(reply_to, metadata)
voice_thread_kwargs = self._thread_kwargs_for_send(
@ -2747,7 +2747,7 @@ class TelegramAdapter(BasePlatformAdapter):
"voice",
reset_media=lambda: audio_file.seek(0),
)
elif ext in (".mp3", ".m4a"):
elif ext in {".mp3", ".m4a"}:
# Telegram's Bot API sendAudio only accepts MP3 / M4A.
_audio_thread = self._metadata_thread_id(metadata)
reply_to_id = self._reply_to_message_id_for_send(reply_to, metadata)
@ -3498,18 +3498,18 @@ class TelegramAdapter(BasePlatformAdapter):
configured = self.config.extra.get("require_mention")
if configured is not None:
if isinstance(configured, str):
return configured.lower() in ("true", "1", "yes", "on")
return configured.lower() in {"true", "1", "yes", "on"}
return bool(configured)
return os.getenv("TELEGRAM_REQUIRE_MENTION", "false").lower() in ("true", "1", "yes", "on")
return os.getenv("TELEGRAM_REQUIRE_MENTION", "false").lower() in {"true", "1", "yes", "on"}
def _telegram_guest_mode(self) -> bool:
"""Return whether non-allowlisted groups may trigger via direct @mention."""
configured = self.config.extra.get("guest_mode")
if configured is not None:
if isinstance(configured, str):
return configured.lower() in ("true", "1", "yes", "on")
return configured.lower() in {"true", "1", "yes", "on"}
return bool(configured)
return os.getenv("TELEGRAM_GUEST_MODE", "false").lower() in ("true", "1", "yes", "on")
return os.getenv("TELEGRAM_GUEST_MODE", "false").lower() in {"true", "1", "yes", "on"}
def _telegram_free_response_chats(self) -> set[str]:
raw = self.config.extra.get("free_response_chats")
@ -3598,7 +3598,7 @@ class TelegramAdapter(BasePlatformAdapter):
if not chat:
return False
chat_type = str(getattr(chat, "type", "")).split(".")[-1].lower()
return chat_type in ("group", "supergroup")
return chat_type in {"group", "supergroup"}
def _is_reply_to_bot(self, message: Message) -> bool:
if not self._bot or not getattr(message, "reply_to_message", None):
@ -4157,7 +4157,7 @@ class TelegramAdapter(BasePlatformAdapter):
# For text files, inject content into event.text (capped at 100 KB)
MAX_TEXT_INJECT_BYTES = 100 * 1024
if ext in (".md", ".txt") and len(raw_bytes) <= MAX_TEXT_INJECT_BYTES:
if ext in {".md", ".txt"} and len(raw_bytes) <= MAX_TEXT_INJECT_BYTES:
try:
text_content = raw_bytes.decode("utf-8")
display_name = original_filename or f"document{ext}"
@ -4396,7 +4396,7 @@ class TelegramAdapter(BasePlatformAdapter):
# Determine chat type
chat_type = "dm"
if chat.type in (ChatType.GROUP, ChatType.SUPERGROUP):
if chat.type in {ChatType.GROUP, ChatType.SUPERGROUP}:
chat_type = "group"
elif chat.type == ChatType.CHANNEL:
chat_type = "channel"
@ -4512,7 +4512,7 @@ class TelegramAdapter(BasePlatformAdapter):
def _reactions_enabled(self) -> bool:
"""Check if message reactions are enabled via config/env."""
return os.getenv("TELEGRAM_REACTIONS", "false").lower() not in ("false", "0", "no")
return os.getenv("TELEGRAM_REACTIONS", "false").lower() not in {"false", "0", "no"}
async def _set_reaction(self, chat_id: str, message_id: str, emoji: str) -> bool:
"""Set a single emoji reaction on a Telegram message."""

View file

@ -295,7 +295,7 @@ class WeComAdapter(BasePlatformAdapter):
auth_payload = await self._wait_for_handshake(req_id)
errcode = auth_payload.get("errcode", 0)
if errcode not in (0, None):
if errcode not in {0, None}:
errmsg = auth_payload.get("errmsg", "authentication failed")
raise RuntimeError(f"{errmsg} (errcode={errcode})")
@ -320,7 +320,7 @@ class WeComAdapter(BasePlatformAdapter):
if self._payload_req_id(payload) == req_id:
return payload
logger.debug("[%s] Ignoring pre-auth payload: %s", self.name, payload.get("cmd"))
elif msg.type in (aiohttp.WSMsgType.CLOSED, aiohttp.WSMsgType.CLOSE, aiohttp.WSMsgType.ERROR):
elif msg.type in {aiohttp.WSMsgType.CLOSED, aiohttp.WSMsgType.CLOSE, aiohttp.WSMsgType.ERROR}:
raise RuntimeError("WeCom websocket closed during authentication")
async def _listen_loop(self) -> None:
@ -360,7 +360,7 @@ class WeComAdapter(BasePlatformAdapter):
payload = self._parse_json(msg.data)
if payload:
await self._dispatch_payload(payload)
elif msg.type in (aiohttp.WSMsgType.CLOSE, aiohttp.WSMsgType.CLOSED, aiohttp.WSMsgType.ERROR):
elif msg.type in {aiohttp.WSMsgType.CLOSE, aiohttp.WSMsgType.CLOSED, aiohttp.WSMsgType.ERROR}:
raise RuntimeError("WeCom websocket closed")
async def _heartbeat_loop(self) -> None:
@ -998,7 +998,7 @@ class WeComAdapter(BasePlatformAdapter):
@staticmethod
def _response_error(response: Dict[str, Any]) -> Optional[str]:
errcode = response.get("errcode", 0)
if errcode in (0, None):
if errcode in {0, None}:
return None
errmsg = str(response.get("errmsg") or "unknown error")
return f"WeCom errcode {errcode}: {errmsg}"

View file

@ -605,7 +605,7 @@ def _assert_weixin_cdn_url(url: str) -> None:
except Exception as exc: # noqa: BLE001
raise ValueError(f"Unparseable media URL: {url!r}") from exc
if scheme not in ("http", "https"):
if scheme not in {"http", "https"}:
raise ValueError(
f"Media URL has disallowed scheme {scheme!r}; only http/https are permitted."
)
@ -983,7 +983,7 @@ def _extract_text(item_list: List[Dict[str, Any]]) -> str:
ref = item.get("ref_msg") or {}
ref_item = ref.get("message_item") or {}
ref_type = ref_item.get("type")
if ref_type in (ITEM_IMAGE, ITEM_VIDEO, ITEM_FILE, ITEM_VOICE):
if ref_type in {ITEM_IMAGE, ITEM_VIDEO, ITEM_FILE, ITEM_VOICE}:
title = ref.get("title") or ""
prefix = f"[引用媒体: {title}]\n" if title else "[引用媒体]\n"
return f"{prefix}{text}".strip()
@ -1331,7 +1331,7 @@ class WeixinAdapter(BasePlatformAdapter):
ret = response.get("ret", 0)
errcode = response.get("errcode", 0)
if ret not in (0, None) or errcode not in (0, None):
if ret not in {0, None} or errcode not in {0, None}:
if (ret == SESSION_EXPIRED_ERRCODE or errcode == SESSION_EXPIRED_ERRCODE
or _is_stale_session_ret(ret, errcode, response.get("errmsg"))):
logger.error("[%s] Session expired; pausing for 10 minutes", self.name)
@ -1601,7 +1601,7 @@ class WeixinAdapter(BasePlatformAdapter):
if resp and isinstance(resp, dict):
ret = resp.get("ret")
errcode = resp.get("errcode")
if (ret is not None and ret not in (0,)) or (errcode is not None and errcode not in (0,)):
if (ret is not None and ret not in {0,}) or (errcode is not None and errcode not in {0,}):
is_session_expired = (
ret == SESSION_EXPIRED_ERRCODE
or errcode == SESSION_EXPIRED_ERRCODE

View file

@ -301,9 +301,9 @@ class WhatsAppAdapter(BasePlatformAdapter):
configured = self.config.extra.get("require_mention")
if configured is not None:
if isinstance(configured, str):
return configured.lower() in ("true", "1", "yes", "on")
return configured.lower() in {"true", "1", "yes", "on"}
return bool(configured)
return os.getenv("WHATSAPP_REQUIRE_MENTION", "false").lower() in ("true", "1", "yes", "on")
return os.getenv("WHATSAPP_REQUIRE_MENTION", "false").lower() in {"true", "1", "yes", "on"}
def _whatsapp_free_response_chats(self) -> set[str]:
raw = self.config.extra.get("free_response_chats")
@ -679,7 +679,7 @@ class WhatsAppAdapter(BasePlatformAdapter):
# getattr-with-default keeps tests that construct the adapter via
# ``WhatsAppAdapter.__new__`` (bypassing __init__) working without
# every _make_adapter() helper having to seed the attribute.
if getattr(self, "_shutting_down", False) and returncode in (0, -2, -15):
if getattr(self, "_shutting_down", False) and returncode in {0, -2, -15}:
logger.info(
"[%s] Bridge exited during shutdown (code %d).",
self.name,
@ -1183,7 +1183,7 @@ class WhatsAppAdapter(BasePlatformAdapter):
if msg_type == MessageType.DOCUMENT and cached_urls:
for doc_path in cached_urls:
ext = Path(doc_path).suffix.lower()
if ext in (".txt", ".md", ".csv", ".json", ".xml", ".yaml", ".yml", ".log", ".py", ".js", ".ts", ".html", ".css"):
if ext in {".txt", ".md", ".csv", ".json", ".xml", ".yaml", ".yml", ".log", ".py", ".js", ".ts", ".html", ".css"}:
try:
file_size = Path(doc_path).stat().st_size
if file_size > MAX_TEXT_INJECT_BYTES:

View file

@ -2228,7 +2228,7 @@ class MediaResolveMiddleware(InboundMiddleware):
resp.raise_for_status()
payload = resp.json()
code = payload.get("code")
if code not in (None, 0):
if code not in {None, 0}:
raise RuntimeError(
f"resource/v1/download failed: code={code}, msg={payload.get('msg', '')}"
)
@ -2391,7 +2391,7 @@ class MediaResolveMiddleware(InboundMiddleware):
rid = m.group(2)
kind, _, filename = head.partition(":")
kind = kind.strip()
if kind not in ("image", "file"):
if kind not in {"image", "file"}:
continue
if rid in seen:
continue
@ -2993,10 +2993,10 @@ class ConnectionManager:
# Fire-and-forget heartbeat ACKs — server always responds but callers don't
# wait on these; silently discard to avoid "Unmatched Response" noise.
if cmd_type == CMD_TYPE["Response"] and cmd in (
if cmd_type == CMD_TYPE["Response"] and cmd in {
"send_group_heartbeat",
"send_private_heartbeat",
):
}:
logger.debug("[%s] Heartbeat ACK received: cmd=%s msg_id=%s", adapter.name, cmd, msg_id)
return
@ -3369,7 +3369,7 @@ class MediaSendHandler(ABC):
# Remove keys already passed explicitly to avoid "multiple values" TypeError
fwd_kwargs = {
k: v for k, v in kwargs.items()
if k not in ("file_uuid", "filename", "content_type")
if k not in {"file_uuid", "filename", "content_type"}
}
msg_body = self.build_msg_body(
upload_result,

View file

@ -150,7 +150,7 @@ def _parse_jpeg_size(buf: bytes) -> Optional[dict[str, int]]:
i += 1
continue
marker = buf[i + 1]
if marker in (0xC0, 0xC2):
if marker in {0xC0, 0xC2}:
h = struct.unpack(">H", buf[i + 5: i + 7])[0]
w = struct.unpack(">H", buf[i + 7: i + 9])[0]
return {"width": w, "height": h}
@ -165,7 +165,7 @@ def _parse_gif_size(buf: bytes) -> Optional[dict[str, int]]:
if len(buf) < 10:
return None
sig = buf[:6].decode("ascii", errors="replace")
if sig not in ("GIF87a", "GIF89a"):
if sig not in {"GIF87a", "GIF89a"}:
return None
w = struct.unpack("<H", buf[6:8])[0]
h = struct.unpack("<H", buf[8:10])[0]

View file

@ -702,7 +702,7 @@ def decode_inbound_push(data: bytes) -> Optional[dict]:
"trace_id": trace_id,
}
# 过滤空值(保持 API 整洁)
return {k: v for k, v in result.items() if v or k in ("msg_body", "msg_seq")}
return {k: v for k, v in result.items() if v or k in {"msg_body", "msg_seq"}}
except Exception as e:
if DEBUG_MODE:
logger.debug("[yuanbao_proto] decode_inbound_push failed: %s", e)