diff --git a/.github/workflows/coverage_and_lint.yml b/.github/workflows/coverage_and_lint.yml index 4ba8f883..6b8b85f1 100644 --- a/.github/workflows/coverage_and_lint.yml +++ b/.github/workflows/coverage_and_lint.yml @@ -15,7 +15,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.11", "3.x"] + python-version: ["3.10", "3.x"] name: "Type Coverage and Linting @ ${{ matrix.python-version }}" steps: @@ -34,22 +34,14 @@ jobs: - name: "Install Python deps @ ${{ matrix.python-version }}" id: install-deps run: | - pip install -U -r requirements.txt + pip install -Ur requirements.txt - name: "Run Pyright @ ${{ matrix.python-version }}" uses: jakebailey/pyright-action@v1 with: no-comments: ${{ matrix.python-version != '3.x' }} warnings: false - - name: Lint + - name: Lint with Ruff if: ${{ always() && steps.install-deps.outcome == 'success' }} - uses: github/super-linter/slim@v4 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - DEFAULT_BRANCH: main - VALIDATE_ALL_CODEBASE: false - VALIDATE_PYTHON_BLACK: true - VALIDATE_PYTHON_ISORT: true - LINTER_RULES_PATH: / - PYTHON_ISORT_CONFIG_FILE: pyproject.toml - PYTHON_BLACK_CONFIG_FILE: pyproject.toml \ No newline at end of file + uses: chartboost/ruff-action@v1 + \ No newline at end of file diff --git a/docs/extensions/attributetable.py b/docs/extensions/attributetable.py index 5b6f41be..5640a7ed 100644 --- a/docs/extensions/attributetable.py +++ b/docs/extensions/attributetable.py @@ -3,7 +3,6 @@ import importlib import inspect import re -from enum import Enum from typing import TYPE_CHECKING, Dict, List, NamedTuple, Optional, Sequence, Tuple from docutils import nodes diff --git a/docs/extensions/details.py b/docs/extensions/details.py index 18a79193..b46a4419 100644 --- a/docs/extensions/details.py +++ b/docs/extensions/details.py @@ -1,5 +1,5 @@ from docutils import nodes -from docutils.parsers.rst import Directive, directives, states +from docutils.parsers.rst import Directive, directives from docutils.parsers.rst.roles import set_classes diff --git a/docs/extensions/exception_hierarchy.py b/docs/extensions/exception_hierarchy.py index 988eeb7d..40551c41 100644 --- a/docs/extensions/exception_hierarchy.py +++ b/docs/extensions/exception_hierarchy.py @@ -1,7 +1,5 @@ from docutils import nodes -from docutils.parsers.rst import Directive, directives, states -from docutils.parsers.rst.roles import set_classes -from sphinx.locale import _ +from docutils.parsers.rst import Directive class exception_hierarchy(nodes.General, nodes.Element): diff --git a/docs/extensions/prettyversion.py b/docs/extensions/prettyversion.py index 4ffb195b..e516a13e 100644 --- a/docs/extensions/prettyversion.py +++ b/docs/extensions/prettyversion.py @@ -1,8 +1,6 @@ from docutils import nodes -from docutils.parsers.rst import Directive, directives, states -from docutils.parsers.rst.roles import set_classes +from docutils.parsers.rst import Directive from docutils.statemachine import StringList -from sphinx.locale import _ class pretty_version_added(nodes.General, nodes.Element): diff --git a/examples/simple.py b/examples/simple.py index 197949b3..079de3ac 100644 --- a/examples/simple.py +++ b/examples/simple.py @@ -21,6 +21,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ + import asyncio import logging from typing import cast @@ -46,10 +47,10 @@ async def setup_hook(self) -> None: await wavelink.Pool.connect(nodes=nodes, client=self, cache_capacity=100) async def on_ready(self) -> None: - logging.info(f"Logged in: {self.user} | {self.user.id}") + logging.info("Logged in: %s | %s", self.user, self.user.id) async def on_wavelink_node_ready(self, payload: wavelink.NodeReadyEventPayload) -> None: - logging.info(f"Wavelink Node connected: {payload.node!r} | Resumed: {payload.resumed}") + logging.info("Wavelink Node connected: %r | Resumed: %s", payload.node, payload.resumed) async def on_wavelink_track_start(self, payload: wavelink.TrackStartEventPayload) -> None: player: wavelink.Player | None = payload.player diff --git a/pyproject.toml b/pyproject.toml index eb90c32d..37aab24d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "wavelink" -version = "3.2.1" +version = "3.3.0" authors = [ { name="PythonistaGuild, EvieePy", email="evieepy@gmail.com" }, ] @@ -38,14 +38,70 @@ dependencies = {file = ["requirements.txt"]} [tool.setuptools.package-data] wavelink = ["py.typed"] -[tool.black] +[tool.ruff] line-length = 120 +indent-width = 4 +exclude = ["venv", "docs/"] -[tool.isort] -profile = "black" +[tool.ruff.lint] +select = [ + "C4", + "E", + "F", + "G", + "I", + "PTH", + "RUF", + "SIM", + "TCH", + "UP", + "W", + "PERF", + "ANN", +] +ignore = [ + "F402", + "F403", + "F405", + "PERF203", + "RUF001", + "RUF009", + "SIM105", + "UP034", + "UP038", + "ANN101", + "ANN102", + "ANN401", + "UP031", + "PTH123", + "E203", + "E501", + "RUF006" +] + +[tool.ruff.lint.isort] +split-on-trailing-comma = true +combine-as-imports = true +lines-after-imports = 2 + +[tool.ruff.lint.flake8-annotations] +allow-star-arg-any = true + +[tool.ruff.lint.flake8-quotes] +inline-quotes = "double" + +[tool.ruff.format] +quote-style = "double" +indent-style = "space" +skip-magic-trailing-comma = false +line-ending = "auto" [tool.pyright] +exclude = ["venv"] ignore = ["test*.py", "examples/*.py", "docs/*"] -pythonVersion = "3.10" +useLibraryCodeForTypes = true typeCheckingMode = "strict" +reportImportCycles = false reportPrivateUsage = false +pythonVersion = "3.10" + diff --git a/requirements.txt b/requirements.txt index 4393996d..af54eec3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,5 @@ aiohttp>=3.7.4,<4 discord.py>=2.0.1 yarl>=1.9.2 -async_timeout \ No newline at end of file +async_timeout +typing_extensions \ No newline at end of file diff --git a/wavelink/__init__.py b/wavelink/__init__.py index 1d059052..7cabbbf6 100644 --- a/wavelink/__init__.py +++ b/wavelink/__init__.py @@ -21,18 +21,18 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ + __title__ = "WaveLink" __author__ = "PythonistaGuild, EvieePy" __license__ = "MIT" __copyright__ = "Copyright 2019-Present (c) PythonistaGuild, EvieePy" -__version__ = "3.2.1" +__version__ = "3.3.0" from .enums import * from .exceptions import * from .filters import * -from .lfu import CapacityZero as CapacityZero -from .lfu import LFUCache as LFUCache +from .lfu import CapacityZero as CapacityZero, LFUCache as LFUCache from .node import * from .payloads import * from .player import Player as Player diff --git a/wavelink/__main__.py b/wavelink/__main__.py index d42cbf7d..9379c753 100644 --- a/wavelink/__main__.py +++ b/wavelink/__main__.py @@ -5,6 +5,7 @@ import wavelink + parser = argparse.ArgumentParser(prog="wavelink") parser.add_argument("--version", action="store_true", help="Get version and debug information for wavelink.") @@ -19,7 +20,7 @@ def get_debug_info() -> None: info: str = f""" wavelink: {wavelink.__version__} - + Python: - {python_info} System: diff --git a/wavelink/backoff.py b/wavelink/backoff.py index df0ccf2f..6dc0d822 100644 --- a/wavelink/backoff.py +++ b/wavelink/backoff.py @@ -21,10 +21,15 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ + from __future__ import annotations import random -from collections.abc import Callable +from typing import TYPE_CHECKING + + +if TYPE_CHECKING: + from collections.abc import Callable class Backoff: diff --git a/wavelink/enums.py b/wavelink/enums.py index 7f2f0dac..2f808fdc 100644 --- a/wavelink/enums.py +++ b/wavelink/enums.py @@ -21,8 +21,10 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ + import enum + __all__ = ("NodeStatus", "TrackSource", "DiscordVoiceCloseType", "AutoPlayMode", "QueueMode") diff --git a/wavelink/exceptions.py b/wavelink/exceptions.py index ad9830b4..7d395d0e 100644 --- a/wavelink/exceptions.py +++ b/wavelink/exceptions.py @@ -21,10 +21,12 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ + from __future__ import annotations from typing import TYPE_CHECKING + if TYPE_CHECKING: from .types.response import ErrorResponse, LoadedErrorPayload diff --git a/wavelink/filters.py b/wavelink/filters.py index 9ab037a1..ffa20b1b 100644 --- a/wavelink/filters.py +++ b/wavelink/filters.py @@ -21,23 +21,27 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ + from __future__ import annotations from typing import TYPE_CHECKING, TypedDict + if TYPE_CHECKING: from typing_extensions import Self, Unpack - from .types.filters import ChannelMix as ChannelMixPayload - from .types.filters import Distortion as DistortionPayload - from .types.filters import Equalizer as EqualizerPayload - from .types.filters import FilterPayload - from .types.filters import Karaoke as KaraokePayload - from .types.filters import LowPass as LowPassPayload - from .types.filters import Rotation as RotationPayload - from .types.filters import Timescale as TimescalePayload - from .types.filters import Tremolo as TremoloPayload - from .types.filters import Vibrato as VibratoPayload + from .types.filters import ( + ChannelMix as ChannelMixPayload, + Distortion as DistortionPayload, + Equalizer as EqualizerPayload, + FilterPayload, + Karaoke as KaraokePayload, + LowPass as LowPassPayload, + Rotation as RotationPayload, + Timescale as TimescalePayload, + Tremolo as TremoloPayload, + Vibrato as VibratoPayload, + ) __all__ = ( diff --git a/wavelink/lfu.py b/wavelink/lfu.py index d02b61c7..8580b721 100644 --- a/wavelink/lfu.py +++ b/wavelink/lfu.py @@ -21,6 +21,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ + from __future__ import annotations from collections import defaultdict @@ -30,8 +31,7 @@ from .exceptions import WavelinkException -class CapacityZero(WavelinkException): - ... +class CapacityZero(WavelinkException): ... class _MissingSentinel: diff --git a/wavelink/node.py b/wavelink/node.py index 24258111..df388b07 100644 --- a/wavelink/node.py +++ b/wavelink/node.py @@ -21,16 +21,15 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ + from __future__ import annotations import logging import secrets import urllib.parse -from collections.abc import Iterable -from typing import TYPE_CHECKING, Any, Literal, TypeAlias +from typing import TYPE_CHECKING, Any, ClassVar, Literal, TypeAlias import aiohttp -import discord from discord.utils import classproperty from . import __version__ @@ -48,7 +47,12 @@ from .tracks import Playable, Playlist from .websocket import Websocket + if TYPE_CHECKING: + from collections.abc import Iterable + + import discord + from .player import Player from .types.request import Request, UpdateSessionRequest from .types.response import ( @@ -160,7 +164,7 @@ def __init__( self._websocket: Websocket | None = None if inactive_player_timeout and inactive_player_timeout < 10: - logger.warn('Setting "inactive_player_timeout" below 10 seconds may result in unwanted side effects.') + logger.warning('Setting "inactive_player_timeout" below 10 seconds may result in unwanted side effects.') self._inactive_player_timeout = ( inactive_player_timeout if inactive_player_timeout and inactive_player_timeout > 0 else None @@ -267,7 +271,7 @@ def session_id(self) -> str | None: async def _pool_closer(self) -> None: try: await self._session.close() - except: + except Exception: pass if not self._has_closed: @@ -392,7 +396,7 @@ async def send( try: exc_data: ErrorResponse = await resp.json() except Exception as e: - logger.warning(f"An error occured making a request on {self!r}: {e}") + logger.warning("An error occured making a request on %r: %s", self, e) raise NodeException(status=resp.status) raise LavalinkException(data=exc_data) @@ -423,7 +427,7 @@ async def _fetch_players(self) -> list[PlayerResponse]: try: exc_data: ErrorResponse = await resp.json() except Exception as e: - logger.warning(f"An error occured making a request on {self!r}: {e}") + logger.warning("An error occured making a request on %r: %s", self, e) raise NodeException(status=resp.status) raise LavalinkException(data=exc_data) @@ -470,7 +474,7 @@ async def _fetch_player(self, guild_id: int, /) -> PlayerResponse: try: exc_data: ErrorResponse = await resp.json() except Exception as e: - logger.warning(f"An error occured making a request on {self!r}: {e}") + logger.warning("An error occured making a request on %r: %s", self, e) raise NodeException(status=resp.status) raise LavalinkException(data=exc_data) @@ -531,7 +535,7 @@ async def _update_player(self, guild_id: int, /, *, data: Request, replace: bool try: exc_data: ErrorResponse = await resp.json() except Exception as e: - logger.warning(f"An error occured making a request on {self!r}: {e}") + logger.warning("An error occured making a request on %r: %s", self, e) raise NodeException(status=resp.status) raise LavalinkException(data=exc_data) @@ -546,7 +550,7 @@ async def _destroy_player(self, guild_id: int, /) -> None: try: exc_data: ErrorResponse = await resp.json() except Exception as e: - logger.warning(f"An error occured making a request on {self!r}: {e}") + logger.warning("An error occured making a request on %r: %s", self, e) raise NodeException(status=resp.status) raise LavalinkException(data=exc_data) @@ -563,7 +567,7 @@ async def _update_session(self, *, data: UpdateSessionRequest) -> UpdateResponse try: exc_data: ErrorResponse = await resp.json() except Exception as e: - logger.warning(f"An error occured making a request on {self!r}: {e}") + logger.warning("An error occured making a request on %r: %s", self, e) raise NodeException(status=resp.status) raise LavalinkException(data=exc_data) @@ -580,16 +584,14 @@ async def _fetch_tracks(self, query: str) -> LoadedResponse: try: exc_data: ErrorResponse = await resp.json() except Exception as e: - logger.warning(f"An error occured making a request on {self!r}: {e}") + logger.warning("An error occured making a request on %r: %s", self, e) raise NodeException(status=resp.status) raise LavalinkException(data=exc_data) - async def _decode_track(self) -> TrackPayload: - ... + async def _decode_track(self) -> TrackPayload: ... - async def _decode_tracks(self) -> list[TrackPayload]: - ... + async def _decode_tracks(self) -> list[TrackPayload]: ... async def _fetch_info(self) -> InfoResponse: uri: str = f"{self.uri}/v4/info" @@ -603,7 +605,7 @@ async def _fetch_info(self) -> InfoResponse: try: exc_data: ErrorResponse = await resp.json() except Exception as e: - logger.warning(f"An error occured making a request on {self!r}: {e}") + logger.warning("An error occured making a request on %r: %s", self, e) raise NodeException(status=resp.status) raise LavalinkException(data=exc_data) @@ -644,7 +646,7 @@ async def _fetch_stats(self) -> StatsResponse: try: exc_data: ErrorResponse = await resp.json() except Exception as e: - logger.warning(f"An error occured making a request on {self!r}: {e}") + logger.warning("An error occured making a request on %r: %s", self, e) raise NodeException(status=resp.status) raise LavalinkException(data=exc_data) @@ -683,7 +685,7 @@ async def _fetch_version(self) -> str: try: exc_data: ErrorResponse = await resp.json() except Exception as e: - logger.warning(f"An error occured making a request on {self!r}: {e}") + logger.warning("An error occured making a request on %r: %s", self, e) raise NodeException(status=resp.status) raise LavalinkException(data=exc_data) @@ -737,7 +739,7 @@ class Pool: All methods and attributes on this class are class level, not instance. Do not create an instance of this class. """ - __nodes: dict[str, Node] = {} + __nodes: ClassVar[dict[str, Node]] = {} __cache: LFUCache | None = None @classmethod @@ -788,7 +790,7 @@ async def connect( continue if node.status in (NodeStatus.CONNECTING, NodeStatus.CONNECTED): - logger.error(f"Unable to connect {node!r} as it is already in a connecting or connected state.") + logger.error("Unable to connect %r as it is already in a connecting or connected state.", node) continue try: @@ -796,11 +798,11 @@ async def connect( except InvalidClientException as e: logger.error(e) except AuthorizationFailedException: - logger.error(f"Failed to authenticate {node!r} on Lavalink with the provided password.") + logger.error("Failed to authenticate %r on Lavalink with the provided password.", node) except NodeException: logger.error( - f"Failed to connect to {node!r}. Check that your Lavalink major version is '4' " - "and that you are trying to connect to Lavalink on the correct port." + "Failed to connect to %r. Check that your Lavalink major version is '4' and that you are trying to connect to Lavalink on the correct port.", + node, ) else: cls.__nodes[node.identifier] = node @@ -826,11 +828,11 @@ async def reconnect(cls) -> dict[str, Node]: except InvalidClientException as e: logger.error(e) except AuthorizationFailedException: - logger.error(f"Failed to authenticate {node!r} on Lavalink with the provided password.") + logger.error("Failed to authenticate %r on Lavalink with the provided password.", node) except NodeException: logger.error( - f"Failed to connect to {node!r}. Check that your Lavalink major version is '4' " - "and that you are trying to connect to Lavalink on the correct port." + "Failed to connect to %r. Check that your Lavalink major version is '4' and that you are trying to connect to Lavalink on the correct port.", + node, ) return cls.nodes diff --git a/wavelink/payloads.py b/wavelink/payloads.py index e869e37a..6d44bd2e 100644 --- a/wavelink/payloads.py +++ b/wavelink/payloads.py @@ -21,6 +21,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ + from __future__ import annotations import datetime @@ -32,6 +33,7 @@ from .filters import Filters from .tracks import Playable + if TYPE_CHECKING: from .node import Node from .player import Player diff --git a/wavelink/player.py b/wavelink/player.py index 625d7b25..49e283a0 100644 --- a/wavelink/player.py +++ b/wavelink/player.py @@ -21,13 +21,13 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ + from __future__ import annotations import asyncio import logging import random import time -from collections import deque from typing import TYPE_CHECKING, Any, TypeAlias import async_timeout @@ -55,12 +55,23 @@ from .queue import Queue from .tracks import Playable, Playlist + if TYPE_CHECKING: - from discord.types.voice import GuildVoiceState as GuildVoiceStatePayload - from discord.types.voice import VoiceServerUpdate as VoiceServerUpdatePayload + from collections import deque + + from discord.abc import Connectable + from discord.types.voice import ( + GuildVoiceState as GuildVoiceStatePayload, + VoiceServerUpdate as VoiceServerUpdatePayload, + ) from typing_extensions import Self from .node import Node + from .payloads import ( + PlayerUpdateEventPayload, + TrackEndEventPayload, + TrackStartEventPayload, + ) from .types.request import Request as RequestPayload from .types.state import PlayerVoiceState, VoiceState @@ -228,21 +239,22 @@ async def _auto_play_event(self, payload: TrackEndEventPayload) -> None: self._error_count = 0 if self.node.status is not NodeStatus.CONNECTED: - logger.warning(f'"Unable to use AutoPlay on Player for Guild "{self.guild}" due to disconnected Node.') + logger.warning( + '"Unable to use AutoPlay on Player for Guild "%s" due to disconnected Node.', str(self.guild) + ) return if not isinstance(self.queue, Queue) or not isinstance(self.auto_queue, Queue): # type: ignore - logger.warning(f'"Unable to use AutoPlay on Player for Guild "{self.guild}" due to unsupported Queue.') + logger.warning( + '"Unable to use AutoPlay on Player for Guild "%s" due to unsupported Queue.', str(self.guild) + ) self._inactivity_start() return if self.queue.mode is QueueMode.loop: await self._do_partial(history=False) - elif self.queue.mode is QueueMode.loop_all: - await self._do_partial() - - elif self._autoplay is AutoPlayMode.partial or self.queue: + elif self.queue.mode is QueueMode.loop_all or (self._autoplay is AutoPlayMode.partial or self.queue): await self._do_partial() elif self._autoplay is AutoPlayMode.enabled: @@ -262,11 +274,18 @@ async def _do_partial(self, *, history: bool = True) -> None: await self.play(track, add_history=history) - async def _do_recommendation(self): + async def _do_recommendation( + self, + *, + populate_track: wavelink.Playable | None = None, + max_population: int | None = None, + ) -> None: assert self.guild is not None assert self.queue.history is not None and self.auto_queue.history is not None - if len(self.auto_queue) > self._auto_cutoff + 1: + max_population_: int = max_population if max_population else self._auto_cutoff + + if len(self.auto_queue) > self._auto_cutoff + 1 and not populate_track: # We still do the inactivity start here since if play fails and we have no more tracks... # we should eventually fire the inactivity event... self._inactivity_start() @@ -286,6 +305,9 @@ async def _do_recommendation(self): seeds: list[Playable] = [t for t in choices if t is not None and t.identifier not in _previous] random.shuffle(seeds) + if populate_track: + seeds.insert(0, populate_track) + spotify: list[str] = [t.identifier for t in seeds if t.source == "spotify"] youtube: list[str] = [t.identifier for t in seeds if t.source == "youtube"] @@ -338,12 +360,7 @@ async def _search(query: str | None) -> T_a: if not search: return [] - tracks: list[Playable] - if isinstance(search, Playlist): - tracks = search.tracks.copy() - else: - tracks = search - + tracks: list[Playable] = search.tracks.copy() if isinstance(search, Playlist) else search return tracks results: tuple[T_a, T_a] = await asyncio.gather(_search(spotify_query), _search(youtube_query)) @@ -352,7 +369,7 @@ async def _search(query: str | None) -> T_a: filtered_r: list[Playable] = [t for r in results for t in r] if not filtered_r and not self.auto_queue: - logger.info(f'Player "{self.guild.id}" could not load any songs via AutoPlay.') + logger.info('Player "%s" could not load any songs via AutoPlay.', self.guild.id) self._inactivity_start() return @@ -371,16 +388,19 @@ async def _search(query: str | None) -> T_a: track._recommended = True added += await self.auto_queue.put_wait(track) - logger.debug(f'Player "{self.guild.id}" added "{added}" tracks to the auto_queue via AutoPlay.') + if added >= max_population_: + break + + logger.debug('Player "%s" added "%s" tracks to the auto_queue via AutoPlay.', self.guild.id, added) - if not self._current: + if not self._current and not populate_track: try: now: Playable = self.auto_queue.get() self.auto_queue.history.put(now) await self.play(now, add_history=False) except wavelink.QueueEmpty: - logger.info(f'Player "{self.guild.id}" could not load any songs via AutoPlay.') + logger.info('Player "%s" could not load any songs via AutoPlay.', self.guild.id) self._inactivity_start() @property @@ -424,7 +444,7 @@ def inactive_timeout(self, value: int | None) -> None: return if value < 10: - logger.warn('Setting "inactive_timeout" below 10 seconds may result in unwanted side effects.') + logger.warning('Setting "inactive_timeout" below 10 seconds may result in unwanted side effects.') self._inactivity_wait = value self._inactivity_cancel() @@ -615,7 +635,7 @@ async def _dispatch_voice_update(self) -> None: else: self._connection_event.set() - logger.debug(f"Player {self.guild.id} is dispatching VOICE_UPDATE.") + logger.debug("Player %s is dispatching VOICE_UPDATE.", self.guild.id) async def connect( self, *, timeout: float = 10.0, reconnect: bool, self_deaf: bool = False, self_mute: bool = False @@ -724,6 +744,8 @@ async def play( paused: bool | None = None, add_history: bool = True, filters: Filters | None = None, + populate: bool = False, + max_populate: int = 5, ) -> Playable: """Play the provided :class:`~wavelink.Playable`. @@ -756,6 +778,23 @@ async def play( filters: Optional[:class:`~wavelink.Filters`] An Optional[:class:`~wavelink.Filters`] to apply when playing this track. Defaults to ``None``. If this is ``None`` the currently set filters on the player will be applied. + populate: bool + Whether the player should find and fill AutoQueue with recommended tracks based on the track provided. + Defaults to ``False``. + + Populate will only search for recommended tracks when the current tracks has been accepted by Lavalink. + E.g. if this method does not raise an error. + + You should consider when you use the ``populate`` keyword argument as populating the AutoQueue on every + request could potentially lead to a large amount of tracks being populated. + max_populate: int + The maximum amount of tracks that should be added to the AutoQueue when the ``populate`` keyword argument is + set to ``True``. This is NOT the exact amount of tracks that will be added. You should set this to a lower + amount to avoid the AutoQueue from being overfilled. + + This argument has no effect when ``populate`` is set to ``False``. + + Defaults to ``5``. Returns @@ -772,6 +811,11 @@ async def play( Added the ``add_history`` keyword-only argument. Added the ``filters`` keyword-only argument. + + + .. versionchanged:: 3.3.0 + + Added the ``populate`` keyword-only argument. """ assert self.guild is not None @@ -789,11 +833,7 @@ async def play( self._previous = self._current self.queue._loaded = track - pause: bool - if paused is not None: - pause = paused - else: - pause = self._paused + pause: bool = paused if paused is not None else self._paused if filters: self._filters = filters @@ -823,6 +863,9 @@ async def play( assert self.queue.history is not None self.queue.history.put(track) + if populate: + await self._do_recommendation(populate_track=track, max_population=max_populate) + return track async def pause(self, value: bool, /) -> None: diff --git a/wavelink/queue.py b/wavelink/queue.py index 121cdc6a..46a84432 100644 --- a/wavelink/queue.py +++ b/wavelink/queue.py @@ -21,6 +21,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ + from __future__ import annotations import asyncio @@ -33,6 +34,7 @@ from .exceptions import QueueEmpty from .tracks import Playable, Playlist + __all__ = ("Queue",) @@ -170,12 +172,10 @@ def __bool__(self) -> bool: return bool(self._items) @overload - def __getitem__(self, __index: SupportsIndex, /) -> Playable: - ... + def __getitem__(self, __index: SupportsIndex, /) -> Playable: ... @overload - def __getitem__(self, __index: slice, /) -> list[Playable]: - ... + def __getitem__(self, __index: slice, /) -> list[Playable]: ... def __getitem__(self, __index: SupportsIndex | slice, /) -> Playable | list[Playable]: return self._items[__index] @@ -353,7 +353,7 @@ async def get_wait(self) -> Playable: try: await waiter - except: # noqa + except: waiter.cancel() try: diff --git a/wavelink/tracks.py b/wavelink/tracks.py index f9926dbf..c8571488 100644 --- a/wavelink/tracks.py +++ b/wavelink/tracks.py @@ -21,9 +21,9 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ + from __future__ import annotations -from collections.abc import Iterator from typing import TYPE_CHECKING, Any, TypeAlias, overload import yarl @@ -33,7 +33,10 @@ from .enums import TrackSource from .utils import ExtrasNamespace + if TYPE_CHECKING: + from collections.abc import Iterator + from .types.tracks import ( PlaylistInfoPayload, PlaylistPayload, @@ -533,12 +536,10 @@ def __len__(self) -> int: return len(self.tracks) @overload - def __getitem__(self, index: int) -> Playable: - ... + def __getitem__(self, index: int) -> Playable: ... @overload - def __getitem__(self, index: slice) -> list[Playable]: - ... + def __getitem__(self, index: slice) -> list[Playable]: ... def __getitem__(self, index: int | slice) -> Playable | list[Playable]: return self.tracks[index] diff --git a/wavelink/types/filters.py b/wavelink/types/filters.py index 08a07f51..49997c22 100644 --- a/wavelink/types/filters.py +++ b/wavelink/types/filters.py @@ -21,6 +21,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ + from typing import Any, TypedDict diff --git a/wavelink/types/request.py b/wavelink/types/request.py index 8a4b9775..225211a7 100644 --- a/wavelink/types/request.py +++ b/wavelink/types/request.py @@ -21,10 +21,12 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ + from __future__ import annotations from typing import TYPE_CHECKING, Any, TypeAlias, TypedDict + if TYPE_CHECKING: from typing_extensions import NotRequired diff --git a/wavelink/types/response.py b/wavelink/types/response.py index 4ef88e21..3b7ed305 100644 --- a/wavelink/types/response.py +++ b/wavelink/types/response.py @@ -21,15 +21,15 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ -from typing import TYPE_CHECKING, Literal, TypedDict -if TYPE_CHECKING: - from typing_extensions import Never, NotRequired +from typing import Literal, TypedDict - from .filters import FilterPayload - from .state import PlayerState - from .stats import CPUStats, FrameStats, MemoryStats - from .tracks import PlaylistPayload, TrackPayload +from typing_extensions import Never, NotRequired + +from .filters import FilterPayload +from .state import PlayerState +from .stats import CPUStats, FrameStats, MemoryStats +from .tracks import PlaylistPayload, TrackPayload class ErrorResponse(TypedDict): diff --git a/wavelink/types/state.py b/wavelink/types/state.py index 03f09269..2e819dd7 100644 --- a/wavelink/types/state.py +++ b/wavelink/types/state.py @@ -21,10 +21,10 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ -from typing import TYPE_CHECKING, TypedDict -if TYPE_CHECKING: - from typing_extensions import NotRequired +from typing import TypedDict + +from typing_extensions import NotRequired class PlayerState(TypedDict): diff --git a/wavelink/types/stats.py b/wavelink/types/stats.py index 1261b5d8..ef9d089a 100644 --- a/wavelink/types/stats.py +++ b/wavelink/types/stats.py @@ -21,6 +21,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ + from typing import TypedDict diff --git a/wavelink/types/tracks.py b/wavelink/types/tracks.py index 4825abaf..8be1c2e2 100644 --- a/wavelink/types/tracks.py +++ b/wavelink/types/tracks.py @@ -21,10 +21,10 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ -from typing import TYPE_CHECKING, Any, TypedDict -if TYPE_CHECKING: - from typing_extensions import NotRequired +from typing import Any, TypedDict + +from typing_extensions import NotRequired class TrackInfoPayload(TypedDict): diff --git a/wavelink/types/websocket.py b/wavelink/types/websocket.py index f5bb9d02..a079eb97 100644 --- a/wavelink/types/websocket.py +++ b/wavelink/types/websocket.py @@ -21,10 +21,12 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ + from __future__ import annotations from typing import TYPE_CHECKING, Literal, TypeAlias, TypedDict + if TYPE_CHECKING: from typing_extensions import NotRequired diff --git a/wavelink/utils.py b/wavelink/utils.py index a3c85f32..bb25444c 100644 --- a/wavelink/utils.py +++ b/wavelink/utils.py @@ -21,8 +21,11 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ + +from collections.abc import Iterator from types import SimpleNamespace -from typing import Any, Iterator +from typing import Any + __all__ = ( "Namespace", diff --git a/wavelink/websocket.py b/wavelink/websocket.py index 4cd068ce..950c3b01 100644 --- a/wavelink/websocket.py +++ b/wavelink/websocket.py @@ -21,6 +21,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ + from __future__ import annotations import asyncio @@ -36,6 +37,7 @@ from .payloads import * from .tracks import Playable + if TYPE_CHECKING: from .node import Node from .player import Player @@ -93,8 +95,8 @@ async def connect(self) -> None: self.keep_alive_task.cancel() except Exception as e: logger.debug( - "Failed to cancel websocket keep alive while connecting. " - f"This is most likely not a problem and will not affect websocket connection: '{e}'" + "Failed to cancel websocket keep alive while connecting. This is most likely not a problem and will not affect websocket connection: '%s'", + e, ) retries: int | None = self.node._retries @@ -115,8 +117,10 @@ async def connect(self) -> None: raise NodeException from e else: logger.warning( - f'An unexpected error occurred while connecting {self.node!r} to Lavalink: "{e}"\n' - f"If this error persists or wavelink is unable to reconnect, please see: {github}" + 'An unexpected error occurred while connecting %r to Lavalink: "%s"\nIf this error persists or wavelink is unable to reconnect, please see: %s', + self.node, + e, + github, ) if self.is_connected(): @@ -125,8 +129,9 @@ async def connect(self) -> None: if retries == 0: logger.warning( - f"{self.node!r} was unable to successfully connect/reconnect to Lavalink after " - f'"{retries + 1}" connection attempt. This Node has exhausted the retry count.' + '%r was unable to successfully connect/reconnect to Lavalink after "%s" connection attempt. This Node has exhausted the retry count.', + self.node, + retries + 1, ) await self.cleanup() @@ -136,7 +141,7 @@ async def connect(self) -> None: retries -= 1 delay: float = self.backoff.calculate() - logger.info(f'{self.node!r} retrying websocket connection in "{delay}" seconds.') + logger.info('%r retrying websocket connection in "%s" seconds.', self.node, delay) await asyncio.sleep(delay) @@ -245,7 +250,7 @@ async def keep_alive(self) -> None: other_payload: ExtraEventPayload = ExtraEventPayload(node=self.node, player=player, data=data) self.dispatch("extra_event", other_payload) else: - logger.debug(f"'Received an unknown OP from Lavalink '{data['op']}'. Disregarding.") + logger.debug("'Received an unknown OP from Lavalink '%s'. Disregarding.", data["op"]) def get_player(self, guild_id: str | int) -> Player | None: return self.node.get_player(int(guild_id)) @@ -254,19 +259,19 @@ def dispatch(self, event: str, /, *args: Any, **kwargs: Any) -> None: assert self.node.client is not None self.node.client.dispatch(f"wavelink_{event}", *args, **kwargs) - logger.debug(f"{self.node!r} dispatched the event 'on_wavelink_{event}'") + logger.debug("%r dispatched the event 'on_wavelink_%s'", self.node, event) async def cleanup(self) -> None: if self.keep_alive_task: try: self.keep_alive_task.cancel() - except: + except Exception: pass if self.socket: try: await self.socket.close() - except: + except Exception: pass self.node._status = NodeStatus.DISCONNECTED @@ -275,4 +280,4 @@ async def cleanup(self) -> None: self.node._websocket = None - logger.debug(f"Successfully cleaned up the websocket for {self.node!r}") + logger.debug("Successfully cleaned up the websocket for %r", self.node)