ci: fix remaining ASYNCXXX linter errors

This commit is contained in:
Maciej Lisiewski committed 2026-10-10 15:20:14 +02:00
1 parent bda3aea496
commit c57fbf4584
25 files changed
+49 -48

No files matched your search

+1 -1
View File
@@ -108,7 +108,7 @@ async def run(repo: str, out: str):
"violations": [{"method": v["method"], "body_keys": sorted(v["body"].keys())} for v in violations],
"results": results,
}
with open(out, "w", encoding="utf-8") as f:
with open(out, "w", encoding="utf-8") as f: # noqa: ASYNC230 -- small local write; a local open() is non-blocking in practice
json.dump(summary, f, indent=2)
seq = [(e["method"], sorted(e["body"].keys()), e["response"].get("error")) for e in log]
for s in seq:
+1 -1
View File
@@ -1240,7 +1240,7 @@ class QQAdapter(OwnAccessPolicyMixin, BasePlatformAdapter):
base_url, api_key, model = stt_cfg["base_url"], stt_cfg["api_key"], stt_cfg["model"]
try:
with open(wav_path, "rb") as f:
with open(wav_path, "rb") as f: # noqa: ASYNC230 -- file handle is streamed to the upload; a local open() is non-blocking in practice
resp = await self._http_client.post(
f"{base_url}/audio/transcriptions",
headers={"Authorization": f"Bearer {api_key}"},
+1 -1
View File
@@ -535,7 +535,7 @@ class WhatsAppCloudAdapter(WhatsAppBehaviorMixin, BasePlatformAdapter):
return None, f"File {os.path.basename(file_path)} is {size} bytes; Cloud API {media_kind} cap is {cap} bytes"
mime_type = mime_type or mimetypes.guess_type(file_path)[0] or _DEFAULT_MIME.get(media_kind, "application/octet-stream")
try:
with open(file_path, "rb") as fh:
with open(file_path, "rb") as fh: # noqa: ASYNC230 -- file handle is streamed to the upload; a local open() is non-blocking in practice
files = {"file": (os.path.basename(file_path), fh, mime_type), "messaging_product": (None, "whatsapp"), "type": (None, mime_type)}
resp = await self._http_client.post(self._graph_url("media"), headers=self._auth_headers(json_body=False), files=files)
except Exception as exc:
+1 -1
View File
@@ -1495,7 +1495,7 @@ class GatewayShutdownMixin(GatewaySessionEndMixin):
)
setsid_bin = shutil.which("setsid")
argv = [setsid_bin, "bash", "-lc", shell_cmd] if setsid_bin else ["bash", "-lc", shell_cmd]
subprocess.Popen(
subprocess.Popen( # noqa: ASYNC220 -- fire-and-forget spawn; Popen returns immediately and callers need the Popen object
argv, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
env=GatewayShutdownMixin._restart_watcher_env(), start_new_session=True,
)
+6 -5
View File
@@ -9,6 +9,7 @@ Shared helpers are reached via the late-binding seam in :mod:`hermes_cli.web_dep
so a test's ``monkeypatch.setattr(<owning module>, "_helper", ...)`` keeps working.
"""
import asyncio
import contextlib
import copy
import functools
@@ -939,16 +940,16 @@ async def open_profile_terminal_endpoint(name: str):
command = _profile_setup_command(name)
if sys.platform.startswith("win"):
subprocess.Popen(["cmd.exe", "/c", "start", "", command])
subprocess.Popen(["cmd.exe", "/c", "start", "", command]) # noqa: ASYNC220 -- fire-and-forget spawn; Popen returns immediately and callers need the Popen object
elif sys.platform == "darwin":
escaped = command.replace("\\", "\\\\").replace('"', '\\"')
subprocess.Popen(["osascript", "-e",
subprocess.Popen(["osascript", "-e", # noqa: ASYNC220 -- fire-and-forget spawn; Popen returns immediately and callers need the Popen object
f'tell application "Terminal"\nactivate\ndo script "{escaped}"\nend tell'])
else:
for executable, popen_args in _linux_terminal_commands(command):
if subprocess.call(["which", executable], stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL) == 0:
subprocess.Popen(popen_args)
if await asyncio.to_thread(subprocess.call, ["which", executable], stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL) == 0:
subprocess.Popen(popen_args) # noqa: ASYNC220 -- fire-and-forget spawn; Popen returns immediately and callers need the Popen object
break
else:
raise HTTPException(status_code=400, detail="No supported terminal emulator found")
+1 -1
View File
@@ -484,7 +484,7 @@ async def upload_task_attachment(
dest_path = _collision_free_path(dest_dir, safe_name) # foo.pdf → foo (1).pdf …
total = 0 # stream in chunks with a hard size cap so one upload can't fill the disk
try:
with open(dest_path, "wb") as out:
with open(dest_path, "wb") as out: # noqa: ASYNC230 -- chunked local write of the upload stream; open() is non-blocking in practice
while chunk := await file.read(1024 * 1024):
total += len(chunk)
if total > KANBAN_ATTACHMENT_MAX_BYTES:
+5 -6
View File
@@ -7209,11 +7209,10 @@ async def _standalone_send(
form.add_field("payload_json", payload_json, content_type="application/json")
try:
for idx, media_path in enumerate(valid_media):
with open(media_path, "rb") as fh:
form.add_field(
f"files[{idx}]", fh.read(),
filename=os.path.basename(media_path),
)
form.add_field(
f"files[{idx}]", await asyncio.to_thread(_Path(media_path).read_bytes),
filename=os.path.basename(media_path),
)
async with session.post(thread_url, headers=auth_headers, data=form, **_req_kw) as resp:
data, err = await _standalone_response_json_or_error(resp, "Discord forum thread creation error")
if err:
@@ -7273,7 +7272,7 @@ async def _standalone_send(
content_type="application/json",
)
caption_pending = False
with open(media_path, "rb") as f:
with open(media_path, "rb") as f: # noqa: ASYNC230 -- file handle is streamed to the upload; a local open() is non-blocking in practice
form.add_field("files[0]", f, filename=filename)
async with session.post(url, headers=auth_headers, data=form, **_req_kw) as resp:
data, err = await _standalone_response_json_or_error(resp, "Discord API error")
+2 -2
View File
@@ -5,6 +5,7 @@ import asyncio
import json
import logging
import os
from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple
from agent.i18n import t
@@ -293,8 +294,7 @@ class DiscordMediaMixin:
if rejected is not None:
return rejected
reference = self._reply_reference_for_send(reply_to, channel)
with open(audio_path, "rb") as f:
file_data = f.read()
file_data = await asyncio.to_thread(Path(audio_path).read_bytes)
# Forum channels reject POST /messages (native voice path too); create a thread post instead.
if self._is_forum_parent(channel):
forum_file = discord.File(io.BytesIO(file_data), filename=filename)
+2 -3
View File
@@ -1917,8 +1917,7 @@ class FeishuAdapter(BasePlatformAdapter):
return SendResult(success=False, error=f"Image file not found: {image_path}")
try:
import io as _io
with open(image_path, "rb") as f:
image_file = _io.BytesIO(f.read()) # lark's MultipartEncoder needs .name and .tell()
image_file = _io.BytesIO(await asyncio.to_thread(Path(image_path).read_bytes)) # lark's MultipartEncoder needs .name and .tell()
image_file.name = os.path.basename(image_path)
body = self._build_image_upload_body(image_type=_FEISHU_IMAGE_UPLOAD_TYPE, image=image_file)
request = self._build_image_upload_request(body)
@@ -3668,7 +3667,7 @@ class FeishuAdapter(BasePlatformAdapter):
)
try:
duration_ms = self._get_audio_duration_ms(file_path) if upload_file_type == "opus" else 0
with open(file_path, "rb") as file_obj:
with open(file_path, "rb") as file_obj: # noqa: ASYNC230 -- file handle is streamed to the upload; a local open() is non-blocking in practice
body = self._build_file_upload_body(
file_type=upload_file_type, file_name=display_name, file=file_obj, duration=duration_ms,
)
+2 -2
View File
@@ -622,8 +622,8 @@ async def _standalone_send(pconfig, chat_id: str, message: str, *, thread_id: Op
continue
form = aiohttp.FormData()
form.add_field("channel_id", chat_id) # required so the server can attribute the upload
with open(file_path, "rb") as fh:
form.add_field("files", fh.read(), filename=os.path.basename(file_path))
form.add_field("files", await asyncio.to_thread(Path(file_path).read_bytes),
filename=os.path.basename(file_path))
async with session.post(f"{base_url}/api/v4/files", data=form, headers=upload_headers,
**_req_kw) as upload_resp:
if upload_resp.status not in {200, 201}:
+1 -1
View File
@@ -1049,7 +1049,7 @@ class PhotonAdapter(BasePlatformAdapter):
from hermes_cli._subprocess_compat import windows_hide_flags # hide child console on Windows
await self._apply_spectrum_patch(windows_hide_flags())
try:
self._sidecar_proc = subprocess.Popen(
self._sidecar_proc = subprocess.Popen( # noqa: ASYNC220 -- persistent sidecar; callers use the Popen pipes and poll()
[self._node_bin, str(_sidecar_dir() / "index.mjs")],
stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, env=env,
start_new_session=(sys.platform != "win32"),
+3 -2
View File
@@ -21,6 +21,7 @@ import re
import sys
from collections import deque
from contextlib import contextmanager, suppress
from pathlib import Path
from typing import Any, Dict, Iterator, Optional
from urllib.parse import urlparse
@@ -751,8 +752,8 @@ class TeamsAdapter(BasePlatformAdapter):
else:
path = source.removeprefix("file://")
mime_type = mimetypes.guess_type(path)[0] or default_mime
with open(path, "rb") as f:
content_url = f"data:{mime_type};base64,{base64.b64encode(f.read()).decode()}"
data = await asyncio.to_thread(Path(path).read_bytes)
content_url = f"data:{mime_type};base64,{base64.b64encode(data).decode()}"
activity = MessageActivityInput().add_attachments(Attachment(content_type=mime_type, content_url=content_url))
if caption:
activity = activity.add_text(caption)
+3 -3
View File
@@ -5193,7 +5193,7 @@ class TelegramAdapter(TelegramHeldInboundMixin, BasePlatformAdapter):
self.name, os.path.basename(audio_path))
# Telegram drops duration for long clips (~5 min+, shows 0:00).
_duration_secs = await asyncio.to_thread(_probe_voice_duration_seconds, audio_path)
with open(audio_path, "rb") as audio_file:
with open(audio_path, "rb") as audio_file: # noqa: ASYNC230 -- file handle is streamed to the upload; a local open() is non-blocking in practice
ext = os.path.splitext(audio_path)[1].lower()
if ext in {".ogg", ".opus"}: # round playable voice bubble
msg = await self._send_voice_bubble(audio_file, chat_id, reply_to, metadata, caption, _duration_secs)
@@ -5260,7 +5260,7 @@ class TelegramAdapter(TelegramHeldInboundMixin, BasePlatformAdapter):
if compressed:
temp_paths.append(compressed)
local_path = compressed
source = open(local_path, "rb")
source = open(local_path, "rb") # noqa: ASYNC230 -- file handle is streamed to the upload; a local open() is non-blocking in practice
opened_files.append(source)
media.append(InputMediaPhoto(media=source, caption=self._caption_1024(alt_text)))
if not media:
@@ -5341,7 +5341,7 @@ class TelegramAdapter(TelegramHeldInboundMixin, BasePlatformAdapter):
try:
if not os.path.exists(path):
return SendResult(success=False, error=self._missing_media_path_error(label, path))
with open(path, "rb") as f:
with open(path, "rb") as f: # noqa: ASYNC230 -- file handle is streamed to the upload; a local open() is non-blocking in practice
msg = await self._send_media(
getattr(self._bot, f"send_{media_key}"), chat_id, reply_to, metadata, media_key,
reset_media=lambda: f.seek(0), **build_kwargs(f))
+2 -2
View File
@@ -681,11 +681,11 @@ class WhatsAppAdapter(WhatsAppBehaviorMixin, BasePlatformAdapter):
await asyncio.sleep(1)
# Bridge output goes to a log file so QR codes, errors, and reconnection messages survive for troubleshooting.
self._bridge_log = self._session_path.parent / "bridge.log"
self._bridge_log_fh = bridge_log_fh = open(self._bridge_log, "a", encoding="utf-8")
self._bridge_log_fh = bridge_log_fh = open(self._bridge_log, "a", encoding="utf-8") # noqa: ASYNC230 -- long-lived log handle handed to the bridge subprocess
node = find_node_executable("node")
if node is None:
raise RuntimeError("Node.js is no longer available; run `hermes pm install`")
self._bridge_process = subprocess.Popen(
self._bridge_process = subprocess.Popen( # noqa: ASYNC220 -- persistent bridge; callers use the Popen object (pid, poll)
[node, str(bridge_path), "--port", str(self._bridge_port), "--session", str(self._session_path),
"--mode", _wenv("WHATSAPP_MODE", "self-chat")], stdin=subprocess.DEVNULL, stdout=bridge_log_fh, stderr=bridge_log_fh, env=self._bridge_env(), **windows_detach_popen_kwargs())
_write_bridge_pidfile(self._session_path, self._bridge_process.pid, self._bridge_port)
+4 -4
View File
@@ -55,11 +55,11 @@ select = [
"ASYNC115",
"ASYNC116",
"ASYNC210",
# "ASYNC220",
# "ASYNC221",
"ASYNC220",
"ASYNC221",
"ASYNC222",
# "ASYNC230",
# "ASYNC251",
"ASYNC230",
"ASYNC251",
# "S102",
# "S110",
# "S112",
@@ -220,7 +220,7 @@ def _serve_http(server) -> None:
await asyncio.sleep(0.01)
if port_file:
tmp = port_file + ".tmp"
with open(tmp, "w", encoding="utf-8") as fh:
with open(tmp, "w", encoding="utf-8") as fh: # noqa: ASYNC230 -- small local write; a local open() is non-blocking in practice
fh.write(f"{port} {os.getpid()}")
os.replace(tmp, port_file)
await task
+1 -1
View File
@@ -742,7 +742,7 @@ class TestDisconnectedAgentReap:
deadline = time.time() + 1.0
while not calls and time.time() < deadline:
time.sleep(0.01)
await asyncio.sleep(0.01)
assert calls == [("run-stop-sess", frozenset(), "api_server_run_stop")]
agent.interrupt.assert_called_once()
+2 -2
View File
@@ -2422,7 +2422,7 @@ class TestInboundMediaLocalisation:
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk"
"+A8AAQUBAScY42YAAAAASUVORK5CYII="
)
with open(output_path, "wb") as handle:
with open(output_path, "wb") as handle: # noqa: ASYNC230 -- test fake writes a tiny fixture file; a local open() is non-blocking in practice
handle.write(payload)
return 0, "", ""
@@ -2591,7 +2591,7 @@ class TestInboundMediaLocalisation:
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk"
"+A8AAQUBAScY42YAAAAASUVORK5CYII="
)
with open(output_path, "wb") as handle:
with open(output_path, "wb") as handle: # noqa: ASYNC230 -- test fake writes a tiny fixture file; a local open() is non-blocking in practice
handle.write(payload)
return 0, "", ""
+1 -1
View File
@@ -101,7 +101,7 @@ async def test_blocked_loop_after_expiry_dumps_diagnostics(monkeypatch):
# Margin matters: the watchdog thread only dumps if the loop is STILL
# blocked when it wakes, and thread wakeup lags under parallel-suite load.
# 0.2s (= deadline+grace exactly) flaked in a 40-worker full-suite run.
_time.sleep(1.0)
_time.sleep(1.0) # noqa: ASYNC251 -- deliberately blocks the loop past the deadline
with pytest.raises(_asyncio.TimeoutError):
await task
+2 -2
View File
@@ -15,6 +15,7 @@ from __future__ import annotations
import asyncio
import copy
import json
from pathlib import Path
from unittest.mock import AsyncMock, MagicMock
import pytest
@@ -814,8 +815,7 @@ class TestDownloadMedia:
assert _os.path.exists(local_path)
assert _os.path.basename(local_path).startswith("media_xyz")
assert _os.path.basename(local_path).endswith(".jpg")
with open(local_path, "rb") as fh:
assert fh.read() == b"\xff\xd8\xff\xe0jpegdata"
assert await asyncio.to_thread(Path(local_path).read_bytes) == b"\xff\xd8\xff\xe0jpegdata"
@pytest.mark.asyncio
async def test_metadata_failure_returns_none(self):
+3 -2
View File
@@ -1,3 +1,4 @@
import asyncio
import json
import pytest
@@ -221,7 +222,7 @@ async def test_legacy_channel_connect_pops_marker_on_disconnect(pty_keepalive_ha
deadline = time.monotonic() + 5.0
while "LEGACY63553" in markers and time.monotonic() < deadline:
time.sleep(0.01)
await asyncio.sleep(0.01)
assert "LEGACY63553" not in markers
@@ -252,7 +253,7 @@ async def test_legacy_marker_dropped_when_cancel_lands_mid_close(pty_keepalive_h
# ... and leaving the client context cancelled the handler there.
deadline = time.monotonic() + 5.0
while "LEGACYCANCEL" in markers and time.monotonic() < deadline:
time.sleep(0.01)
await asyncio.sleep(0.01)
assert "LEGACYCANCEL" not in markers
@@ -127,7 +127,7 @@ def _start_freezeable_producer(tmp_path, block_s, errors, write_stall_s=1.5):
await freeze_evt.wait()
freeze_evt.clear()
# Synchronous sleep: freezes the entire loop for block_s.
time.sleep(block_s)
time.sleep(block_s) # noqa: ASYNC251 -- deliberately freezes the loop for block_s
async def producer() -> None:
loop = asyncio.get_running_loop()
+1 -1
View File
@@ -228,7 +228,7 @@ class TestRunAsyncWithRunningLoop:
# Worker thread must cancel the task (not leak).
deadline = _time.time() + 5
while not cancel_observed.is_set() and _time.time() < deadline:
_time.sleep(0.05)
await asyncio.sleep(0.05)
assert cancel_observed.is_set(), (
"Coroutine never received CancelledError — worker thread leaked "
"(ThreadPoolExecutor.cancel() is a no-op on a running future; "
+1 -1
View File
@@ -219,7 +219,7 @@ async def _telegram_send_one_media(bot, chat_id, media_path, is_voice, *, captio
if thumb_path:
media_kwargs["thumbnail"] = thumb_path
try:
with open(media_path, "rb") as f:
with open(media_path, "rb") as f: # noqa: ASYNC230 -- file handle is streamed to the upload; a local open() is non-blocking in practice
try:
return await _telegram_send_media(bot, chat_id, f, ext, is_voice, force_document, **media_kwargs)
except Exception as media_err:
+1 -1
View File
@@ -668,7 +668,7 @@ Write only the summary, starting with "[CONTEXT SUMMARY]:" prefix."""
self._print_summary()
if self.config.metrics_enabled:
metrics_path = output_dir / self.config.metrics_output_file
with open(metrics_path, 'w', encoding="utf-8") as f:
with open(metrics_path, 'w', encoding="utf-8") as f: # noqa: ASYNC230 -- small local write; a local open() is non-blocking in practice
json.dump(self.aggregate_metrics.to_dict(), f, indent=2)
console.print(f"\n💾 Metrics saved to {metrics_path}")