fold OrderedEnum into autotag module

This puts the OrderedEnum generic class next to where it is actually used. It
also refers to the recipe it is taken from on docs.python.org. I also took the
opportunity to give this a capitalized name (since it's a proper type).
This commit is contained in:
Adrian Sampson 2014-04-08 17:10:50 -07:00
parent 443b8089d5
commit 323be89d4d
7 changed files with 76 additions and 66 deletions

View file

@ -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')

View file

@ -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):

View file

@ -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.

View file

@ -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.

View file

@ -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__)

View file

@ -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',

View file

@ -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')