From bf6999d4d97830efd09aff6c1b07762d03b4d45b Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Fri, 9 Feb 2024 11:27:19 +0100 Subject: [PATCH] Add support for Python 3.12 + fix issues with type checking (#1071) * Add support for python version 3.12 * Fix issues with imports that need to be type inspected --- .../common/models/config_entries.py | 9 ++--- music_assistant/common/models/player_queue.py | 9 ++--- .../server/controllers/media/playlists.py | 20 +++++++---- music_assistant/server/controllers/music.py | 25 ++++++++++---- .../server/controllers/player_queues.py | 18 +++++++--- music_assistant/server/controllers/streams.py | 3 +- music_assistant/server/helpers/util.py | 6 ++-- .../server/providers/ytmusic/__init__.py | 3 +- music_assistant/server/server.py | 33 ++++++++++++------- pyproject.toml | 5 +-- 10 files changed, 80 insertions(+), 51 deletions(-) diff --git a/music_assistant/common/models/config_entries.py b/music_assistant/common/models/config_entries.py index f348cb86..d14b4d09 100644 --- a/music_assistant/common/models/config_entries.py +++ b/music_assistant/common/models/config_entries.py @@ -3,12 +3,14 @@ from __future__ import annotations import logging +from collections.abc import Iterable # noqa: TCH003 from dataclasses import dataclass from types import NoneType -from typing import TYPE_CHECKING, Any +from typing import Any from mashumaro import DataClassDictMixin +from music_assistant.common.models.enums import ProviderType # noqa: TCH001 from music_assistant.constants import ( CONF_AUTO_PLAY, CONF_CROSSFADE, @@ -27,11 +29,6 @@ from music_assistant.constants import ( from .enums import ConfigEntryType -if TYPE_CHECKING: - from collections.abc import Iterable - - from music_assistant.common.models.enums import ProviderType - LOGGER = logging.getLogger(__name__) ENCRYPT_CALLBACK: callable[[str], str] | None = None diff --git a/music_assistant/common/models/player_queue.py b/music_assistant/common/models/player_queue.py index 48a455de..ed3e28bb 100644 --- a/music_assistant/common/models/player_queue.py +++ b/music_assistant/common/models/player_queue.py @@ -4,16 +4,13 @@ from __future__ import annotations import time from dataclasses import dataclass, field -from typing import TYPE_CHECKING from mashumaro import DataClassDictMixin -from .enums import PlayerState, RepeatMode - -if TYPE_CHECKING: - from music_assistant.common.models.media_items import MediaItemType +from music_assistant.common.models.media_items import MediaItemType # noqa: TCH001 - from .queue_item import QueueItem +from .enums import PlayerState, RepeatMode +from .queue_item import QueueItem # noqa: TCH001 @dataclass diff --git a/music_assistant/server/controllers/media/playlists.py b/music_assistant/server/controllers/media/playlists.py index 18adbf86..a7a80164 100644 --- a/music_assistant/server/controllers/media/playlists.py +++ b/music_assistant/server/controllers/media/playlists.py @@ -5,7 +5,8 @@ from __future__ import annotations import asyncio import random import time -from typing import TYPE_CHECKING, Any +from collections.abc import AsyncGenerator # noqa: TCH003 +from typing import Any from music_assistant.common.helpers.datetime import utc_timestamp from music_assistant.common.helpers.json import serialize_to_json @@ -17,15 +18,17 @@ from music_assistant.common.models.errors import ( ProviderUnavailableError, UnsupportedFeaturedException, ) -from music_assistant.common.models.media_items import ItemMapping, Playlist, PlaylistTrack, Track +from music_assistant.common.models.media_items import ( + ItemMapping, + Playlist, + PlaylistTrack, + Track, +) from music_assistant.constants import DB_TABLE_PLAYLISTS from music_assistant.server.helpers.compare import compare_strings from .base import MediaControllerBase -if TYPE_CHECKING: - from collections.abc import AsyncGenerator - class PlaylistController(MediaControllerBase[Playlist]): """Controller managing MediaItems of type Playlist.""" @@ -142,7 +145,10 @@ class PlaylistController(MediaControllerBase[Playlist]): return library_item async def tracks( - self, item_id: str, provider_instance_id_or_domain: str, force_refresh: bool = False + self, + item_id: str, + provider_instance_id_or_domain: str, + force_refresh: bool = False, ) -> AsyncGenerator[PlaylistTrack, None]: """Return playlist tracks for the given provider playlist id.""" playlist = await self.get( @@ -152,7 +158,7 @@ class PlaylistController(MediaControllerBase[Playlist]): async for track in self._get_provider_playlist_tracks( prov.item_id, prov.provider_instance, - cache_checksum=str(time.time()) if force_refresh else playlist.metadata.checksum, + cache_checksum=(str(time.time()) if force_refresh else playlist.metadata.checksum), ): yield track diff --git a/music_assistant/server/controllers/music.py b/music_assistant/server/controllers/music.py index b4db9e12..86cba133 100644 --- a/music_assistant/server/controllers/music.py +++ b/music_assistant/server/controllers/music.py @@ -6,6 +6,7 @@ import asyncio import os import shutil import statistics +from collections.abc import AsyncGenerator # noqa: TCH003 from contextlib import suppress from itertools import zip_longest from typing import TYPE_CHECKING @@ -23,7 +24,11 @@ from music_assistant.common.models.enums import ( ProviderType, ) from music_assistant.common.models.errors import MediaNotFoundError, MusicAssistantError -from music_assistant.common.models.media_items import BrowseFolder, MediaItemType, SearchResults +from music_assistant.common.models.media_items import ( + BrowseFolder, + MediaItemType, + SearchResults, +) from music_assistant.common.models.provider import SyncTask from music_assistant.constants import ( DB_SCHEMA_VERSION, @@ -49,8 +54,6 @@ from .media.radio import RadioController from .media.tracks import TracksController if TYPE_CHECKING: - from collections.abc import AsyncGenerator - from music_assistant.common.models.config_entries import CoreConfig from music_assistant.server.models.music_provider import MusicProvider @@ -452,7 +455,11 @@ class MusicController(CoreController): for item in result: if item.available: return await self.get_item( - item.media_type, item.item_id, item.provider, lazy=False, add_to_library=True + item.media_type, + item.item_id, + item.provider, + lazy=False, + add_to_library=True, ) return None @@ -462,7 +469,11 @@ class MusicController(CoreController): """List integrated loudness for a track in db.""" await self.database.insert( DB_TABLE_TRACK_LOUDNESS, - {"item_id": item_id, "provider": provider_instance_id_or_domain, "loudness": loudness}, + { + "item_id": item_id, + "provider": provider_instance_id_or_domain, + "loudness": loudness, + }, allow_replace=True, ) @@ -590,7 +601,9 @@ class MusicController(CoreController): self.in_progress_syncs.remove(sync_spec) if task_err := task.exception(): self.logger.warning( - "Sync task for %s completed with errors", provider.name, exc_info=task_err + "Sync task for %s completed with errors", + provider.name, + exc_info=task_err, ) else: self.logger.info("Sync task for %s completed", provider.name) diff --git a/music_assistant/server/controllers/player_queues.py b/music_assistant/server/controllers/player_queues.py index 99ce19a4..cb4dc61a 100644 --- a/music_assistant/server/controllers/player_queues.py +++ b/music_assistant/server/controllers/player_queues.py @@ -5,6 +5,7 @@ from __future__ import annotations import logging import random import time +from collections.abc import AsyncGenerator # noqa: TCH003 from contextlib import suppress from typing import TYPE_CHECKING, Any @@ -38,7 +39,7 @@ from music_assistant.server.helpers.audio import set_stream_details from music_assistant.server.models.core_controller import CoreController if TYPE_CHECKING: - from collections.abc import AsyncGenerator, Iterator + from collections.abc import Iterator from music_assistant.common.models.media_items import Album, Artist, Track from music_assistant.common.models.player import Player @@ -299,7 +300,8 @@ class PlayerQueuesController(CoreController): if option is None: option = QueueOption( await self.mass.config.get_core_config_value( - self.domain, f"default_enqueue_action_{media_item.media_type.value}" + self.domain, + f"default_enqueue_action_{media_item.media_type.value}", ) ) if option == QueueOption.REPLACE_NEXT and queue.state not in ( @@ -676,7 +678,9 @@ class PlayerQueuesController(CoreController): queue_items = [QueueItem.from_dict(x) for x in prev_items] except Exception as err: self.logger.warning( - "Failed to restore the queue(items) for %s - %s", player.display_name, str(err) + "Failed to restore the queue(items) for %s - %s", + player.display_name, + str(err), ) if queue is None: queue = PlayerQueue( @@ -1035,7 +1039,9 @@ class PlayerQueuesController(CoreController): async def _get_artist_tracks(self, artist: Artist) -> list[Track]: """Return tracks for given artist, based on user preference.""" artist_items_conf = self.mass.config.get_raw_core_config_value( - self.domain, CONF_DEFAULT_ENQUEUE_SELECT_ARTIST, ENQUEUE_SELECT_ARTIST_DEFAULT_VALUE + self.domain, + CONF_DEFAULT_ENQUEUE_SELECT_ARTIST, + ENQUEUE_SELECT_ARTIST_DEFAULT_VALUE, ) if artist_items_conf == "library_tracks": # make sure we have an in-library artist @@ -1105,7 +1111,9 @@ class PlayerQueuesController(CoreController): async def _get_album_tracks(self, album: Album) -> list[Track]: """Return tracks for given album, based on user preference.""" album_items_conf = self.mass.config.get_raw_core_config_value( - self.domain, CONF_DEFAULT_ENQUEUE_SELECT_ALBUM, ENQUEUE_SELECT_ALBUM_DEFAULT_VALUE + self.domain, + CONF_DEFAULT_ENQUEUE_SELECT_ALBUM, + ENQUEUE_SELECT_ALBUM_DEFAULT_VALUE, ) if album_items_conf == "library_tracks": # make sure we have an in-library album diff --git a/music_assistant/server/controllers/streams.py b/music_assistant/server/controllers/streams.py index d85be427..4ee75d09 100644 --- a/music_assistant/server/controllers/streams.py +++ b/music_assistant/server/controllers/streams.py @@ -12,6 +12,7 @@ import asyncio import logging import time import urllib.parse +from collections.abc import AsyncGenerator # noqa: TCH003 from contextlib import suppress from typing import TYPE_CHECKING @@ -51,8 +52,6 @@ from music_assistant.server.helpers.webserver import Webserver from music_assistant.server.models.core_controller import CoreController if TYPE_CHECKING: - from collections.abc import AsyncGenerator - from music_assistant.common.models.config_entries import CoreConfig from music_assistant.common.models.player import Player from music_assistant.common.models.player_queue import PlayerQueue diff --git a/music_assistant/server/helpers/util.py b/music_assistant/server/helpers/util.py index 0d00fb96..33b9eacb 100644 --- a/music_assistant/server/helpers/util.py +++ b/music_assistant/server/helpers/util.py @@ -32,13 +32,13 @@ async def install_package(package: str) -> None: """Install package with pip, raise when install failed.""" cmd = f"python3 -m pip install --find-links {HA_WHEELS} {package}" proc = await asyncio.create_subprocess_shell( - cmd, stderr=asyncio.subprocess.PIPE, stdout=asyncio.subprocess.DEVNULL + cmd, stderr=asyncio.subprocess.STDOUT, stdout=asyncio.subprocess.PIPE ) - _, stderr = await proc.communicate() + stdout, _ = await proc.communicate() if proc.returncode != 0: - msg = f"Failed to install package {package}\n{stderr.decode()}" + msg = f"Failed to install package {package}\n{stdout.decode()}" raise RuntimeError(msg) diff --git a/music_assistant/server/providers/ytmusic/__init__.py b/music_assistant/server/providers/ytmusic/__init__.py index 9701d639..ae05fed5 100644 --- a/music_assistant/server/providers/ytmusic/__init__.py +++ b/music_assistant/server/providers/ytmusic/__init__.py @@ -4,9 +4,10 @@ from __future__ import annotations import asyncio import re +from collections.abc import AsyncGenerator # noqa: TCH003 from operator import itemgetter from time import time -from typing import TYPE_CHECKING, AsyncGenerator # noqa: UP035 +from typing import TYPE_CHECKING from urllib.parse import unquote import pytube diff --git a/music_assistant/server/server.py b/music_assistant/server/server.py index 32d21515..0c808048 100644 --- a/music_assistant/server/server.py +++ b/music_assistant/server/server.py @@ -41,16 +41,18 @@ from music_assistant.server.helpers.images import get_icon_string from music_assistant.server.helpers.util import ( get_package_version, get_provider_module, + install_package, is_hass_supervisor, ) +from .models import ProviderInstanceType # noqa: TCH001 + if TYPE_CHECKING: from types import TracebackType from music_assistant.common.models.config_entries import ProviderConfig from music_assistant.server.models.core_controller import CoreController - from .models import ProviderInstanceType EventCallBackType = Callable[[MassEvent], None] EventSubscriptionType = tuple[ @@ -514,13 +516,12 @@ class MusicAssistant: async def __load_provider_manifests(self) -> None: """Preload all available provider manifest files.""" - for dir_str in os.listdir(PROVIDERS_PATH): - dir_path = os.path.join(PROVIDERS_PATH, dir_str) - if not os.path.isdir(dir_path): - continue + + async def load_provider_manifest(provider_domain: str, provider_path: str) -> None: + """Preload all available provider manifest files.""" # get files in subdirectory - for file_str in os.listdir(dir_path): - file_path = os.path.join(dir_path, file_str) + for file_str in os.listdir(provider_path): + file_path = os.path.join(provider_path, file_str) if not os.path.isfile(file_path): continue if file_str != "manifest.json": @@ -529,23 +530,33 @@ class MusicAssistant: provider_manifest = await ProviderManifest.parse(file_path) # check for icon.svg file if not provider_manifest.icon_svg: - icon_path = os.path.join(dir_path, "icon.svg") + icon_path = os.path.join(provider_path, "icon.svg") if os.path.isfile(icon_path): provider_manifest.icon_svg = await get_icon_string(icon_path) # check for dark_icon file if not provider_manifest.icon_svg_dark: - icon_path = os.path.join(dir_path, "icon_dark.svg") + icon_path = os.path.join(provider_path, "icon_dark.svg") if os.path.isfile(icon_path): provider_manifest.icon_svg_dark = await get_icon_string(icon_path) + # install requirements + for requirement in provider_manifest.requirements: + await install_package(requirement) self._provider_manifests[provider_manifest.domain] = provider_manifest - LOGGER.debug("Loaded manifest for provider %s", dir_str) + LOGGER.debug("Loaded manifest for provider %s", provider_manifest.name) except Exception as exc: # pylint: disable=broad-except LOGGER.exception( "Error while loading manifest for provider %s", - dir_str, + provider_domain, exc_info=exc, ) + async with asyncio.TaskGroup() as tg: + for dir_str in os.listdir(PROVIDERS_PATH): + dir_path = os.path.join(PROVIDERS_PATH, dir_str) + if not os.path.isdir(dir_path): + continue + tg.create_task(load_provider_manifest(dir_str, dir_path)) + async def _setup_discovery(self) -> None: """Make this Music Assistant instance discoverable on the network.""" zeroconf_type = "_mass._tcp.local." diff --git a/pyproject.toml b/pyproject.toml index 4a959f52..3507eafc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,3 @@ -[build-system] -requires = ["setuptools~=62.3", "wheel~=0.37.1"] -build-backend = "setuptools.build_meta" - [project] name = "music_assistant" # The version is set by GH action on release @@ -16,6 +12,7 @@ authors = [ classifiers = [ "Environment :: Console", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", ] dependencies = ["aiohttp", "orjson", "mashumaro"] -- 2.34.1