fix(discord): define view classes after lazy discord.py install

When discord.py is not installed at import time, DISCORD_AVAILABLE=False
and the view class definitions at module bottom are skipped.
check_discord_requirements() performs a lazy install and sets
DISCORD_AVAILABLE=True but never re-ran the class definitions, causing
NameError on the first button interaction (exec approval, slash confirm, etc.).

Extract the five ui.View subclasses into _define_discord_view_classes() and
call it both at module load (when discord.py is pre-installed) and inside
check_discord_requirements() after a successful lazy install.
This commit is contained in:
EloquentBrush0x 2026-05-19 03:14:16 +03:00 committed by kshitij
parent 7552e0f3c0
commit 5a3317693c
2 changed files with 97 additions and 1 deletions

View file

@ -111,6 +111,7 @@ def check_discord_requirements() -> bool:
Intents = _Intents
commands = _commands
DISCORD_AVAILABLE = True
_define_discord_view_classes()
return True
@ -4949,7 +4950,17 @@ def _component_check_auth(
return False
if DISCORD_AVAILABLE:
def _define_discord_view_classes() -> None:
"""Register Discord UI view classes as module globals.
Called at module load (when discord.py is pre-installed) and also from
check_discord_requirements() after a lazy install, so view classes are
always defined whenever DISCORD_AVAILABLE is True. Without this,
ExecApprovalView and siblings are only defined at import time; a later
lazy install sets DISCORD_AVAILABLE=True but leaves the classes
undefined, causing NameError on the first button interaction.
"""
global ExecApprovalView, SlashConfirmView, UpdatePromptView, ModelPickerView, ClarifyChoiceView
class ExecApprovalView(discord.ui.View):
"""
@ -5649,3 +5660,7 @@ if DISCORD_AVAILABLE:
self.resolved = True
for child in self.children:
child.disabled = True
if DISCORD_AVAILABLE:
_define_discord_view_classes()

View file

@ -0,0 +1,81 @@
"""Regression: Discord UI view classes must be defined after lazy-install.
When discord.py is NOT installed at module load time, the
``if DISCORD_AVAILABLE:`` guard at the bottom of gateway/platforms/discord.py
evaluates to False and is skipped leaving ExecApprovalView and its four
siblings undefined in the module globals.
check_discord_requirements() must call _define_discord_view_classes() after
a successful lazy install so that all view classes are available the moment
DISCORD_AVAILABLE flips to True. Without this, the first button interaction
(exec approval, slash confirm, etc.) raises NameError even though
DISCORD_AVAILABLE=True.
Fixes: lazy-install path NameError for ExecApprovalView, SlashConfirmView,
UpdatePromptView, ModelPickerView, ClarifyChoiceView.
"""
import importlib
import sys
from unittest.mock import patch
import pytest
_VIEW_NAMES = [
"ExecApprovalView",
"SlashConfirmView",
"UpdatePromptView",
"ModelPickerView",
"ClarifyChoiceView",
]
class TestDefineDiscordViewClasses:
"""_define_discord_view_classes() registers all UI view classes in module globals."""
def test_registers_all_five_view_classes(self, monkeypatch):
"""Calling _define_discord_view_classes() must (re)define all 5 view classes."""
dp = importlib.import_module("gateway.platforms.discord")
# Remove the classes to simulate the state where the module was loaded
# with DISCORD_AVAILABLE=False (the lazy-install scenario).
for name in _VIEW_NAMES:
monkeypatch.delattr(dp, name)
# Pre-condition: classes are gone
for name in _VIEW_NAMES:
assert not hasattr(dp, name), f"{name} should be absent before the call"
dp._define_discord_view_classes()
for name in _VIEW_NAMES:
assert hasattr(dp, name), f"{name} must be defined after _define_discord_view_classes()"
assert isinstance(getattr(dp, name), type), f"{name} must be a class"
def test_check_discord_requirements_calls_define_on_lazy_install(self, monkeypatch):
"""check_discord_requirements() must call _define_discord_view_classes() on
a successful lazy install so view classes exist when DISCORD_AVAILABLE=True."""
dp = importlib.import_module("gateway.platforms.discord")
# Simulate discord not yet available at module load.
monkeypatch.setattr(dp, "DISCORD_AVAILABLE", False)
define_called = [False]
orig_define = dp._define_discord_view_classes
def _spy_define():
define_called[0] = True
orig_define()
monkeypatch.setattr(dp, "_define_discord_view_classes", _spy_define)
# Patch lazy_deps.ensure to be a no-op (pretend install succeeds).
# The discord imports inside check_discord_requirements() succeed because
# _ensure_discord_mock() in conftest.py already registered the mock.
with patch("tools.lazy_deps.ensure"):
result = dp.check_discord_requirements()
assert result is True, "check_discord_requirements() should return True after lazy install"
assert define_called[0], (
"check_discord_requirements() must call _define_discord_view_classes() "
"after a successful lazy install so view classes are not undefined"
)