Skip to content

Commit

Permalink
add HasLength layer to fix dodgy workaround ItemLimiter (#78)
Browse files Browse the repository at this point in the history
* add HasLength layer to fix dodgy workaround ItemLimiter

* update release history
  • Loading branch information
geo-martino authored May 25, 2024
1 parent f65b07c commit e83c08d
Show file tree
Hide file tree
Showing 20 changed files with 64 additions and 50 deletions.
2 changes: 2 additions & 0 deletions docs/release-history.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
-------
Expand Down
18 changes: 15 additions & 3 deletions musify/core/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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__ = ()
Expand Down Expand Up @@ -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__ = ()
Expand All @@ -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
4 changes: 2 additions & 2 deletions musify/file/base.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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__ = ()
Expand Down
4 changes: 2 additions & 2 deletions musify/libraries/core/collection.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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__ = ()
Expand Down
4 changes: 2 additions & 2 deletions musify/libraries/core/object.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__ = ()
Expand Down
12 changes: 6 additions & 6 deletions musify/processors/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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",)
Expand Down Expand Up @@ -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",)
Expand Down
4 changes: 2 additions & 2 deletions musify/processors/limit.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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]
Expand Down
4 changes: 2 additions & 2 deletions tests/core/base.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from abc import ABCMeta, abstractmethod
from abc import ABC, abstractmethod

import pytest

Expand All @@ -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
Expand Down
6 changes: 3 additions & 3 deletions tests/core/enum.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
8 changes: 4 additions & 4 deletions tests/libraries/core/collection.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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.
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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.
Expand Down
4 changes: 2 additions & 2 deletions tests/libraries/local/library/testers.py
Original file line number Diff line number Diff line change
@@ -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
4 changes: 2 additions & 2 deletions tests/libraries/local/playlist/testers.py
Original file line number Diff line number Diff line change
@@ -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
4 changes: 2 additions & 2 deletions tests/libraries/local/track/testers.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from abc import ABCMeta
from abc import ABC
from collections.abc import Iterable, Collection
from random import randrange, sample

Expand All @@ -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]:
Expand Down
4 changes: 2 additions & 2 deletions tests/libraries/remote/core/library.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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]:
Expand Down
6 changes: 3 additions & 3 deletions tests/libraries/remote/core/object.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from abc import ABCMeta, abstractmethod
from abc import ABC, abstractmethod
from collections.abc import Iterable
from typing import Any

Expand All @@ -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]:
Expand Down Expand Up @@ -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:
Expand Down
4 changes: 2 additions & 2 deletions tests/libraries/remote/core/processors/check.py
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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
Expand Down
4 changes: 2 additions & 2 deletions tests/libraries/remote/core/processors/search.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand Down
4 changes: 2 additions & 2 deletions tests/libraries/remote/spotify/object/testers.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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]:
Expand Down
10 changes: 5 additions & 5 deletions tests/log/test_logger.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
Loading

0 comments on commit e83c08d

Please sign in to comment.