diff --git a/.github/workflows/python-test.yml b/.github/workflows/python-test.yml index a01042e..83da677 100644 --- a/.github/workflows/python-test.yml +++ b/.github/workflows/python-test.yml @@ -20,7 +20,7 @@ jobs: - name: Install package and dependencies run: | python -m pip install --upgrade pip - pip install flake8 + pip install flake8 mypy types-python-dateutil pip install . - name: Lint with flake8 run: | @@ -28,5 +28,7 @@ jobs: flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics + - name: Type checking with mypy + run: mypy --ignore-missing-imports --strict . - name: Test with unittest run: python -m unittest discover -s tests diff --git a/LICENSE b/LICENCE similarity index 98% rename from LICENSE rename to LICENCE index b48bef0..eabfe7f 100644 --- a/LICENSE +++ b/LICENCE @@ -1,4 +1,4 @@ -MIT License +MIT Licence Copyright (c) 2020 kernitus diff --git a/beetsplug/date_wrapper.py b/beetsplug/date_wrapper.py index c840da2..6ab7094 100644 --- a/beetsplug/date_wrapper.py +++ b/beetsplug/date_wrapper.py @@ -1,4 +1,6 @@ import datetime +from typing import Optional + from dateutil import parser @@ -9,7 +11,8 @@ class DateWrapper(datetime.datetime): with the month and day being optional. """ - def __new__(cls, y: int = None, m: int = None, d: int = None, iso_string: str = None): + def __new__(cls, y: Optional[int] = None, m: Optional[int] = None, d: Optional[int] = None, + iso_string: Optional[str] = None): """ Create a new datetime object using a convenience wrapper. Must specify at least one of either year or iso_string. @@ -38,7 +41,8 @@ def today(cls): today = datetime.date.today() return DateWrapper(today.year, today.month, today.day) - def __init__(self, y=None, m=None, d=None, iso_string=None): + def __init__(self, y: Optional[int] = None, m: Optional[int] = None, d: Optional[int] = None, + iso_string: Optional[str] = None): if y is not None: self.y = min(max(y, datetime.MINYEAR), datetime.MAXYEAR) self.m = m if (m is None or 0 < m <= 12) else 1 diff --git a/beetsplug/oldestdate.py b/beetsplug/oldestdate.py index f334665..7ce0f91 100644 --- a/beetsplug/oldestdate.py +++ b/beetsplug/oldestdate.py @@ -1,9 +1,10 @@ +from typing import Optional, Any import mediafile import musicbrainzngs from beets import ui, config from beets.autotag import hooks, TrackInfo -from beets.importer import action -from beets.library import Item +from beets.importer import action, ImportTask, ImportSession +from beets.library import Item, Library from beets.plugins import BeetsPlugin from .date_wrapper import DateWrapper @@ -14,9 +15,12 @@ "https://github.com/kernitus/beets-oldestdate" ) +# Type alias +Recording = dict[str, Any] -# Extract first valid work_id from recording -def _get_work_id_from_recording(recording): + +def _get_work_id_from_recording(recording: Recording) -> Optional[str]: + """Extract first valid work_id from recording""" work_id = None if 'work-relation-list' in recording: @@ -30,8 +34,8 @@ def _get_work_id_from_recording(recording): return work_id -# Returns whether this recording contains at least one of the specified artists -def _contains_artist(recording, artist_ids): +def _contains_artist(recording: Recording, artist_ids: list[str]) -> bool: + """Returns whether this recording contains at least one of the specified artists""" artist_found = False if 'artist-credit' in recording: for artist in recording['artist-credit']: @@ -43,8 +47,8 @@ def _contains_artist(recording, artist_ids): return artist_found -# Extract artist ids from a recording -def _get_artist_ids_from_recording(recording): +def _get_artist_ids_from_recording(recording: Recording) -> list[str]: + """Extract artist ids from a recording""" ids = [] if 'artist-credit' in recording: @@ -56,8 +60,8 @@ def _get_artist_ids_from_recording(recording): return ids -# Returns whether given fetched recording is a cover of a work -def _is_cover(recording): +def _is_cover(recording: Recording) -> bool: + """Returns whether given fetched recording is a cover of a work""" if 'work-relation-list' in recording: for work in recording['work-relation-list']: if 'attribute-list' in work: @@ -66,14 +70,14 @@ def _is_cover(recording): return False -# Fetch work, including recording relations -def _fetch_work(work_id): +def _fetch_work(work_id: str) -> Recording: + """Fetch work, including recording relations""" return musicbrainzngs.get_work_by_id(work_id, ['recording-rels'])['work'] class OldestDatePlugin(BeetsPlugin): - _importing = False - _recordings_cache = dict() + _importing: bool = False + _recordings_cache: dict[str, Recording] = dict() def __init__(self): super(OldestDatePlugin, self).__init__() @@ -126,12 +130,12 @@ def commands(self): recording_date_command.func = self._command_func return [recording_date_command] - # Fetch the recording associated with each candidate - def _import_trackinfo(self, info): + def _import_trackinfo(self, info: TrackInfo) -> None: + """Fetch the recording associated with each candidate""" if 'track_id' in info: self._fetch_recording(info.track_id) - def track_distance(self, _, info: TrackInfo): + def track_distance(self, _: Item, info: TrackInfo) -> hooks.Distance: dist = hooks.Distance() if info.data_source != 'MusicBrainz': self._log.debug('Skipping track with non MusicBrainz data source {0.artist} - {0.title}', info) @@ -141,10 +145,10 @@ def track_distance(self, _, info: TrackInfo): return dist - def _import_task_created(self, task, session): + def _import_task_created(self, task: ImportTask, _: ImportSession) -> None: task.item.mb_trackid = None - def _import_task_choice(self, task, session): + def _import_task_choice(self, task: ImportTask, _: ImportSession) -> None: match = task.match if not match: return @@ -174,24 +178,24 @@ def _import_task_choice(self, task, session): task.choice_flag = action.SKIP return - # Return whether the recording has a work id - def _has_work_id(self, recording_id): + def _has_work_id(self, recording_id: str) -> bool: + """Return whether the recording has a work id""" recording = self._get_recording(recording_id) work_id = _get_work_id_from_recording(recording) return work_id is not None - # This queries the local database, not the files. - def _command_func(self, lib, session, args): + def _command_func(self, lib: Library, _: ImportSession, args: list[str]) -> None: + """This queries the local database, not the files.""" for item in lib.items(args): self._process_file(item) - def _on_import(self, session, task): + def _on_import(self, _: ImportSession, task: ImportTask) -> None: if self.config['auto']: self._importing = True for item in task.imported_items(): self._process_file(item) - def _process_file(self, item: Item): + def _process_file(self, item: Item) -> None: if not item.mb_trackid or item.data_source != 'MusicBrainz': self._log.info('Skipping track with no mb_trackid: {0.artist} - {0.title}', item) return @@ -234,19 +238,20 @@ def _process_file(self, item: Item): if not self._importing: item.write() - # Fetch and cache recording from MusicBrainz, including releases and work relations - def _fetch_recording(self, recording_id): + def _fetch_recording(self, recording_id: str) -> Recording: + """Fetch and cache recording from MusicBrainz, including releases and work relations""" recording = musicbrainzngs.get_recording_by_id(recording_id, ['artists', 'releases', 'work-rels'])['recording'] self._recordings_cache[recording_id] = recording return recording - # Get recording from cache or MusicBrainz - def _get_recording(self, recording_id): + def _get_recording(self, recording_id: str) -> Recording: + """Get recording from cache or MusicBrainz""" return self._recordings_cache[ recording_id] if recording_id in self._recordings_cache else self._fetch_recording(recording_id) - # Get oldest date from a recording - def _extract_oldest_recording_date(self, recordings, starting_date, is_cover, approach): + def _extract_oldest_recording_date(self, recordings: list[Recording], starting_date: DateWrapper, + is_cover: bool, approach: str) -> DateWrapper: + """Get oldest date from a recording""" oldest_date = starting_date for rec in recordings: @@ -279,8 +284,9 @@ def _extract_oldest_recording_date(self, recordings, starting_date, is_cover, ap return oldest_date - # Get oldest date from a release - def _extract_oldest_release_date(self, recordings, starting_date, is_cover, artist_ids): + def _extract_oldest_release_date(self, recordings: list[Recording], starting_date: DateWrapper, + is_cover: bool, artist_ids: list[str]) -> DateWrapper: + """Get oldest date from a release""" oldest_date = starting_date release_types = self.config['release_types'].get() @@ -328,8 +334,9 @@ def _extract_oldest_release_date(self, recordings, starting_date, is_cover, arti return oldest_date - # Iterates through a list of recordings and returns oldest date - def _iterate_dates(self, recordings, starting_date, is_cover, artist_ids): + def _iterate_dates(self, recordings: list[Recording], starting_date: DateWrapper, + is_cover: bool, artist_ids: list[str]) -> Optional[DateWrapper]: + """Iterates through a list of recordings and returns oldest date""" approach = self.config['approach'].get() oldest_date = starting_date @@ -343,7 +350,7 @@ def _iterate_dates(self, recordings, starting_date, is_cover, artist_ids): return None if oldest_date == DateWrapper.today() else oldest_date - def _get_oldest_date(self, recording_id, item_date): + def _get_oldest_date(self, recording_id: str, item_date: Optional[DateWrapper]) -> Optional[DateWrapper]: recording = self._get_recording(recording_id) is_cover = _is_cover(recording) work_id = _get_work_id_from_recording(recording)