From d898e0eb7f2a0df757113fafbcc52d17a1a36fd9 Mon Sep 17 00:00:00 2001 From: Siddharth Balyan <52913345+alt-glitch@users.noreply.github.com> Date: Thu, 14 May 2026 10:41:46 +0530 Subject: [PATCH] fix(gateway): complete lazy-install rebind for slack/feishu/matrix + add ensure_and_bind helper (#25038) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #25028. The lazy-install hooks added in #25014 installed packages correctly but failed to rebind module-level globals after install: - Slack: missing aiohttp rebind → NameError on file uploads - Feishu: none of the ~25 lark_oapi symbols rebound → TypeError on adapter instantiation - Matrix: mautrix.types enums stayed as stubs → mismatched values at runtime Introduces tools.lazy_deps.ensure_and_bind() — a DRY helper that combines ensure() + importer-callable + globals().update(). This eliminates the error-prone pattern of manually listing every global that needs updating after lazy-install. Each platform adapter now defines a single _import() function returning all bindings. Also fixes: pyproject.toml [slack] extra was missing aiohttp (needed by slack-bolt's async path). --- gateway/platforms/feishu.py | 66 +++++++++++++++++++++++++++++-------- gateway/platforms/matrix.py | 28 ++++++++++++---- gateway/platforms/slack.py | 35 ++++++++++---------- pyproject.toml | 2 +- tools/lazy_deps.py | 55 ++++++++++++++++++++++++++++++- uv.lock | 2 ++ 6 files changed, 149 insertions(+), 39 deletions(-) diff --git a/gateway/platforms/feishu.py b/gateway/platforms/feishu.py index e7be062e84c..6481c8fa31a 100644 --- a/gateway/platforms/feishu.py +++ b/gateway/platforms/feishu.py @@ -1346,22 +1346,62 @@ def check_feishu_requirements() -> bool: """Check if Feishu/Lark dependencies are available. Lazy-installs lark-oapi via ``tools.lazy_deps.ensure("platform.feishu")`` - on first call if not present. + on first call if not present. Rebinds all module-level globals on success. """ - global FEISHU_AVAILABLE if FEISHU_AVAILABLE: return True - try: - from tools.lazy_deps import ensure as _lazy_ensure - _lazy_ensure("platform.feishu", prompt=False) - except Exception: - return False - try: - import lark_oapi # noqa: F401 - except ImportError: - return False - FEISHU_AVAILABLE = True - return True + + def _import(): + import lark_oapi as lark + from lark_oapi.api.application.v6 import GetApplicationRequest + from lark_oapi.api.im.v1 import ( + CreateFileRequest, CreateFileRequestBody, + CreateImageRequest, CreateImageRequestBody, + CreateMessageRequest, CreateMessageRequestBody, + GetChatRequest, GetMessageRequest, GetMessageResourceRequest, + P2ImMessageMessageReadV1, + ReplyMessageRequest, ReplyMessageRequestBody, + UpdateMessageRequest, UpdateMessageRequestBody, + ) + from lark_oapi.core import AccessTokenType, HttpMethod + from lark_oapi.core.const import FEISHU_DOMAIN, LARK_DOMAIN + from lark_oapi.core.model import BaseRequest + from lark_oapi.event.callback.model.p2_card_action_trigger import ( + CallBackCard, P2CardActionTriggerResponse, + ) + from lark_oapi.event.dispatcher_handler import EventDispatcherHandler + from lark_oapi.ws import Client as FeishuWSClient + return { + "lark": lark, + "GetApplicationRequest": GetApplicationRequest, + "CreateFileRequest": CreateFileRequest, + "CreateFileRequestBody": CreateFileRequestBody, + "CreateImageRequest": CreateImageRequest, + "CreateImageRequestBody": CreateImageRequestBody, + "CreateMessageRequest": CreateMessageRequest, + "CreateMessageRequestBody": CreateMessageRequestBody, + "GetChatRequest": GetChatRequest, + "GetMessageRequest": GetMessageRequest, + "GetMessageResourceRequest": GetMessageResourceRequest, + "P2ImMessageMessageReadV1": P2ImMessageMessageReadV1, + "ReplyMessageRequest": ReplyMessageRequest, + "ReplyMessageRequestBody": ReplyMessageRequestBody, + "UpdateMessageRequest": UpdateMessageRequest, + "UpdateMessageRequestBody": UpdateMessageRequestBody, + "AccessTokenType": AccessTokenType, + "HttpMethod": HttpMethod, + "FEISHU_DOMAIN": FEISHU_DOMAIN, + "LARK_DOMAIN": LARK_DOMAIN, + "BaseRequest": BaseRequest, + "CallBackCard": CallBackCard, + "P2CardActionTriggerResponse": P2CardActionTriggerResponse, + "EventDispatcherHandler": EventDispatcherHandler, + "FeishuWSClient": FeishuWSClient, + "FEISHU_AVAILABLE": True, + } + + from tools.lazy_deps import ensure_and_bind + return ensure_and_bind("platform.feishu", _import, globals(), prompt=False) class FeishuAdapter(BasePlatformAdapter): diff --git a/gateway/platforms/matrix.py b/gateway/platforms/matrix.py index 12075e67837..95dc73201c5 100644 --- a/gateway/platforms/matrix.py +++ b/gateway/platforms/matrix.py @@ -227,7 +227,7 @@ def check_matrix_requirements() -> bool: """Return True if the Matrix adapter can be used. Lazy-installs mautrix via ``tools.lazy_deps.ensure("platform.matrix")`` - on first call if not present. + on first call if not present. Rebinds all module-level type globals on success. """ token = os.getenv("MATRIX_ACCESS_TOKEN", "") password = os.getenv("MATRIX_PASSWORD", "") @@ -242,11 +242,27 @@ def check_matrix_requirements() -> bool: try: import mautrix # noqa: F401 except ImportError: - try: - from tools.lazy_deps import ensure as _lazy_ensure - _lazy_ensure("platform.matrix", prompt=False) - import mautrix # noqa: F401, F811 - except Exception: + def _import(): + from mautrix.types import ( + ContentURI, EventID, EventType, PaginationDirection, + PresenceState, RoomCreatePreset, RoomID, SyncToken, + TrustState, UserID, + ) + return { + "ContentURI": ContentURI, + "EventID": EventID, + "EventType": EventType, + "PaginationDirection": PaginationDirection, + "PresenceState": PresenceState, + "RoomCreatePreset": RoomCreatePreset, + "RoomID": RoomID, + "SyncToken": SyncToken, + "TrustState": TrustState, + "UserID": UserID, + } + + from tools.lazy_deps import ensure_and_bind + if not ensure_and_bind("platform.matrix", _import, globals(), prompt=False): logger.warning( "Matrix: mautrix not installed. Run: pip install 'mautrix[encryption]'" ) diff --git a/gateway/platforms/slack.py b/gateway/platforms/slack.py index 53d7c57da40..ca34ab4acac 100644 --- a/gateway/platforms/slack.py +++ b/gateway/platforms/slack.py @@ -76,27 +76,26 @@ def check_slack_requirements() -> bool: """Check if Slack dependencies are available. Lazy-installs slack-bolt/slack-sdk via ``tools.lazy_deps.ensure("platform.slack")`` - on first call if not present. + on first call if not present. Rebinds all module-level globals on success. """ - global SLACK_AVAILABLE, AsyncApp, AsyncSocketModeHandler, AsyncWebClient if SLACK_AVAILABLE: return True - try: - from tools.lazy_deps import ensure as _lazy_ensure - _lazy_ensure("platform.slack", prompt=False) - except Exception: - return False - try: - from slack_bolt.async_app import AsyncApp as _App - from slack_bolt.adapter.socket_mode.async_handler import AsyncSocketModeHandler as _Handler - from slack_sdk.web.async_client import AsyncWebClient as _Client - except ImportError: - return False - AsyncApp = _App - AsyncSocketModeHandler = _Handler - AsyncWebClient = _Client - SLACK_AVAILABLE = True - return True + + def _import(): + from slack_bolt.async_app import AsyncApp + from slack_bolt.adapter.socket_mode.async_handler import AsyncSocketModeHandler + from slack_sdk.web.async_client import AsyncWebClient + import aiohttp + return { + "AsyncApp": AsyncApp, + "AsyncSocketModeHandler": AsyncSocketModeHandler, + "AsyncWebClient": AsyncWebClient, + "aiohttp": aiohttp, + "SLACK_AVAILABLE": True, + } + + from tools.lazy_deps import ensure_and_bind + return ensure_and_bind("platform.slack", _import, globals(), prompt=False) def _extract_text_from_slack_blocks(blocks: list) -> str: diff --git a/pyproject.toml b/pyproject.toml index 118f30c501c..a880bcb05bf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -83,7 +83,7 @@ hindsight = ["hindsight-client==0.6.1"] dev = ["debugpy==1.8.20", "pytest==9.0.2", "pytest-asyncio==1.3.0", "pytest-xdist==3.8.0", "pytest-split==0.11.0", "mcp==1.26.0", "ty==0.0.21", "ruff==0.15.10"] messaging = ["python-telegram-bot[webhooks]==22.6", "discord.py[voice]==2.7.1", "aiohttp==3.13.3", "slack-bolt==1.27.0", "slack-sdk==3.40.1", "qrcode==7.4.2"] cron = [] # croniter is now a core dependency; this extra kept for back-compat -slack = ["slack-bolt==1.27.0", "slack-sdk==3.40.1"] +slack = ["slack-bolt==1.27.0", "slack-sdk==3.40.1", "aiohttp==3.13.3"] matrix = ["mautrix[encryption]==0.21.0", "Markdown==3.10.2", "aiosqlite==0.22.1", "asyncpg==0.31.0", "aiohttp-socks==0.11.0"] cli = ["simple-term-menu==1.6.6"] tts-premium = ["elevenlabs==1.59.0"] diff --git a/tools/lazy_deps.py b/tools/lazy_deps.py index 6e298c23320..60883663439 100644 --- a/tools/lazy_deps.py +++ b/tools/lazy_deps.py @@ -59,7 +59,7 @@ import subprocess import sys from dataclasses import dataclass from pathlib import Path -from typing import Optional +from typing import Any, Callable, Optional logger = logging.getLogger(__name__) @@ -440,3 +440,56 @@ def feature_install_command(feature: str) -> Optional[str]: return None specs = LAZY_DEPS[feature] return "uv pip install " + " ".join(repr(s) for s in specs) + + +def ensure_and_bind( + feature: str, + importer: Callable[[], dict[str, Any]], + target_globals: dict, + *, + prompt: bool = False, +) -> bool: + """Ensure a feature is installed, then rebind names into the caller's globals. + + Combines :func:`ensure` with a post-install import step that rebinds + module-level names. This eliminates the error-prone pattern of manually + listing every global that needs updating after lazy-install. + + ``importer`` is a zero-arg callable that returns a dict of + ``{name: value}`` for all symbols the caller needs rebound. It is called + only after :func:`ensure` succeeds (or if the packages are already + installed). + + Returns True on success, False if deps couldn't be installed or imported. + + Example usage in a platform adapter:: + + def check_slack_requirements() -> bool: + if SLACK_AVAILABLE: + return True + def _import(): + from slack_bolt.async_app import AsyncApp + from slack_bolt.adapter.socket_mode.async_handler import AsyncSocketModeHandler + from slack_sdk.web.async_client import AsyncWebClient + import aiohttp + return { + "AsyncApp": AsyncApp, + "AsyncSocketModeHandler": AsyncSocketModeHandler, + "AsyncWebClient": AsyncWebClient, + "aiohttp": aiohttp, + "SLACK_AVAILABLE": True, + } + return ensure_and_bind("platform.slack", _import, globals(), prompt=False) + """ + try: + ensure(feature, prompt=prompt) + except (FeatureUnavailable, Exception): + return False + + try: + bindings = importer() + except ImportError: + return False + + target_globals.update(bindings) + return True diff --git a/uv.lock b/uv.lock index 713cd588fd6..a519cc2b194 100644 --- a/uv.lock +++ b/uv.lock @@ -2092,6 +2092,7 @@ rl = [ { name = "wandb" }, ] slack = [ + { name = "aiohttp" }, { name = "slack-bolt" }, { name = "slack-sdk" }, ] @@ -2149,6 +2150,7 @@ requires-dist = [ { name = "agent-client-protocol", marker = "extra == 'acp'", specifier = "==0.9.0" }, { name = "aiohttp", marker = "extra == 'homeassistant'", specifier = "==3.13.3" }, { name = "aiohttp", marker = "extra == 'messaging'", specifier = "==3.13.3" }, + { name = "aiohttp", marker = "extra == 'slack'", specifier = "==3.13.3" }, { name = "aiohttp", marker = "extra == 'sms'", specifier = "==3.13.3" }, { name = "aiohttp-socks", marker = "extra == 'matrix'", specifier = "==0.11.0" }, { name = "aiosqlite", marker = "extra == 'matrix'", specifier = "==0.22.1" },