diff --git a/beets/autotag/__init__.py b/beets/autotag/__init__.py index 9001c295d..3477f2128 100644 --- a/beets/autotag/__init__.py +++ b/beets/autotag/__init__.py @@ -24,7 +24,7 @@ from beets.util import sorted_walk, ancestry, displayable_path # Parts of external interface. from .hooks import AlbumInfo, TrackInfo, AlbumMatch, TrackMatch from .match import tag_item, tag_album -from .match import recommendation +from .match import Recommendation # Global logger. log = logging.getLogger('beets') diff --git a/beets/autotag/match.py b/beets/autotag/match.py index f80b41789..f981c90ca 100644 --- a/beets/autotag/match.py +++ b/beets/autotag/match.py @@ -21,16 +21,14 @@ import datetime import logging import re from munkres import Munkres +import enum from beets import plugins from beets import config from beets.util import plurality -from beets.util.enumeration import OrderedEnum from beets.autotag import hooks # Recommendation enumeration. -recommendation = OrderedEnum('recommendation', ['none', 'low', 'medium', - 'strong']) # Artist signals that indicate "various artists". These are used at the # album level to determine whether a given release is likely a VA @@ -42,6 +40,40 @@ VA_ARTISTS = (u'', u'various artists', u'various', u'va', u'unknown') log = logging.getLogger('beets') +# Recommendation enumeration. + +# https://docs.python.org/3.4/library/enum.html#orderedenum +class OrderedEnum(enum.Enum): + """An Enum subclass that allows comparison of members. + """ + def __ge__(self, other): + if self.__class__ is other.__class__: + return self.value >= other.value + return NotImplemented + def __gt__(self, other): + if self.__class__ is other.__class__: + return self.value > other.value + return NotImplemented + def __le__(self, other): + if self.__class__ is other.__class__: + return self.value <= other.value + return NotImplemented + def __lt__(self, other): + if self.__class__ is other.__class__: + return self.value < other.value + return NotImplemented + + +class Recommendation(OrderedEnum): + """Indicates a qualitative suggestion to the user about what should + be done with a given match. + """ + none = 0 + low = 1 + medium = 2 + strong = 3 + + # Primary matching functionality. def current_metadata(items): @@ -252,7 +284,7 @@ def match_by_id(items): return None # If all album IDs are equal, look up the album. - if bool(reduce(lambda x,y: x if x==y else (), albumids)): + if bool(reduce(lambda x,y: x if x == y else (), albumids)): albumid = albumids[0] log.debug('Searching for discovered album ID: ' + albumid) return hooks.album_for_mbid(albumid) @@ -269,26 +301,26 @@ def _recommendation(results): """ if not results: # No candidates: no recommendation. - return recommendation.none + return Recommendation.none # Basic distance thresholding. min_dist = results[0].distance if min_dist < config['match']['strong_rec_thresh'].as_number(): # Strong recommendation level. - rec = recommendation.strong + rec = Recommendation.strong elif min_dist <= config['match']['medium_rec_thresh'].as_number(): # Medium recommendation level. - rec = recommendation.medium + rec = Recommendation.medium elif len(results) == 1: # Only a single candidate. - rec = recommendation.low + rec = Recommendation.low elif results[1].distance - min_dist >= \ config['match']['rec_gap_thresh'].as_number(): # Gap between first two candidates is large. - rec = recommendation.low + rec = Recommendation.low else: # No conclusion. Return immediately. Can't be downgraded any further. - return recommendation.none + return Recommendation.none # Downgrade to the max rec if it is lower than the current rec for an # applied penalty. @@ -300,10 +332,10 @@ def _recommendation(results): for key in keys: if key in max_rec_view.keys(): max_rec = max_rec_view[key].as_choice({ - 'strong': recommendation.strong, - 'medium': recommendation.medium, - 'low': recommendation.low, - 'none': recommendation.none, + 'strong': Recommendation.strong, + 'medium': Recommendation.medium, + 'low': Recommendation.low, + 'none': Recommendation.none, }) rec = min(rec, max_rec) @@ -347,7 +379,7 @@ def tag_album(items, search_artist=None, search_album=None, - The current album. - A list of AlbumMatch objects. The candidates are sorted by distance (i.e., best match first). - - A recommendation. + - A :class:`Recommendation`. If search_artist and search_album or search_id are provided, then they are used as search terms in place of the current metadata. """ @@ -378,7 +410,7 @@ def tag_album(items, search_artist=None, search_album=None, # If we have a very good MBID match, return immediately. # Otherwise, this match will compete against metadata-based # matches. - if rec == recommendation.strong: + if rec == Recommendation.strong: log.debug('ID match.') return cur_artist, cur_album, candidates.values(), rec @@ -429,7 +461,7 @@ def tag_item(item, search_artist=None, search_title=None, hooks.TrackMatch(dist, track_info) # If this is a good match, then don't keep searching. rec = _recommendation(candidates.values()) - if rec == recommendation.strong and not config['import']['timid']: + if rec == Recommendation.strong and not config['import']['timid']: log.debug('Track ID match.') return candidates.values(), rec @@ -438,7 +470,7 @@ def tag_item(item, search_artist=None, search_title=None, if candidates: return candidates.values(), rec else: - return [], recommendation.none + return [], Recommendation.none # Search terms. if not (search_artist and search_title): diff --git a/beets/importer.py b/beets/importer.py index d2161b66d..b02ee0ede 100644 --- a/beets/importer.py +++ b/beets/importer.py @@ -664,7 +664,7 @@ def user_query(session): process. The coroutine accepts an ImportTask objects. It uses the - session's ``choose_match`` method to determine the ``action`` for + session's `choose_match` method to determine the `action` for this task. Depending on the action additional stages are exectuted and the processed task is yielded. diff --git a/beets/ui/commands.py b/beets/ui/commands.py index d83f60a77..1b1aab5b0 100644 --- a/beets/ui/commands.py +++ b/beets/ui/commands.py @@ -28,7 +28,7 @@ import beets from beets import ui from beets.ui import print_, input_, decargs from beets import autotag -from beets.autotag import recommendation +from beets.autotag import Recommendation from beets.autotag import hooks from beets import plugins from beets import importer @@ -402,7 +402,7 @@ def _summary_judment(rec): made. """ if config['import']['quiet']: - if rec == recommendation.strong: + if rec == Recommendation.strong: return importer.action.APPLY else: action = config['import']['quiet_fallback'].as_choice({ @@ -410,7 +410,7 @@ def _summary_judment(rec): 'asis': importer.action.ASIS, }) - elif rec == recommendation.none: + elif rec == Recommendation.none: action = config['import']['none_rec_action'].as_choice({ 'skip': importer.action.SKIP, 'asis': importer.action.ASIS, @@ -479,13 +479,13 @@ def choose_candidate(candidates, singleton, rec, cur_artist=None, # Is the change good enough? bypass_candidates = False - if rec != recommendation.none: + if rec != Recommendation.none: match = candidates[0] bypass_candidates = True while True: # Display and choose from candidates. - require = rec <= recommendation.low + require = rec <= Recommendation.low if not bypass_candidates: # Display list of candidates. @@ -559,7 +559,7 @@ def choose_candidate(candidates, singleton, rec, cur_artist=None, show_change(cur_artist, cur_album, match) # Exact match => tag automatically if we're not in timid mode. - if rec == recommendation.strong and not config['import']['timid']: + if rec == Recommendation.strong and not config['import']['timid']: return match # Ask for confirmation. diff --git a/test/test_autotag.py b/test/test_autotag.py index d78cf91e6..0d8e0edf8 100644 --- a/test/test_autotag.py +++ b/test/test_autotag.py @@ -1020,6 +1020,21 @@ class StringDistanceTest(unittest.TestCase): dist = string_dist(u'\xe9\xe1\xf1', u'ean') self.assertEqual(dist, 0.0) + +class EnumTest(_common.TestCase): + """ + Test Enum Subclasses defined in beets.util.enumeration + """ + def test_ordered_enum(self): + OrderedEnumTest = match.OrderedEnum('OrderedEnumTest', ['a', 'b', 'c']) + self.assertLess(OrderedEnumTest.a, OrderedEnumTest.b) + self.assertLess(OrderedEnumTest.a, OrderedEnumTest.c) + self.assertLess(OrderedEnumTest.b, OrderedEnumTest.c) + self.assertGreater(OrderedEnumTest.b, OrderedEnumTest.a) + self.assertGreater(OrderedEnumTest.c, OrderedEnumTest.a) + self.assertGreater(OrderedEnumTest.c, OrderedEnumTest.b) + + def suite(): return unittest.TestLoader().loadTestsFromName(__name__) diff --git a/test/test_ui.py b/test/test_ui.py index c5c96b3ac..2f8edf3ec 100644 --- a/test/test_ui.py +++ b/test/test_ui.py @@ -436,7 +436,7 @@ class AutotagTest(_common.TestCase): 'path', [_common.item()], ) - task.set_candidates('artist', 'album', [], autotag.recommendation.none) + task.set_candidates('artist', 'album', [], autotag.Recommendation.none) session = _common.import_session(cli=True) res = session.choose_match(task) self.assertEqual(res, result) @@ -791,7 +791,8 @@ class ShowChangeTest(_common.TestCase): self.info = autotag.AlbumInfo( u'the album', u'album id', u'the artist', u'artist id', [ autotag.TrackInfo(u'the title', u'track id', index=1) - ]) + ] + ) def _show_change(self, items=None, info=None, cur_artist=u'the artist', cur_album=u'the album', diff --git a/test/test_util.py b/test/test_util.py deleted file mode 100644 index e678ba32f..000000000 --- a/test/test_util.py +++ /dev/null @@ -1,38 +0,0 @@ -# This file is part of beets. -# Copyright 2013, Adrian Sampson. -# -# Permission is hereby granted, free of charge, to any person obtaining -# a copy of this software and associated documentation files (the -# "Software"), to deal in the Software without restriction, including -# without limitation the rights to use, copy, modify, merge, publish, -# distribute, sublicense, and/or sell copies of the Software, and to -# permit persons to whom the Software is furnished to do so, subject to -# the following conditions: -# -# The above copyright notice and this permission notice shall be -# included in all copies or substantial portions of the Software. - -"""Tests for utils.""" - -import _common -from _common import unittest -from beets.util.enumeration import OrderedEnum - -class EnumTest(_common.TestCase): - """ - Test Enum Subclasses defined in beets.util.enumeration - """ - def test_ordered_enum(self): - OrderedEnumTest = OrderedEnum('OrderedEnumTest', ['a', 'b', 'c']) - self.assertLess(OrderedEnumTest.a, OrderedEnumTest.b) - self.assertLess(OrderedEnumTest.a, OrderedEnumTest.c) - self.assertLess(OrderedEnumTest.b, OrderedEnumTest.c) - self.assertGreater(OrderedEnumTest.b, OrderedEnumTest.a) - self.assertGreater(OrderedEnumTest.c, OrderedEnumTest.a) - self.assertGreater(OrderedEnumTest.c, OrderedEnumTest.b) - -def suite(): - return unittest.TestLoader().loadTestsFromName(__name__) - -if __name__ == '__main__': - unittest.main(defaultTest='suite')