diff --git a/docs/release-history.rst b/docs/release-history.rst index 02e74d0a..4f125dd7 100644 --- a/docs/release-history.rst +++ b/docs/release-history.rst @@ -80,6 +80,8 @@ Fixed to have lower case paths * :py:meth:`.RemoteLibrary.restore_playlists` now correctly handles the backup output from :py:meth:`.RemoteLibrary.backup_playlists` +* Issue detecting stdout_handlers affecting :py:meth:`.MusifyLogger.print` and :py:meth:`.MusifyLogger.get_iterator`. + Now works as expected. Removed ------- diff --git a/musify/core/base.py b/musify/core/base.py index 41938c69..cd533e3e 100644 --- a/musify/core/base.py +++ b/musify/core/base.py @@ -3,7 +3,7 @@ """ from __future__ import annotations -from abc import ABCMeta, abstractmethod +from abc import ABC, abstractmethod from collections.abc import Hashable from typing import Any @@ -40,7 +40,7 @@ def __gt__(self, other: MusifyObject): return self.name > other.name -class MusifyItem(MusifyObject, Hashable, metaclass=ABCMeta): +class MusifyItem(MusifyObject, Hashable, ABC): """Generic class for storing an item.""" __slots__ = () @@ -76,7 +76,7 @@ def __getitem__(self, key: str) -> Any: # noinspection PyPropertyDefinition -class MusifyItemSettable(MusifyItem, metaclass=ABCMeta): +class MusifyItemSettable(MusifyItem, ABC): """Generic class for storing an item that can have select properties modified.""" __slots__ = () @@ -92,3 +92,15 @@ def _uri_setter(self, value: str | None): raise NotImplementedError uri = property(lambda self: self._uri_getter(), lambda self, v: self._uri_setter(v)) + + +class HasLength(ABC): + """Simple protocol for an object which has a length""" + + __slots__ = () + + @property + @abstractmethod + def length(self): + """Total duration of this object in seconds""" + raise NotImplementedError diff --git a/musify/file/base.py b/musify/file/base.py index 16cf1d32..0652d829 100644 --- a/musify/file/base.py +++ b/musify/file/base.py @@ -1,7 +1,7 @@ """ Generic base classes and functions for file operations. """ -from abc import ABCMeta, abstractmethod +from abc import ABC, abstractmethod from collections.abc import Hashable from datetime import datetime from glob import glob @@ -11,7 +11,7 @@ from musify.file.exception import InvalidFileType, FileDoesNotExistError -class File(Hashable, metaclass=ABCMeta): +class File(Hashable, ABC): """Generic class for representing a file on a system.""" __slots__ = () diff --git a/musify/libraries/core/collection.py b/musify/libraries/core/collection.py index c199092a..7b89a9b9 100644 --- a/musify/libraries/core/collection.py +++ b/musify/libraries/core/collection.py @@ -8,7 +8,7 @@ from dataclasses import dataclass from typing import Any, SupportsIndex, Self -from musify.core.base import MusifyObject, MusifyItem +from musify.core.base import MusifyObject, MusifyItem, HasLength from musify.core.enum import Field from musify.exception import MusifyTypeError, MusifyKeyError, MusifyAttributeError from musify.file.base import File @@ -105,7 +105,7 @@ def get_value_from_item(self, item: RemoteObject) -> str: return item.url_ext -class MusifyCollection[T: MusifyItem](MusifyObject, MutableSequence[T], ABC): +class MusifyCollection[T: MusifyItem](MusifyObject, HasLength, MutableSequence[T], ABC): """Generic class for storing a collection of musify items.""" __slots__ = () diff --git a/musify/libraries/core/object.py b/musify/libraries/core/object.py index e549c2d5..6fe995a8 100644 --- a/musify/libraries/core/object.py +++ b/musify/libraries/core/object.py @@ -9,13 +9,13 @@ from copy import deepcopy from typing import Self -from musify.core.base import MusifyItem +from musify.core.base import MusifyItem, HasLength from musify.exception import MusifyTypeError from musify.libraries.core.collection import MusifyCollection from musify.libraries.remote.core.enum import RemoteObjectType -class Track(MusifyItem, ABC): +class Track(MusifyItem, HasLength, ABC): """Represents a track including its metadata/tags/properties.""" __slots__ = () diff --git a/musify/processors/base.py b/musify/processors/base.py index 11e07e45..009135e2 100644 --- a/musify/processors/base.py +++ b/musify/processors/base.py @@ -2,7 +2,7 @@ Base classes for all processors in this module. Also contains decorators for use in implementations. """ import logging -from abc import ABCMeta, abstractmethod +from abc import ABC, abstractmethod from collections.abc import Mapping, Callable, Collection, Iterable, MutableSequence from functools import partial, update_wrapper from typing import Any, Optional @@ -13,13 +13,13 @@ from musify.utils import get_user_input, get_max_width, align_string -class Processor(PrettyPrinter, metaclass=ABCMeta): +class Processor(PrettyPrinter, ABC): """Generic base class for processors""" __slots__ = () -class InputProcessor(Processor, metaclass=ABCMeta): +class InputProcessor(Processor, ABC): """ Processor that gets user input as part of it processing. @@ -81,7 +81,7 @@ def __call__(self, *args, **kwargs) -> Any: # noinspection SpellCheckingInspection -class DynamicProcessor(Processor, metaclass=ABCMeta): +class DynamicProcessor(Processor, ABC): """ Base class for implementations with :py:func:`dynamicprocessormethod` methods. @@ -159,7 +159,7 @@ def __call__(self, *args, **kwargs) -> Any: return self._processor_method(*args, **kwargs) -class Filter[T](Processor, metaclass=ABCMeta): +class Filter[T](Processor, ABC): """Base class for filtering down values based on some settings""" __slots__ = ("_transform",) @@ -197,7 +197,7 @@ def __bool__(self): return self.ready -class FilterComposite[T](Filter[T], Collection[Filter], metaclass=ABCMeta): +class FilterComposite[T](Filter[T], Collection[Filter], ABC): """Composite filter which filters based on many :py:class:`Filter` objects""" __slots__ = ("filters",) diff --git a/musify/processors/limit.py b/musify/processors/limit.py index 8169ad7b..22f03e24 100644 --- a/musify/processors/limit.py +++ b/musify/processors/limit.py @@ -6,7 +6,7 @@ from operator import mul from random import shuffle -from musify.core.base import MusifyItem +from musify.core.base import MusifyItem, HasLength from musify.core.enum import MusifyEnum, Fields from musify.file.base import File from musify.libraries.core.object import Track @@ -149,7 +149,7 @@ def _convert(self, item: MusifyItem) -> float: :raise ItemLimiterError: When the given limit type cannot be found """ if 10 < self.kind.value < 20: - if getattr(item, "length", None) is None: # TODO: there should be a better way of handling this... + if not isinstance(item, HasLength): raise LimiterProcessorError("The given item cannot be limited on length as it does not have a length.") factors = (1, 60, 60, 24, 7)[:self.kind.value % 10] diff --git a/tests/core/base.py b/tests/core/base.py index da0b3a8f..0acdf815 100644 --- a/tests/core/base.py +++ b/tests/core/base.py @@ -1,4 +1,4 @@ -from abc import ABCMeta, abstractmethod +from abc import ABC, abstractmethod import pytest @@ -7,7 +7,7 @@ from tests.core.printer import PrettyPrinterTester -class MusifyItemTester(PrettyPrinterTester, metaclass=ABCMeta): +class MusifyItemTester(PrettyPrinterTester, ABC): """Run generic tests for :py:class:`MusifyItem` implementations""" @abstractmethod diff --git a/tests/core/enum.py b/tests/core/enum.py index 157fd72a..7a45603e 100644 --- a/tests/core/enum.py +++ b/tests/core/enum.py @@ -1,4 +1,4 @@ -from abc import ABC, abstractmethod, ABCMeta +from abc import ABC, abstractmethod from collections.abc import Container from musify.core.base import MusifyObject @@ -24,7 +24,7 @@ def test_gets_enum_from_name_and_value(self): assert self.cls.from_value(*all_enums, fail_on_many=False) == set(all_enums) -class FieldTester(EnumTester, metaclass=ABCMeta): +class FieldTester(EnumTester, ABC): """Run generic tests for :py:class:`Field` enum implementations""" @abstractmethod @@ -64,7 +64,7 @@ def test_all_fields_are_valid(self, reference_cls: type[MusifyObject], reference assert isinstance(getattr(reference_cls, name), property) -class TagFieldTester(FieldTester, metaclass=ABCMeta): +class TagFieldTester(FieldTester, ABC): """Run generic tests for :py:class:`TagField` enum implementations""" @property diff --git a/tests/libraries/core/collection.py b/tests/libraries/core/collection.py index 7d45a6e6..75f2fe1d 100644 --- a/tests/libraries/core/collection.py +++ b/tests/libraries/core/collection.py @@ -1,4 +1,4 @@ -from abc import ABCMeta, abstractmethod +from abc import ABC, abstractmethod from collections.abc import Iterable from copy import deepcopy from random import sample @@ -17,7 +17,7 @@ from tests.core.printer import PrettyPrinterTester -class MusifyCollectionTester(PrettyPrinterTester, metaclass=ABCMeta): +class MusifyCollectionTester(PrettyPrinterTester, ABC): """ Run generic tests for :py:class:`MusifyCollection` implementations. The collection must have 3 or more items and all items must be unique. @@ -210,7 +210,7 @@ def test_collection_difference_and_intersection( assert collection.intersection(other) == collection.items -class PlaylistTester(MusifyCollectionTester, metaclass=ABCMeta): +class PlaylistTester(MusifyCollectionTester, ABC): @abstractmethod def playlist(self, *args, **kwargs) -> Playlist: @@ -291,7 +291,7 @@ def test_merge_dunder_methods[T: Track](playlist: Playlist[T], collection_merge_ assert playlist[initial_count:] == other.items -class LibraryTester(MusifyCollectionTester, metaclass=ABCMeta): +class LibraryTester(MusifyCollectionTester, ABC): """ Run generic tests for :py:class:`Library` implementations. The collection must have 3 or more playlists and all playlists must be unique. diff --git a/tests/libraries/local/library/testers.py b/tests/libraries/local/library/testers.py index 03d0d02c..9e0088db 100644 --- a/tests/libraries/local/library/testers.py +++ b/tests/libraries/local/library/testers.py @@ -1,8 +1,8 @@ -from abc import ABCMeta +from abc import ABC from tests.libraries.core.collection import LibraryTester from tests.libraries.local.track.testers import LocalCollectionTester -class LocalLibraryTester(LibraryTester, LocalCollectionTester, metaclass=ABCMeta): +class LocalLibraryTester(LibraryTester, LocalCollectionTester, ABC): pass diff --git a/tests/libraries/local/playlist/testers.py b/tests/libraries/local/playlist/testers.py index 1ba39010..d766be4f 100644 --- a/tests/libraries/local/playlist/testers.py +++ b/tests/libraries/local/playlist/testers.py @@ -1,8 +1,8 @@ -from abc import ABCMeta +from abc import ABC from tests.libraries.core.collection import PlaylistTester from tests.libraries.local.track.testers import LocalCollectionTester -class LocalPlaylistTester(PlaylistTester, LocalCollectionTester, metaclass=ABCMeta): +class LocalPlaylistTester(PlaylistTester, LocalCollectionTester, ABC): pass diff --git a/tests/libraries/local/track/testers.py b/tests/libraries/local/track/testers.py index a25d7dcb..3a1ff83a 100644 --- a/tests/libraries/local/track/testers.py +++ b/tests/libraries/local/track/testers.py @@ -1,4 +1,4 @@ -from abc import ABCMeta +from abc import ABC from collections.abc import Iterable, Collection from random import randrange, sample @@ -13,7 +13,7 @@ from tests.libraries.remote.spotify.api.mock import SpotifyMock -class LocalCollectionTester(MusifyCollectionTester, metaclass=ABCMeta): +class LocalCollectionTester(MusifyCollectionTester, ABC): @pytest.fixture def collection_merge_items(self) -> Iterable[LocalTrack]: diff --git a/tests/libraries/remote/core/library.py b/tests/libraries/remote/core/library.py index df71a27b..b193b5ea 100644 --- a/tests/libraries/remote/core/library.py +++ b/tests/libraries/remote/core/library.py @@ -1,5 +1,5 @@ import re -from abc import ABCMeta, abstractmethod +from abc import ABC, abstractmethod from collections.abc import Collection, Mapping from copy import copy, deepcopy from random import choice @@ -17,7 +17,7 @@ from tests.libraries.remote.core.utils import RemoteMock -class RemoteLibraryTester(RemoteCollectionTester, LibraryTester, metaclass=ABCMeta): +class RemoteLibraryTester(RemoteCollectionTester, LibraryTester, ABC): @abstractmethod def collection_merge_items(self, *args, **kwargs) -> list[RemoteTrack]: diff --git a/tests/libraries/remote/core/object.py b/tests/libraries/remote/core/object.py index f52561f9..b3466683 100644 --- a/tests/libraries/remote/core/object.py +++ b/tests/libraries/remote/core/object.py @@ -1,4 +1,4 @@ -from abc import ABCMeta, abstractmethod +from abc import ABC, abstractmethod from collections.abc import Iterable from typing import Any @@ -15,7 +15,7 @@ from tests.libraries.remote.core.utils import RemoteMock -class RemoteCollectionTester(MusifyCollectionTester, metaclass=ABCMeta): +class RemoteCollectionTester(MusifyCollectionTester, ABC): @abstractmethod def collection_merge_items(self, *args, **kwargs) -> Iterable[RemoteItem]: @@ -60,7 +60,7 @@ def test_collection_getitem_dunder_method( assert collection["this key does not exist"] -class RemotePlaylistTester(RemoteCollectionTester, PlaylistTester, metaclass=ABCMeta): +class RemotePlaylistTester(RemoteCollectionTester, PlaylistTester, ABC): @staticmethod def _get_payload_from_request(request: RequestCall) -> dict[str, Any] | None: diff --git a/tests/libraries/remote/core/processors/check.py b/tests/libraries/remote/core/processors/check.py index e5fbfeee..39d2acc9 100644 --- a/tests/libraries/remote/core/processors/check.py +++ b/tests/libraries/remote/core/processors/check.py @@ -1,5 +1,5 @@ import re -from abc import ABCMeta, abstractmethod +from abc import ABC, abstractmethod from itertools import batched from random import randrange, choice @@ -20,7 +20,7 @@ from tests.utils import random_str, get_stdout -class RemoteItemCheckerTester(PrettyPrinterTester, metaclass=ABCMeta): +class RemoteItemCheckerTester(PrettyPrinterTester, ABC): """Run generic tests for :py:class:`RemoteItemSearcher` implementations.""" @pytest.fixture diff --git a/tests/libraries/remote/core/processors/search.py b/tests/libraries/remote/core/processors/search.py index c82182a4..d5b385b7 100644 --- a/tests/libraries/remote/core/processors/search.py +++ b/tests/libraries/remote/core/processors/search.py @@ -1,4 +1,4 @@ -from abc import ABCMeta, abstractmethod +from abc import ABC, abstractmethod from collections.abc import Iterable, Callable, Awaitable from copy import copy from urllib.parse import unquote @@ -19,7 +19,7 @@ from tests.libraries.remote.core.utils import RemoteMock -class RemoteItemSearcherTester(PrettyPrinterTester, metaclass=ABCMeta): +class RemoteItemSearcherTester(PrettyPrinterTester, ABC): """Run generic tests for :py:class:`RemoteItemSearcher` implementations.""" @pytest.fixture diff --git a/tests/libraries/remote/spotify/object/testers.py b/tests/libraries/remote/spotify/object/testers.py index 35a7651c..277dfa9f 100644 --- a/tests/libraries/remote/spotify/object/testers.py +++ b/tests/libraries/remote/spotify/object/testers.py @@ -1,4 +1,4 @@ -from abc import ABCMeta, abstractmethod +from abc import ABC, abstractmethod from collections.abc import Iterable from typing import Any from urllib.parse import unquote @@ -13,7 +13,7 @@ from tests.libraries.remote.spotify.api.mock import SpotifyMock -class SpotifyCollectionLoaderTester(RemoteCollectionTester, metaclass=ABCMeta): +class SpotifyCollectionLoaderTester(RemoteCollectionTester, ABC): @abstractmethod def collection_merge_items(self, *args, **kwargs) -> Iterable[SpotifyItem]: diff --git a/tests/log/test_logger.py b/tests/log/test_logger.py index e2ed6617..b48484ff 100644 --- a/tests/log/test_logger.py +++ b/tests/log/test_logger.py @@ -33,15 +33,15 @@ def logger() -> MusifyLogger: def test_print(logger: MusifyLogger, capfd: pytest.CaptureFixture): handler = logging.StreamHandler(sys.stdout) handler.setLevel(logging.WARNING) - logger.addHandler(handler) + logging._handlers[handler.name] = handler - logger.print(logging.ERROR) # ERROR is above handler level, print line + logger.print(logging.ERROR) # ERROR is above handler level assert capfd.readouterr().out == '\n' - logger.print(logging.WARNING) # WARNING is below handler level, print line + logger.print(logging.WARNING) # WARNING is at handler level assert capfd.readouterr().out == '\n' - logger.print(logging.INFO) # INFO is below handler level, don't print line + logger.print(logging.INFO) # INFO is below handler level assert capfd.readouterr().out == '' # compact is True, never print lines @@ -76,7 +76,7 @@ def test_file_paths(logger: MusifyLogger): def test_getting_iterator_as_progress_bar(logger: MusifyLogger): handler = logging.StreamHandler(sys.stdout) handler.setLevel(logging.DEBUG) # forces leave to be False - logger.addHandler(handler) + logging._handlers[handler.name] = handler logger._bars.clear() bar = logger.get_iterator(iterable=range(0, 50), initial=10, disable=True, file=sys.stderr) diff --git a/tests/processors/test_filter.py b/tests/processors/test_filter.py index 544c55ee..c7309f2c 100644 --- a/tests/processors/test_filter.py +++ b/tests/processors/test_filter.py @@ -1,4 +1,4 @@ -from abc import ABCMeta +from abc import ABC from random import sample, shuffle, randrange import pytest @@ -16,7 +16,7 @@ from tests.utils import random_str, path_resources -class FilterTester(PrettyPrinterTester, metaclass=ABCMeta): +class FilterTester(PrettyPrinterTester, ABC): pass