Skip to content

Commit 5f3c4e2

Browse files
authored
fix: Add MissingVoiceDependencies error (#3158)
If this breaks: paillat said "trust"
1 parent 4498ae5 commit 5f3c4e2

File tree

9 files changed

+87
-54
lines changed

9 files changed

+87
-54
lines changed

discord/client.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -70,8 +70,7 @@
7070
from .threads import Thread
7171
from .ui.view import BaseView
7272
from .user import ClientUser, User
73-
from .utils import _D, _FETCHABLE, MISSING
74-
from .voice.utils.dependencies import warn_if_voice_dependencies_missing
73+
from .utils import _D, _FETCHABLE, MISSING, warn_if_voice_dependencies_missing
7574
from .webhook import Webhook
7675
from .widget import Widget
7776

@@ -93,7 +92,6 @@
9392

9493
Coro = TypeVar("Coro", bound=Callable[..., Coroutine[Any, Any, Any]])
9594

96-
9795
_log = logging.getLogger(__name__)
9896

9997

discord/errors.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@
6464
"ApplicationCommandError",
6565
"CheckFailure",
6666
"ApplicationCommandInvokeError",
67+
"MissingVoiceDependenciesError",
6768
)
6869

6970

@@ -411,3 +412,23 @@ def __init__(self, e: Exception) -> None:
411412
super().__init__(
412413
f"Application Command raised an exception: {e.__class__.__name__}: {e}"
413414
)
415+
416+
417+
class MissingVoiceDependenciesError(RuntimeError, DiscordException):
418+
"""Raised when required voice dependencies are not installed.
419+
420+
.. note::
421+
This exception inherits from both :exc:`RuntimeError` and :exc:`DiscordException`.
422+
423+
Attributes:
424+
missing: tuple[str, ...]
425+
The missing dependencies that are required for voice support.
426+
"""
427+
428+
def __init__(self, missing: tuple[str, ...]) -> None:
429+
self.missing: tuple[str, ...] = missing
430+
deps = ", ".join(missing)
431+
super().__init__(
432+
f"{deps} {'is' if len(missing) == 1 else 'are'} required for voice support. "
433+
'Install them with "pip install py-cord[voice]".'
434+
)

discord/utils.py

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,6 @@
9393
else:
9494
HAS_MSGSPEC = True
9595

96-
9796
__all__ = (
9897
"parse_time",
9998
"warn_deprecated",
@@ -140,7 +139,6 @@
140139
)
141140
EMOJIS_MAP = {}
142141

143-
144142
UNICODE_EMOJIS = set(EMOJIS_MAP.values())
145143

146144

@@ -176,7 +174,6 @@ class _RequestLike(Protocol):
176174
AutocompleteContext = Any
177175
OptionChoice = Any
178176

179-
180177
T = TypeVar("T")
181178
T_co = TypeVar("T_co", covariant=True)
182179
_Iter = Union[Iterator[T], AsyncIterator[T]]
@@ -1632,3 +1629,39 @@ def users_to_csv(users: Iterable[Snowflake]) -> io.BytesIO:
16321629
A file-like object containing the CSV data.
16331630
"""
16341631
return io.BytesIO("\n".join(map(lambda u: str(u.id), users)).encode("utf-8"))
1632+
1633+
1634+
voice_dependency_warning_emitted = False
1635+
1636+
1637+
def get_missing_voice_dependencies() -> tuple[str, ...]:
1638+
missing: list[str] = []
1639+
try:
1640+
import nacl.secret
1641+
import nacl.utils
1642+
except ImportError:
1643+
missing.append("PyNaCl")
1644+
1645+
try:
1646+
import davey
1647+
except ImportError:
1648+
missing.append("davey")
1649+
return tuple(missing)
1650+
1651+
1652+
def warn_if_voice_dependencies_missing() -> None:
1653+
global voice_dependency_warning_emitted
1654+
if voice_dependency_warning_emitted:
1655+
return
1656+
1657+
missing = get_missing_voice_dependencies()
1658+
if not missing:
1659+
return
1660+
1661+
voice_dependency_warning_emitted = True
1662+
deps = ", ".join(missing)
1663+
_log.warning(
1664+
"%s %s not installed, voice will NOT be supported",
1665+
deps,
1666+
"is" if len(missing) == 1 else "are",
1667+
)

discord/voice/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,12 @@
88
:license: MIT, see LICENSE for more details.
99
"""
1010

11+
from ..errors import MissingVoiceDependenciesError
12+
from ..utils import get_missing_voice_dependencies
13+
14+
if missing := get_missing_voice_dependencies():
15+
raise MissingVoiceDependenciesError(missing=missing)
16+
1117
from ._types import *
1218
from .client import *
1319
from .packets import *

discord/voice/client.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,11 +40,12 @@
4040
from discord.sinks.errors import RecordingException
4141
from discord.utils import MISSING
4242

43+
from ..utils import get_missing_voice_dependencies
4344
from ._types import VoiceProtocol
4445
from .enums import OpCodes
4546
from .receive import AudioReader
4647
from .state import VoiceConnectionState
47-
from .utils.dependencies import HAS_DAVEY, HAS_NACL, get_missing_voice_dependencies
48+
from .utils.dependencies import HAS_DAVEY, HAS_NACL
4849

4950
if HAS_NACL:
5051
import nacl.secret

discord/voice/utils/dependencies.py

