Add support for Python 3.12 + fix issues with type checking (#1071)
authorMarcel van der Veldt <m.vanderveldt@outlook.com>
Fri, 9 Feb 2024 10:27:19 +0000 (11:27 +0100)
committerGitHub <noreply@github.com>
Fri, 9 Feb 2024 10:27:19 +0000 (11:27 +0100)
* Add support for python version 3.12

* Fix issues with imports that need to be type inspected

music_assistant/common/models/config_entries.py
music_assistant/common/models/player_queue.py
music_assistant/server/controllers/media/playlists.py
music_assistant/server/controllers/music.py
music_assistant/server/controllers/player_queues.py
music_assistant/server/controllers/streams.py
music_assistant/server/helpers/util.py
music_assistant/server/providers/ytmusic/__init__.py
music_assistant/server/server.py
pyproject.toml

index f348cb862728fc98d6d28e540f66c89869760290..d14b4d09e92510f6c0a17340bf5c45f82afbbc6c 100644 (file)
@@ -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
index 48a455decafd7053bbe55678d27a25fdb9a99681..ed3e28bb3467a0666e5e9554ddf81fb8272af94d 100644 (file)
@@ -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
index 18adbf867fbaa647387f23424d34f8ccf0204a65..a7a8016431ef65f5c15244c45dc7c9036297cc31 100644 (file)
@@ -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
 
index b4db9e12ec7223af569f725131ed919fc66eb188..86cba133c4d834c7d523c57e9700e30cbd022114 100644 (file)
@@ -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)
index 99ce19a451c8688139e126f8775f117e4b98ec0b..cb4dc61ab1f64d424c097c2beee18bc48373f325 100644 (file)
@@ -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
index d85be427542c4471f5787845c3869b814630117a..4ee75d097156f78d822afba971fc14afff8f44bb 100644 (file)
@@ -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
index 0d00fb96de545806d29f8d634baac2a731d92ba5..33b9eacb4b9f442c8af709b5cc5b07245f65a6be 100644 (file)
@@ -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)
 
 
index 9701d639504a8993ac0f0e1cfad28c81a802cf80..ae05fed5ad8f79301258c3a1f833ab314a215e03 100644 (file)
@@ -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
index 32d21515df2567b862aa607d6c771335586a04fc..0c80804843e65bff6cb48ec8869f375b4c7941eb 100644 (file)
@@ -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."
index 4a959f522f839a7c152d8667f4b24aeaac0d087c..3507eafc9742a9821af5c268506bcec4bcd13fc1 100644 (file)
@@ -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"]