diff --git a/.gitignore b/.gitignore index 50a0f290c..b93d93305 100644 --- a/.gitignore +++ b/.gitignore @@ -84,3 +84,8 @@ ENV/ # Rope project settings .ropeproject + +# PyDev and Eclipse project settings +/.project +/.pydevproject +/.settings diff --git a/.travis.yml b/.travis.yml index 89097b937..39796cbdc 100644 --- a/.travis.yml +++ b/.travis.yml @@ -17,14 +17,13 @@ matrix: env: {TOX_ENV: py35-test} # - python: pypy # - env: {TOX_ENV: pypy-test} - - python: 2.7 - env: {TOX_ENV: py27-flake8} - python: 3.4 env: {TOX_ENV: py34-flake8} - python: 2.7 env: {TOX_ENV: docs} allow_failures: - python: 3.4 + env: {TOX_ENV: py34-test} - python: 3.5 # Non-Python dependencies. diff --git a/beets/autotag/match.py b/beets/autotag/match.py index 96057fe9a..1b21f725a 100644 --- a/beets/autotag/match.py +++ b/beets/autotag/match.py @@ -327,6 +327,11 @@ def _recommendation(results): return rec +def _sort_candidates(candidates): + """Sort candidates by distance.""" + return sorted(candidates, key=lambda match: match.distance) + + def _add_candidate(items, results, info): """Given a candidate AlbumInfo object, attempt to add the candidate to the output dictionary of AlbumMatch objects. This involves @@ -443,7 +448,7 @@ def tag_album(items, search_artist=None, search_album=None, _add_candidate(items, candidates, info) # Sort and get the recommendation. - candidates = sorted(candidates.values()) + candidates = _sort_candidates(candidates.values()) rec = _recommendation(candidates) return cur_artist, cur_album, candidates, rec @@ -471,16 +476,16 @@ def tag_item(item, search_artist=None, search_title=None, candidates[track_info.track_id] = \ hooks.TrackMatch(dist, track_info) # If this is a good match, then don't keep searching. - rec = _recommendation(sorted(candidates.values())) + rec = _recommendation(_sort_candidates(candidates.values())) if rec == Recommendation.strong and \ not config['import']['timid']: log.debug(u'Track ID match.') - return sorted(candidates.values()), rec + return _sort_candidates(candidates.values()), rec # If we're searching by ID, don't proceed. if search_ids: if candidates: - return sorted(candidates.values()), rec + return _sort_candidates(candidates.values()), rec else: return [], Recommendation.none @@ -496,6 +501,6 @@ def tag_item(item, search_artist=None, search_title=None, # Sort by distance and return with recommendation. log.debug(u'Found {0} candidates.', len(candidates)) - candidates = sorted(candidates.values()) + candidates = _sort_candidates(candidates.values()) rec = _recommendation(candidates) return candidates, rec diff --git a/beets/importer.py b/beets/importer.py index f3272311f..6a1be6ddc 100644 --- a/beets/importer.py +++ b/beets/importer.py @@ -944,7 +944,7 @@ class ArchiveImportTask(SentinelImportTask): return False for path_test, _ in cls.handlers(): - if path_test(path): + if path_test(util.py3_path(path)): return True return False @@ -990,7 +990,7 @@ class ArchiveImportTask(SentinelImportTask): try: extract_to = mkdtemp() - archive = handler_class(self.toppath, mode='r') + archive = handler_class(util.py3_path(self.toppath), mode='r') archive.extractall(extract_to) finally: archive.close() diff --git a/beets/util/__init__.py b/beets/util/__init__.py index fec6d56db..01bd452bc 100644 --- a/beets/util/__init__.py +++ b/beets/util/__init__.py @@ -443,8 +443,7 @@ def move(path, dest, replace=False): path = syspath(path) dest = syspath(dest) if os.path.exists(dest) and not replace: - raise FilesystemError(u'file exists', 'rename', (path, dest), - traceback.format_exc()) + raise FilesystemError(u'file exists', 'rename', (path, dest)) # First, try renaming the file. try: @@ -469,13 +468,19 @@ def link(path, dest, replace=False): path = syspath(path) dest = syspath(dest) if os.path.exists(dest) and not replace: - raise FilesystemError(u'file exists', 'rename', (path, dest), - traceback.format_exc()) + raise FilesystemError(u'file exists', 'rename', (path, dest)) try: os.symlink(path, dest) - except OSError: - raise FilesystemError(u'Operating system does not support symbolic ' - u'links.', 'link', (path, dest), + except NotImplementedError: + # raised on python >= 3.2 and Windows versions before Vista + raise FilesystemError(u'OS does not support symbolic links.' + 'link', (path, dest), traceback.format_exc()) + except OSError as exc: + # TODO: Windows version checks can be removed for python 3 + if hasattr('sys', 'getwindowsversion'): + if sys.getwindowsversion()[0] < 6: # is before Vista + exc = u'OS does not support symbolic links.' + raise FilesystemError(exc, 'link', (path, dest), traceback.format_exc()) diff --git a/beetsplug/bpd/__init__.py b/beetsplug/bpd/__init__.py index 3aabeb869..5b6a8a9a7 100644 --- a/beetsplug/bpd/__init__.py +++ b/beetsplug/bpd/__init__.py @@ -640,8 +640,8 @@ class Command(object): """A command issued by the client for processing by the server. """ - command_re = re.compile(br'^([^ \t]+)[ \t]*') - arg_re = re.compile(br'"((?:\\"|[^"])+)"|([^ \t"]+)') + command_re = re.compile(r'^([^ \t]+)[ \t]*') + arg_re = re.compile(r'"((?:\\"|[^"])+)"|([^ \t"]+)') def __init__(self, s): """Creates a new `Command` from the given string, `s`, parsing @@ -656,11 +656,10 @@ class Command(object): if match[0]: # Quoted argument. arg = match[0] - arg = arg.replace(b'\\"', b'"').replace(b'\\\\', b'\\') + arg = arg.replace('\\"', '"').replace('\\\\', '\\') else: # Unquoted argument. arg = match[1] - arg = arg.decode('utf8') self.args.append(arg) def run(self, conn): diff --git a/beetsplug/bpd/gstplayer.py b/beetsplug/bpd/gstplayer.py index 8fa0f7a81..6972a2722 100644 --- a/beetsplug/bpd/gstplayer.py +++ b/beetsplug/bpd/gstplayer.py @@ -29,9 +29,9 @@ from six.moves import urllib from beets import ui import gi -from gi.repository import GLib, Gst - gi.require_version('Gst', '1.0') +from gi.repository import GLib, Gst # noqa ignore=E402 + Gst.init(None) diff --git a/beetsplug/convert.py b/beetsplug/convert.py index 14f3f3452..4e3556684 100644 --- a/beetsplug/convert.py +++ b/beetsplug/convert.py @@ -47,7 +47,7 @@ def replace_ext(path, ext): The new extension must not contain a leading dot. """ - ext_dot = util.bytestring_path('.' + ext) + ext_dot = b'.' + ext return os.path.splitext(path)[0] + ext_dot @@ -68,7 +68,7 @@ def get_format(fmt=None): .format(fmt) ) except ConfigTypeError: - command = config['convert']['formats'][fmt].get(bytes) + command = config['convert']['formats'][fmt].get(str) extension = fmt # Convenience and backwards-compatibility shortcuts. @@ -428,7 +428,9 @@ class ConvertPlugin(BeetsPlugin): # Create a temporary file for the conversion. tmpdir = self.config['tmpdir'].get() - fd, dest = tempfile.mkstemp('.' + ext, dir=tmpdir) + if tmpdir: + tmpdir = util.py3_path(util.bytestring_path(tmpdir)) + fd, dest = tempfile.mkstemp(util.py3_path(b'.' + ext), dir=tmpdir) os.close(fd) dest = util.bytestring_path(dest) _temp_files.append(dest) # Delete the transcode later. diff --git a/beetsplug/importfeeds.py b/beetsplug/importfeeds.py index 5e4333f73..18f63a2bd 100644 --- a/beetsplug/importfeeds.py +++ b/beetsplug/importfeeds.py @@ -24,7 +24,7 @@ import os import re from beets.plugins import BeetsPlugin -from beets.util import mkdirall, normpath, syspath, bytestring_path +from beets.util import mkdirall, normpath, syspath, bytestring_path, link from beets import config M3U_DEFAULT_NAME = 'imported.m3u' @@ -131,7 +131,7 @@ class ImportFeedsPlugin(BeetsPlugin): for path in paths: dest = os.path.join(feedsdir, os.path.basename(path)) if not os.path.exists(syspath(dest)): - os.symlink(syspath(path), syspath(dest)) + link(path, dest) if 'echo' in formats: self._log.info(u"Location of imported music:") diff --git a/beetsplug/lastgenre/__init__.py b/beetsplug/lastgenre/__init__.py index bc3155b94..f901bdaac 100644 --- a/beetsplug/lastgenre/__init__.py +++ b/beetsplug/lastgenre/__init__.py @@ -25,6 +25,7 @@ The scraper script used is available here: https://gist.github.com/1241307 """ import pylast +import codecs import os import yaml import traceback @@ -140,7 +141,8 @@ class LastGenrePlugin(plugins.BeetsPlugin): c14n_filename = C14N_TREE if c14n_filename: c14n_filename = normpath(c14n_filename) - genres_tree = yaml.load(open(c14n_filename, 'r')) + genres_file = codecs.open(c14n_filename, 'r', encoding='utf-8') + genres_tree = yaml.load(genres_file) flatten_tree(genres_tree, [], self.c14n_branches) @property diff --git a/beetsplug/play.py b/beetsplug/play.py index 7d5b4c227..27da15b37 100644 --- a/beetsplug/play.py +++ b/beetsplug/play.py @@ -137,8 +137,7 @@ class PlayPlugin(BeetsPlugin): else: open_args = [self._create_tmp_playlist(paths)] - self._log.debug(u'executing command: {} {}', command_str, - util.text_string(b' '.join(open_args))) + self._log.debug(u'executing command: {} {!r}', command_str, open_args) try: util.interactive_open(open_args, command_str) except OSError as exc: diff --git a/beetsplug/replaygain.py b/beetsplug/replaygain.py index e25f1a045..cae55f5b5 100644 --- a/beetsplug/replaygain.py +++ b/beetsplug/replaygain.py @@ -308,9 +308,9 @@ class CommandBackend(Backend): def format_supported(self, item): """Checks whether the given item is supported by the selected tool. """ - if 'mp3gain' in self.command and item.format != 'MP3': + if b'mp3gain' in self.command and item.format != 'MP3': return False - elif 'aacgain' in self.command and item.format not in ('MP3', 'AAC'): + elif b'aacgain' in self.command and item.format not in ('MP3', 'AAC'): return False return True diff --git a/test/_common.py b/test/_common.py index 12a3d479e..cfec6afb1 100644 --- a/test/_common.py +++ b/test/_common.py @@ -53,7 +53,7 @@ log.setLevel(logging.DEBUG) _item_ident = 0 # OS feature test. -HAVE_SYMLINK = hasattr(os, 'symlink') +HAVE_SYMLINK = sys.platform != 'win32' def item(lib=None): diff --git a/test/helper.py b/test/helper.py index ae6dc4fbd..93813004b 100644 --- a/test/helper.py +++ b/test/helper.py @@ -422,6 +422,7 @@ class TestHelper(object): # Running beets commands def run_command(self, *args): + sys.argv = ['beet'] # avoid leakage from test suite args if hasattr(self, 'lib'): lib = self.lib else: diff --git a/test/test_importer.py b/test/test_importer.py index 29487ec26..07b6680ea 100644 --- a/test/test_importer.py +++ b/test/test_importer.py @@ -30,7 +30,7 @@ from mock import patch from test import _common from test._common import unittest -from beets.util import displayable_path, bytestring_path +from beets.util import displayable_path, bytestring_path, py3_path from test.helper import TestImportSession, TestHelper, has_program, capture_log from beets import importer from beets.importer import albums_in_dir @@ -355,9 +355,9 @@ class NonAutotaggedImportTest(_common.TestCase, ImportHelper): def create_archive(session): - (handle, path) = mkstemp(dir=session.temp_dir) + (handle, path) = mkstemp(dir=py3_path(session.temp_dir)) os.close(handle) - archive = ZipFile(path, mode='w') + archive = ZipFile(py3_path(path), mode='w') archive.write(os.path.join(_common.RSRC, b'full.mp3'), 'full.mp3') archive.close() @@ -414,7 +414,7 @@ class ImportTarTest(ImportZipTest): def create_archive(self): (handle, path) = mkstemp(dir=self.temp_dir) os.close(handle) - archive = TarFile(path, mode='w') + archive = TarFile(py3_path(path), mode='w') archive.add(os.path.join(_common.RSRC, b'full.mp3'), 'full.mp3') archive.close() diff --git a/test/test_keyfinder.py b/test/test_keyfinder.py index 00952fe14..a9b1ae085 100644 --- a/test/test_keyfinder.py +++ b/test/test_keyfinder.py @@ -46,7 +46,7 @@ class KeyFinderTest(unittest.TestCase, TestHelper): item.load() self.assertEqual(item['initial_key'], 'C#m') self.command_output.assert_called_with( - ['KeyFinder', '-f', util.syspath(item.path)]) + [b'KeyFinder', b'-f', util.syspath(item.path)]) def test_add_key_on_import(self): self.command_output.return_value = 'dbm' diff --git a/test/test_types_plugin.py b/test/test_types_plugin.py index 6a3df8051..67e682844 100644 --- a/test/test_types_plugin.py +++ b/test/test_types_plugin.py @@ -48,7 +48,7 @@ class TypesPluginTest(unittest.TestCase, TestHelper): # Match in range out = self.list(u'myint:1..3') - self.assertIn(b'aaa', out) + self.assertIn('aaa', out) def test_album_integer_modify_and_query(self): self.config['types'] = {'myint': u'int'} @@ -64,7 +64,7 @@ class TypesPluginTest(unittest.TestCase, TestHelper): # Match in range out = self.list_album(u'myint:1..3') - self.assertIn(b'aaa', out) + self.assertIn('aaa', out) def test_float_modify_and_query(self): self.config['types'] = {'myfloat': u'float'} @@ -76,7 +76,7 @@ class TypesPluginTest(unittest.TestCase, TestHelper): # Match in range out = self.list(u'myfloat:-10..0') - self.assertIn(b'aaa', out) + self.assertIn('aaa', out) def test_bool_modify_and_query(self): self.config['types'] = {'mybool': u'bool'} diff --git a/test/test_ui.py b/test/test_ui.py index 365b40923..94594d9f7 100644 --- a/test/test_ui.py +++ b/test/test_ui.py @@ -774,7 +774,7 @@ class ConfigTest(unittest.TestCase, TestHelper, _common.Assertions): ]) def test_cli_config_option(self): - config_path = os.path.join(self.temp_dir, 'config.yaml') + config_path = os.path.join(self.temp_dir, b'config.yaml') with open(config_path, 'w') as file: file.write('anoption: value') @@ -785,7 +785,7 @@ class ConfigTest(unittest.TestCase, TestHelper, _common.Assertions): with open(self.user_config_path, 'w') as file: file.write('anoption: value') - cli_config_path = os.path.join(self.temp_dir, 'config.yaml') + cli_config_path = os.path.join(self.temp_dir, b'config.yaml') with open(cli_config_path, 'w') as file: file.write('anoption: cli overwrite') @@ -798,7 +798,7 @@ class ConfigTest(unittest.TestCase, TestHelper, _common.Assertions): with open(env_config_path, 'w') as file: file.write('anoption: value') - cli_config_path = os.path.join(self.temp_dir, 'config.yaml') + cli_config_path = os.path.join(self.temp_dir, b'config.yaml') with open(cli_config_path, 'w') as file: file.write('anoption: cli overwrite') @@ -807,8 +807,8 @@ class ConfigTest(unittest.TestCase, TestHelper, _common.Assertions): # @unittest.skip('Difficult to implement with optparse') # def test_multiple_cli_config_files(self): -# cli_config_path_1 = os.path.join(self.temp_dir, 'config.yaml') -# cli_config_path_2 = os.path.join(self.temp_dir, 'config_2.yaml') +# cli_config_path_1 = os.path.join(self.temp_dir, b'config.yaml') +# cli_config_path_2 = os.path.join(self.temp_dir, b'config_2.yaml') # # with open(cli_config_path_1, 'w') as file: # file.write('first: value') @@ -823,9 +823,9 @@ class ConfigTest(unittest.TestCase, TestHelper, _common.Assertions): # # @unittest.skip('Difficult to implement with optparse') # def test_multiple_cli_config_overwrite(self): -# cli_config_path = os.path.join(self.temp_dir, 'config.yaml') +# cli_config_path = os.path.join(self.temp_dir, b'config.yaml') # cli_overwrite_config_path = os.path.join(self.temp_dir, -# 'overwrite_config.yaml') +# b'overwrite_config.yaml') # # with open(cli_config_path, 'w') as file: # file.write('anoption: value') @@ -838,7 +838,7 @@ class ConfigTest(unittest.TestCase, TestHelper, _common.Assertions): # self.assertEqual(config['anoption'].get(), 'cli overwrite') def test_cli_config_paths_resolve_relative_to_user_dir(self): - cli_config_path = os.path.join(self.temp_dir, 'config.yaml') + cli_config_path = os.path.join(self.temp_dir, b'config.yaml') with open(cli_config_path, 'w') as file: file.write('library: beets.db\n') file.write('statefile: state') @@ -856,7 +856,7 @@ class ConfigTest(unittest.TestCase, TestHelper, _common.Assertions): def test_cli_config_paths_resolve_relative_to_beetsdir(self): os.environ['BEETSDIR'] = util.py3_path(self.beetsdir) - cli_config_path = os.path.join(self.temp_dir, 'config.yaml') + cli_config_path = os.path.join(self.temp_dir, b'config.yaml') with open(cli_config_path, 'w') as file: file.write('library: beets.db\n') file.write('statefile: state') @@ -878,7 +878,7 @@ class ConfigTest(unittest.TestCase, TestHelper, _common.Assertions): os.path.join(os.getcwd(), 'foo.db')) def test_cli_config_file_loads_plugin_commands(self): - cli_config_path = os.path.join(self.temp_dir, 'config.yaml') + cli_config_path = os.path.join(self.temp_dir, b'config.yaml') with open(cli_config_path, 'w') as file: file.write('pluginpath: %s\n' % _common.PLUGINPATH) file.write('plugins: test')