Lines changed: 0 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,6 @@
2222
DEALINGS IN THE SOFTWARE.
2323
"""
2424

25-
import logging
26-
2725
try:
2826
import davey
2927
except ImportError:
@@ -40,34 +38,3 @@
4038
HAS_NACL = False
4139
else:
4240
HAS_NACL = True
43-
44-
VOICE_DEPENDENCY_WARNING_EMITTED = False
45-
46-
_log = logging.getLogger("discord.client")
47-
48-
49-
def get_missing_voice_dependencies() -> tuple[str, ...]:
50-
missing: list[str] = []
51-
if not HAS_NACL:
52-
missing.append("PyNaCl")
53-
if not HAS_DAVEY:
54-
missing.append("davey")
55-
return tuple(missing)
56-
57-
58-
def warn_if_voice_dependencies_missing() -> None:
59-
global VOICE_DEPENDENCY_WARNING_EMITTED
60-
if VOICE_DEPENDENCY_WARNING_EMITTED:
61-
return
62-
63-
missing = get_missing_voice_dependencies()
64-
if not missing:
65-
return
66-
67-
VOICE_DEPENDENCY_WARNING_EMITTED = True
68-
deps = ", ".join(missing)
69-
_log.warning(
70-
"%s %s not installed, voice will NOT be supported",
71-
deps,
72-
"is" if len(missing) == 1 else "are",
73-
)

docs/api/exceptions.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ Exception Hierarchy
4141
- :exc:`sinks.MKVSinkError`
4242
- :exc:`sinks.MKASinkError`
4343
- :exc:`sinks.OGGSinkError`
44+
- :exc:`MissingVoiceDependenciesError`
4445

4546
Objects
4647
-------
@@ -124,3 +125,5 @@ The following exceptions are thrown by the library.
124125
.. autoexception:: discord.sinks.MKASinkError
125126

126127
.. autoexception:: discord.sinks.OGGSinkError
128+
129+
.. autoexception:: discord.MissingVoiceDependenciesError

tests/voice/test_dependency_behavior.py

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -27,14 +27,13 @@
2727
import pytest
2828

2929
import discord
30-
from discord.voice.utils import dependencies as voice_dependencies
3130

3231

3332
def test_client_warns_once_when_voice_dependencies_are_missing(caplog):
34-
if not voice_dependencies.get_missing_voice_dependencies():
33+
if not discord.utils.get_missing_voice_dependencies():
3534
pytest.skip("requires an environment without the voice extra")
3635

37-
voice_dependencies.VOICE_DEPENDENCY_WARNING_EMITTED = False
36+
discord.utils.voice_dependency_warning_emitted = False
3837

3938
with caplog.at_level(logging.WARNING, logger="discord.client"):
4039
discord.Client()
@@ -43,19 +42,25 @@ def test_client_warns_once_when_voice_dependencies_are_missing(caplog):
4342
warnings = [
4443
record.getMessage()
4544
for record in caplog.records
46-
if record.name == "discord.client"
45+
if record.name == "discord.utils"
4746
]
4847
assert len(warnings) == 1
4948
assert warnings[0].endswith("voice will NOT be supported")
50-
for dependency in voice_dependencies.get_missing_voice_dependencies():
49+
for dependency in discord.utils.get_missing_voice_dependencies():
5150
assert dependency in warnings[0]
5251

5352

54-
def test_voice_modules_remain_importable_without_voice_dependencies():
55-
if not voice_dependencies.get_missing_voice_dependencies():
53+
def test_voice_modules_imports_without_voice_dependencies():
54+
if not discord.utils.get_missing_voice_dependencies():
5655
pytest.skip("requires an environment without the voice extra")
5756

5857
__import__("discord")
59-
__import__("discord.voice")
60-
__import__("discord.voice.gateway")
61-
__import__("discord.voice.receive.reader")
58+
59+
with pytest.raises(discord.MissingVoiceDependenciesError):
60+
__import__("discord.voice")
61+
62+
with pytest.raises(discord.MissingVoiceDependenciesError):
63+
__import__("discord.voice.gateway")
64+
65+
with pytest.raises(discord.MissingVoiceDependenciesError):
66+
__import__("discord.voice.receive.reader")

tests/voice/test_imports.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,14 +27,13 @@
2727
import pytest
2828

2929
import discord
30-
from discord.voice.utils import dependencies as voice_dependencies
3130

3231

3332
def test_client_does_not_warn_when_voice_dependencies_are_available(caplog):
34-
if voice_dependencies.get_missing_voice_dependencies():
33+
if discord.utils.get_missing_voice_dependencies():
3534
pytest.skip("requires an environment with the voice extra")
3635

37-
voice_dependencies.VOICE_DEPENDENCY_WARNING_EMITTED = False
36+
discord.utils.voice_dependency_warning_emitted = False
3837

3938
with caplog.at_level(logging.WARNING, logger="discord.client"):
4039
discord.Client()
@@ -48,7 +47,7 @@ def test_client_does_not_warn_when_voice_dependencies_are_available(caplog):
4847

4948

5049
def test_voice_modules_import_with_voice_extra():
51-
if voice_dependencies.get_missing_voice_dependencies():
50+
if discord.utils.get_missing_voice_dependencies():
5251
pytest.skip("requires an environment with the voice extra")
5352

5453
__import__("discord.voice")

0 commit comments

Comments
 (0)