diff --git a/mopidy/utils/config.py b/mopidy/utils/config.py index 1a3127b5..aa1b06fd 100644 --- a/mopidy/utils/config.py +++ b/mopidy/utils/config.py @@ -33,6 +33,39 @@ def validate_maximum(value, maximum): raise ValueError('%r must be smaller than %r.' % (value, maximum)) +# TODO: move this and levenshtein to a more appropriate class. +def did_you_mean(name, choices): + """Suggest most likely setting based on levenshtein.""" + if not choices: + return None + + name = name.lower() + candidates = [(levenshtein(name, c), c) for c in choices] + candidates.sort() + + if candidates[0][0] <= 3: + return candidates[0][1] + return None + + +def levenshtein(a, b): + """Calculates the Levenshtein distance between a and b.""" + n, m = len(a), len(b) + if n > m: + return levenshtein(b, a) + + current = xrange(n + 1) + for i in xrange(1, m + 1): + previous, current = current, [i] + [0] * n + for j in xrange(1, n + 1): + add, delete = previous[j] + 1, current[j - 1] + 1 + change = previous[j - 1] + if a[j - 1] != b[i - 1]: + change += 1 + current[j] = min(add, delete, change) + return current[n] + + class ConfigValue(object): """Represents a config key's value and how to handle it. @@ -248,6 +281,9 @@ class ConfigSchema(object): values[key] = self._schema[key].deserialize(value) except KeyError: # not in our schema errors[key] = 'unknown config key.' + suggestion = did_you_mean(key, self._schema.keys()) + if suggestion: + errors[key] += ' Did you mean %s?' % suggestion except ValueError as e: # deserialization failed errors[key] = str(e) diff --git a/tests/utils/config_test.py b/tests/utils/config_test.py index 77c846df..ad86b961 100644 --- a/tests/utils/config_test.py +++ b/tests/utils/config_test.py @@ -395,3 +395,22 @@ class LogLevelConfigSchemaTest(unittest.TestCase): result = schema.format('levels', {'foo.bar': logging.DEBUG, 'baz': logging.INFO}) self.assertEqual('\n'.join(expected), result) + +class DidYouMeanTest(unittest.TestCase): + def testSuggestoins(self): + choices = ('enabled', 'username', 'password', 'bitrate', 'timeout') + + suggestion = config.did_you_mean('bitrate', choices) + self.assertEqual(suggestion, 'bitrate') + + suggestion = config.did_you_mean('bitrote', choices) + self.assertEqual(suggestion, 'bitrate') + + suggestion = config.did_you_mean('Bitrot', choices) + self.assertEqual(suggestion, 'bitrate') + + suggestion = config.did_you_mean('BTROT', choices) + self.assertEqual(suggestion, 'bitrate') + + suggestion = config.did_you_mean('btro', choices) + self.assertEqual(suggestion, None) diff --git a/tests/utils/settings_test.py b/tests/utils/settings_test.py index 3b1e67b0..3aa595e3 100644 --- a/tests/utils/settings_test.py +++ b/tests/utils/settings_test.py @@ -149,27 +149,3 @@ class SettingsProxyTest(unittest.TestCase): def test_value_ending_in_path_can_be_none(self): self.settings.TEST_PATH = None self.assertEqual(self.settings.TEST_PATH, None) - - -class DidYouMeanTest(unittest.TestCase): - def testSuggestoins(self): - defaults = { - 'MPD_SERVER_HOSTNAME': '::', - 'MPD_SERVER_PORT': 6600, - 'SPOTIFY_BITRATE': 160, - } - - suggestion = setting_utils.did_you_mean('spotify_bitrate', defaults) - self.assertEqual(suggestion, 'SPOTIFY_BITRATE') - - suggestion = setting_utils.did_you_mean('SPOTIFY_BITROTE', defaults) - self.assertEqual(suggestion, 'SPOTIFY_BITRATE') - - suggestion = setting_utils.did_you_mean('SPITIFY_BITROT', defaults) - self.assertEqual(suggestion, 'SPOTIFY_BITRATE') - - suggestion = setting_utils.did_you_mean('SPTIFY_BITROT', defaults) - self.assertEqual(suggestion, 'SPOTIFY_BITRATE') - - suggestion = setting_utils.did_you_mean('SPTIFY_BITRO', defaults) - self.assertEqual(suggestion, None)