diff --git a/beets/mediafile.py b/beets/mediafile.py index 7d1b07280..9db6ba4db 100644 --- a/beets/mediafile.py +++ b/beets/mediafile.py @@ -56,15 +56,13 @@ import imghdr import os import traceback import enum - -from beets import logging -from beets.util import displayable_path, syspath, as_string +import logging import six __all__ = ['UnreadableFileError', 'FileTypeError', 'MediaFile'] -log = logging.getLogger('beets') +log = logging.getLogger(__name__) # Human-readable type names. TYPES = { @@ -90,7 +88,7 @@ class UnreadableFileError(Exception): """Mutagen is not able to extract information from the file. """ def __init__(self, path): - Exception.__init__(self, displayable_path(path)) + Exception.__init__(self, repr(path)) class FileTypeError(UnreadableFileError): @@ -100,11 +98,10 @@ class FileTypeError(UnreadableFileError): mutagen type is not supported by `Mediafile`. """ def __init__(self, path, mutagen_type=None): - path = displayable_path(path) if mutagen_type is None: - msg = path + msg = repr(path) else: - msg = u'{0}: of mutagen type {1}'.format(path, mutagen_type) + msg = u'{0}: of mutagen type {1}'.format(repr(path), mutagen_type) Exception.__init__(self, msg) @@ -112,7 +109,7 @@ class MutagenError(UnreadableFileError): """Raised when Mutagen fails unexpectedly---probably due to a bug. """ def __init__(self, path, mutagen_exc): - msg = u'{0}: {1}'.format(displayable_path(path), mutagen_exc) + msg = u'{0}: {1}'.format(repr(path), mutagen_exc) Exception.__init__(self, msg) @@ -134,12 +131,12 @@ def mutagen_call(action, path, func, *args, **kwargs): try: return func(*args, **kwargs) except mutagen.MutagenError as exc: - log.debug(u'{} failed: {}', action, six.text_type(exc)) + log.debug(u'%s failed: %s', action, six.text_type(exc)) raise UnreadableFileError(path) except Exception as exc: # Isolate bugs in Mutagen. - log.debug(u'{}', traceback.format_exc()) - log.error(u'uncaught Mutagen exception in {}: {}', action, exc) + log.debug(u'%s', traceback.format_exc()) + log.error(u'uncaught Mutagen exception in %s: %s', action, exc) raise MutagenError(path, exc) @@ -405,7 +402,7 @@ class Image(object): try: type = list(ImageType)[type] except IndexError: - log.debug(u"ignoring unknown image type index {0}", type) + log.debug(u"ignoring unknown image type index %s", type) type = ImageType.other self.type = type @@ -1412,7 +1409,6 @@ class MediaFile(object): By default, MP3 files are saved with ID3v2.4 tags. You can use the older ID3v2.3 standard by specifying the `id3v23` option. """ - path = syspath(path) self.path = path self.mgfile = mutagen_call('open', path, mutagen.File, path) @@ -1488,7 +1484,12 @@ class MediaFile(object): """ for property, descriptor in cls.__dict__.items(): if isinstance(descriptor, MediaField): - yield as_string(property) + if isinstance(property, bytes): + # On Python 2, class field names are bytes. This method + # produces text strings. + yield property.decode('utf8', 'ignore') + else: + yield property @classmethod def _field_sort_name(cls, name): diff --git a/beetsplug/badfiles.py b/beetsplug/badfiles.py index bb14c7762..5ab0ae983 100644 --- a/beetsplug/badfiles.py +++ b/beetsplug/badfiles.py @@ -105,7 +105,7 @@ class BadFiles(BeetsPlugin): ui.print_(u"{}: checker exited withs status {}" .format(ui.colorize('text_error', dpath), status)) for line in output: - ui.print_(" {}".format(displayable_path(line))) + ui.print_(u" {}".format(displayable_path(line))) elif errors > 0: ui.print_(u"{}: checker found {} errors or warnings" .format(ui.colorize('text_warning', dpath), errors)) diff --git a/docs/changelog.rst b/docs/changelog.rst index 17024badb..22e453f57 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4,7 +4,9 @@ Changelog 1.4.2 (in development) ---------------------- -Changelog goes here! +Fixes: + +* :doc:`/plugins/badfiles`: Fix a crash on non-ASCII filenames. :bug:`2299` 1.4.1 (November 25, 2016) diff --git a/docs/plugins/fetchart.rst b/docs/plugins/fetchart.rst index c839b90ba..7eb8c194e 100644 --- a/docs/plugins/fetchart.rst +++ b/docs/plugins/fetchart.rst @@ -60,7 +60,7 @@ file. The available options are: Default: None. - **google_engine**: The custom search engine to use. Default: The `beets custom search engine`_, which searches the entire web. - **fanarttv_key**: The personal API key for requesting art from +- **fanarttv_key**: The personal API key for requesting art from fanart.tv. See below. - **store_source**: If enabled, fetchart stores the artwork's source in a flexible tag named ``art_source``. See below for the rationale behind this. diff --git a/docs/plugins/web.rst b/docs/plugins/web.rst index b267af9d2..f4ae063e0 100644 --- a/docs/plugins/web.rst +++ b/docs/plugins/web.rst @@ -72,7 +72,7 @@ The Web backend is built using a simple REST+JSON API with the excellent `Backbone.js`_. This allows future non-Web clients to use the same backend API. .. _Flask: http://flask.pocoo.org/ -.. _Backbone.js: http://documentcloud.github.com/backbone/ +.. _Backbone.js: http://backbonejs.org Eventually, to make the Web player really viable, we should use a Flash fallback for unsupported formats/browsers. There are a number of options for this: diff --git a/test/_common.py b/test/_common.py index 09747e595..597f0dc38 100644 --- a/test/_common.py +++ b/test/_common.py @@ -333,6 +333,29 @@ class Bag(object): return self.fields.get(key) +# Convenience methods for setting up a temporary sandbox directory for tests +# that need to interact with the filesystem. + +class TempDirMixin(object): + """Text mixin for creating and deleting a temporary directory. + """ + + def create_temp_dir(self): + """Create a temporary directory and assign it into `self.temp_dir`. + Call `remove_temp_dir` later to delete it. + """ + path = tempfile.mkdtemp() + if not isinstance(path, bytes): + path = path.encode('utf8') + self.temp_dir = path + + def remove_temp_dir(self): + """Delete the temporary directory created by `create_temp_dir`. + """ + if os.path.isdir(self.temp_dir): + shutil.rmtree(self.temp_dir) + + # Platform mocking. @contextmanager diff --git a/test/test_acousticbrainz.py b/test/test_acousticbrainz.py index 82a69c527..0b1407581 100644 --- a/test/test_acousticbrainz.py +++ b/test/test_acousticbrainz.py @@ -16,12 +16,13 @@ """Tests for the 'acousticbrainz' plugin. """ -from __future__ import absolute_import, print_function +from __future__ import division, absolute_import, print_function import json import os.path +import unittest -from test._common import unittest, RSRC +from test._common import RSRC from beetsplug.acousticbrainz import AcousticPlugin, ABSCHEME diff --git a/test/test_art.py b/test/test_art.py index 5c3a4d7e9..aba180780 100644 --- a/test/test_art.py +++ b/test/test_art.py @@ -19,12 +19,12 @@ from __future__ import division, absolute_import, print_function import os import shutil +import unittest import responses from mock import patch from test import _common -from test._common import unittest from beetsplug import fetchart from beets.autotag import AlbumInfo, AlbumMatch from beets import config diff --git a/test/test_autotag.py b/test/test_autotag.py index b2f3fd61d..6f107afa4 100644 --- a/test/test_autotag.py +++ b/test/test_autotag.py @@ -19,9 +19,9 @@ from __future__ import division, absolute_import, print_function import re import copy +import unittest from test import _common -from test._common import unittest from beets import autotag from beets.autotag import match from beets.autotag.hooks import Distance, string_dist diff --git a/test/test_bucket.py b/test/test_bucket.py index daade86ad..61f6cfe24 100644 --- a/test/test_bucket.py +++ b/test/test_bucket.py @@ -17,7 +17,7 @@ from __future__ import division, absolute_import, print_function -from test._common import unittest +import unittest from beetsplug import bucket from beets import config, ui diff --git a/test/test_config_command.py b/test/test_config_command.py index 26242132d..35ba6ca0e 100644 --- a/test/test_config_command.py +++ b/test/test_config_command.py @@ -7,11 +7,11 @@ import yaml from mock import patch from tempfile import mkdtemp from shutil import rmtree +import unittest from beets import ui from beets import config -from test._common import unittest from test.helper import TestHelper from beets.library import Library import six diff --git a/test/test_convert.py b/test/test_convert.py index 0d1804288..2a32e51e7 100644 --- a/test/test_convert.py +++ b/test/test_convert.py @@ -17,8 +17,9 @@ from __future__ import division, absolute_import, print_function import re import os.path +import unittest + from test import _common -from test._common import unittest from test import helper from test.helper import control_stdin, capture_log diff --git a/test/test_datequery.py b/test/test_datequery.py index c139c0c4a..1e1625db2 100644 --- a/test/test_datequery.py +++ b/test/test_datequery.py @@ -18,8 +18,8 @@ from __future__ import division, absolute_import, print_function from test import _common -from test._common import unittest from datetime import datetime +import unittest import time from beets.dbcore.query import _parse_periods, DateInterval, DateQuery diff --git a/test/test_dbcore.py b/test/test_dbcore.py index bb6a56fab..d070e257e 100644 --- a/test/test_dbcore.py +++ b/test/test_dbcore.py @@ -20,10 +20,10 @@ from __future__ import division, absolute_import, print_function import os import shutil import sqlite3 +import unittest from six import assertRaisesRegex from test import _common -from test._common import unittest from beets import dbcore from tempfile import mkstemp import six diff --git a/test/test_discogs.py b/test/test_discogs.py index 0f89ff923..5fe5926b8 100644 --- a/test/test_discogs.py +++ b/test/test_discogs.py @@ -17,8 +17,9 @@ """ from __future__ import division, absolute_import, print_function +import unittest from test import _common -from test._common import unittest, Bag +from test._common import Bag from beetsplug.discogs import DiscogsPlugin diff --git a/test/test_edit.py b/test/test_edit.py index a23860b07..2900d092a 100644 --- a/test/test_edit.py +++ b/test/test_edit.py @@ -15,10 +15,10 @@ from __future__ import division, absolute_import, print_function import codecs +import unittest from mock import patch from test import _common -from test._common import unittest from test.helper import TestHelper, control_stdin from test.test_ui_importer import TerminalImportSessionSetup from test.test_importer import ImportHelper, AutotagStub diff --git a/test/test_embedart.py b/test/test_embedart.py index 0d64b0d9a..ee08ecb4e 100644 --- a/test/test_embedart.py +++ b/test/test_embedart.py @@ -19,9 +19,9 @@ import os.path import shutil from mock import patch, MagicMock import tempfile +import unittest from test import _common -from test._common import unittest from test.helper import TestHelper from beets.mediafile import MediaFile diff --git a/test/test_embyupdate.py b/test/test_embyupdate.py index 17e82c73e..2b83a4896 100644 --- a/test/test_embyupdate.py +++ b/test/test_embyupdate.py @@ -2,9 +2,9 @@ from __future__ import division, absolute_import, print_function -from test._common import unittest from test.helper import TestHelper from beetsplug import embyupdate +import unittest import responses diff --git a/test/test_fetchart.py b/test/test_fetchart.py index 5def538eb..91bc34101 100644 --- a/test/test_fetchart.py +++ b/test/test_fetchart.py @@ -16,7 +16,7 @@ from __future__ import division, absolute_import, print_function import os -from test._common import unittest +import unittest from test.helper import TestHelper from beets import util diff --git a/test/test_filefilter.py b/test/test_filefilter.py index 09b6d94a4..e7a4119d4 100644 --- a/test/test_filefilter.py +++ b/test/test_filefilter.py @@ -20,9 +20,9 @@ from __future__ import division, absolute_import, print_function import os import shutil +import unittest from test import _common -from test._common import unittest from test.helper import capture_log from test.test_importer import ImportHelper from beets import config diff --git a/test/test_files.py b/test/test_files.py index 884aa40d0..b566f363e 100644 --- a/test/test_files.py +++ b/test/test_files.py @@ -21,9 +21,9 @@ import shutil import os import stat from os.path import join +import unittest from test import _common -from test._common import unittest from test._common import item, touch import beets.library from beets import util diff --git a/test/test_ftintitle.py b/test/test_ftintitle.py index 711b5daa6..d698263be 100644 --- a/test/test_ftintitle.py +++ b/test/test_ftintitle.py @@ -17,7 +17,7 @@ from __future__ import division, absolute_import, print_function -from test._common import unittest +import unittest from test.helper import TestHelper from beetsplug import ftintitle diff --git a/test/test_hidden.py b/test/test_hidden.py index cb673a64a..e2dd1afb3 100644 --- a/test/test_hidden.py +++ b/test/test_hidden.py @@ -17,7 +17,7 @@ from __future__ import division, absolute_import, print_function -from test._common import unittest +import unittest import sys import tempfile from beets.util import hidden diff --git a/test/test_hook.py b/test/test_hook.py index cd6b2734f..39fd08959 100644 --- a/test/test_hook.py +++ b/test/test_hook.py @@ -17,9 +17,9 @@ from __future__ import division, absolute_import, print_function import os.path import tempfile +import unittest from test import _common -from test._common import unittest from test.helper import TestHelper from beets import config diff --git a/test/test_ihate.py b/test/test_ihate.py index 1b23964ae..f52617481 100644 --- a/test/test_ihate.py +++ b/test/test_ihate.py @@ -4,7 +4,7 @@ from __future__ import division, absolute_import, print_function -from test._common import unittest +import unittest from beets import importer from beets.library import Item from beetsplug.ihate import IHatePlugin diff --git a/test/test_importadded.py b/test/test_importadded.py index bd8d8b506..52aa26756 100644 --- a/test/test_importadded.py +++ b/test/test_importadded.py @@ -18,8 +18,8 @@ from __future__ import division, absolute_import, print_function """Tests for the `importadded` plugin.""" import os +import unittest -from test._common import unittest from test.test_importer import ImportHelper, AutotagStub from beets import importer from beets import util diff --git a/test/test_importer.py b/test/test_importer.py index 55b03378c..500ca027d 100644 --- a/test/test_importer.py +++ b/test/test_importer.py @@ -27,9 +27,9 @@ from tempfile import mkstemp from zipfile import ZipFile from tarfile import TarFile from mock import patch, Mock +import unittest from test import _common -from test._common import unittest from beets.util import displayable_path, bytestring_path, py3_path from test.helper import TestImportSession, TestHelper, has_program, capture_log from beets import importer diff --git a/test/test_importfeeds.py b/test/test_importfeeds.py index adb2885c7..064f5eee5 100644 --- a/test/test_importfeeds.py +++ b/test/test_importfeeds.py @@ -6,8 +6,8 @@ import os import os.path import tempfile import shutil +import unittest -from test._common import unittest from beets import config from beets.library import Item, Album, Library from beetsplug.importfeeds import ImportFeedsPlugin diff --git a/test/test_info.py b/test/test_info.py index f7973796e..f5375e2f6 100644 --- a/test/test_info.py +++ b/test/test_info.py @@ -15,7 +15,7 @@ from __future__ import division, absolute_import, print_function -from test._common import unittest +import unittest from test.helper import TestHelper from beets.mediafile import MediaFile diff --git a/test/test_ipfs.py b/test/test_ipfs.py index 66d895f2e..9c523d6e5 100644 --- a/test/test_ipfs.py +++ b/test/test_ipfs.py @@ -20,11 +20,12 @@ from beets import library from beets.util import bytestring_path, _fsencoding from beetsplug.ipfs import IPFSPlugin -from test import _common -from test._common import unittest -from test.helper import TestHelper +import unittest import os +from test import _common +from test.helper import TestHelper + @patch('beets.util.command_output', Mock()) class IPFSPluginTest(unittest.TestCase, TestHelper): diff --git a/test/test_keyfinder.py b/test/test_keyfinder.py index 57e2bcd9c..c2b4227d7 100644 --- a/test/test_keyfinder.py +++ b/test/test_keyfinder.py @@ -16,7 +16,7 @@ from __future__ import division, absolute_import, print_function from mock import patch -from test._common import unittest +import unittest from test.helper import TestHelper from beets.library import Item diff --git a/test/test_lastgenre.py b/test/test_lastgenre.py index c060e95a9..f57471f1c 100644 --- a/test/test_lastgenre.py +++ b/test/test_lastgenre.py @@ -17,10 +17,10 @@ from __future__ import division, absolute_import, print_function +import unittest from mock import Mock from test import _common -from test._common import unittest from beetsplug import lastgenre from beets import config diff --git a/test/test_library.py b/test/test_library.py index 174e6d527..26c78c99d 100644 --- a/test/test_library.py +++ b/test/test_library.py @@ -25,9 +25,9 @@ import re import unicodedata import sys import time +import unittest from test import _common -from test._common import unittest from test._common import item import beets.library import beets.mediafile diff --git a/test/test_logging.py b/test/test_logging.py index 6e5e6c8b6..a6b02a572 100644 --- a/test/test_logging.py +++ b/test/test_logging.py @@ -7,12 +7,13 @@ import sys import threading import logging as log from six import StringIO +import unittest import beets.logging as blog from beets import plugins, ui import beetsplug from test import _common -from test._common import unittest, TestCase +from test._common import TestCase from test import helper import six diff --git a/test/test_lyrics.py b/test/test_lyrics.py index 429dee4e8..13ba07fdf 100644 --- a/test/test_lyrics.py +++ b/test/test_lyrics.py @@ -18,13 +18,13 @@ from __future__ import division, absolute_import, print_function import os -from test import _common import sys import re +import unittest +from test import _common from mock import MagicMock -from test._common import unittest from beetsplug import lyrics from beets.library import Item from beets.util import confit, bytestring_path diff --git a/test/test_mb.py b/test/test_mb.py index 43656451d..ecbcf3c59 100644 --- a/test/test_mb.py +++ b/test/test_mb.py @@ -18,9 +18,10 @@ from __future__ import division, absolute_import, print_function from test import _common -from test._common import unittest from beets.autotag import mb from beets import config + +import unittest import mock diff --git a/test/test_mbsubmit.py b/test/test_mbsubmit.py index a33d8418b..3402c8464 100644 --- a/test/test_mbsubmit.py +++ b/test/test_mbsubmit.py @@ -15,7 +15,7 @@ from __future__ import division, absolute_import, print_function -from test._common import unittest +import unittest from test.helper import capture_stdout, control_stdin, TestHelper from test.test_importer import ImportHelper, AutotagStub from test.test_ui_importer import TerminalImportSessionSetup diff --git a/test/test_mbsync.py b/test/test_mbsync.py index 57213878c..411af8d0a 100644 --- a/test/test_mbsync.py +++ b/test/test_mbsync.py @@ -15,9 +15,9 @@ from __future__ import division, absolute_import, print_function +import unittest from mock import patch -from test._common import unittest from test.helper import TestHelper,\ generate_album_info, \ generate_track_info, \ diff --git a/test/test_mediafile.py b/test/test_mediafile.py index 32d93f815..463cd534d 100644 --- a/test/test_mediafile.py +++ b/test/test_mediafile.py @@ -20,20 +20,14 @@ from __future__ import division, absolute_import, print_function import os import shutil -import tempfile import datetime import time +import unittest from six import assertCountEqual from test import _common -from test._common import unittest -from beets.mediafile import MediaFile, MediaField, Image, \ - MP3DescStorageStyle, StorageStyle, MP4StorageStyle, \ - ASFStorageStyle, ImageType, CoverArtField, UnreadableFileError -from beets.library import Item -from beets.plugins import BeetsPlugin -from beets.util import bytestring_path -import six +from beets.mediafile import MediaFile, Image, \ + ImageType, CoverArtField, UnreadableFileError class ArtTestMixin(object): @@ -298,80 +292,8 @@ class GenreListTestMixin(object): assertCountEqual(self, mediafile.genres, [u'the genre', u'another']) -field_extension = MediaField( - MP3DescStorageStyle(u'customtag'), - MP4StorageStyle('----:com.apple.iTunes:customtag'), - StorageStyle('customtag'), - ASFStorageStyle('customtag'), -) - - -class ExtendedFieldTestMixin(object): - - def test_extended_field_write(self): - plugin = BeetsPlugin() - plugin.add_media_field('customtag', field_extension) - - try: - mediafile = self._mediafile_fixture('empty') - mediafile.customtag = u'F#' - mediafile.save() - - mediafile = MediaFile(mediafile.path) - self.assertEqual(mediafile.customtag, u'F#') - - finally: - delattr(MediaFile, 'customtag') - Item._media_fields.remove('customtag') - - def test_write_extended_tag_from_item(self): - plugin = BeetsPlugin() - plugin.add_media_field('customtag', field_extension) - - try: - mediafile = self._mediafile_fixture('empty') - self.assertIsNone(mediafile.customtag) - - item = Item(path=mediafile.path, customtag=u'Gb') - item.write() - mediafile = MediaFile(mediafile.path) - self.assertEqual(mediafile.customtag, u'Gb') - - finally: - delattr(MediaFile, 'customtag') - Item._media_fields.remove('customtag') - - def test_read_flexible_attribute_from_file(self): - plugin = BeetsPlugin() - plugin.add_media_field('customtag', field_extension) - - try: - mediafile = self._mediafile_fixture('empty') - mediafile.update({'customtag': u'F#'}) - mediafile.save() - - item = Item.from_path(mediafile.path) - self.assertEqual(item['customtag'], u'F#') - - finally: - delattr(MediaFile, 'customtag') - Item._media_fields.remove('customtag') - - def test_invalid_descriptor(self): - with self.assertRaises(ValueError) as cm: - MediaFile.add_field('somekey', True) - self.assertIn(u'must be an instance of MediaField', - six.text_type(cm.exception)) - - def test_overwrite_property(self): - with self.assertRaises(ValueError) as cm: - MediaFile.add_field('artist', MediaField()) - self.assertIn(u'property "artist" already exists', - six.text_type(cm.exception)) - - class ReadWriteTestBase(ArtTestMixin, GenreListTestMixin, - ExtendedFieldTestMixin): + _common.TempDirMixin): """Test writing and reading tags. Subclasses must set ``extension`` and ``audio_properties``. """ @@ -456,11 +378,10 @@ class ReadWriteTestBase(ArtTestMixin, GenreListTestMixin, ] def setUp(self): - self.temp_dir = bytestring_path(tempfile.mkdtemp()) + self.create_temp_dir() def tearDown(self): - if os.path.isdir(self.temp_dir): - shutil.rmtree(self.temp_dir) + self.remove_temp_dir() def test_read_nonexisting(self): mediafile = self._mediafile_fixture('full') @@ -709,7 +630,9 @@ class ReadWriteTestBase(ArtTestMixin, GenreListTestMixin, self.fail('\n '.join(errors)) def _mediafile_fixture(self, name): - name = bytestring_path(name + '.' + self.extension) + name = name + '.' + self.extension + if not isinstance(name, bytes): + name = name.encode('utf8') src = os.path.join(_common.RSRC, name) target = os.path.join(self.temp_dir, name) shutil.copy(src, target) diff --git a/test/test_mediafile_edge.py b/test/test_mediafile_edge.py index ae758f142..657ca455d 100644 --- a/test/test_mediafile_edge.py +++ b/test/test_mediafile_edge.py @@ -19,18 +19,16 @@ from __future__ import division, absolute_import, print_function import os import shutil +import unittest import mutagen.id3 from test import _common -from test._common import unittest -from test.helper import TestHelper -from beets.util import bytestring_path -import beets.mediafile +from beets import mediafile import six -_sc = beets.mediafile._safe_cast +_sc = mediafile._safe_cast class EdgeTest(unittest.TestCase): @@ -38,7 +36,7 @@ class EdgeTest(unittest.TestCase): # Some files have an ID3 frame that has a list with no elements. # This is very hard to produce, so this is just the first 8192 # bytes of a file found "in the wild". - emptylist = beets.mediafile.MediaFile( + emptylist = mediafile.MediaFile( os.path.join(_common.RSRC, b'emptylist.mp3') ) genre = emptylist.genre @@ -47,7 +45,7 @@ class EdgeTest(unittest.TestCase): def test_release_time_with_space(self): # Ensures that release times delimited by spaces are ignored. # Amie Street produces such files. - space_time = beets.mediafile.MediaFile( + space_time = mediafile.MediaFile( os.path.join(_common.RSRC, b'space_time.mp3') ) self.assertEqual(space_time.year, 2009) @@ -57,7 +55,7 @@ class EdgeTest(unittest.TestCase): def test_release_time_with_t(self): # Ensures that release times delimited by Ts are ignored. # The iTunes Store produces such files. - t_time = beets.mediafile.MediaFile( + t_time = mediafile.MediaFile( os.path.join(_common.RSRC, b't_time.m4a') ) self.assertEqual(t_time.year, 1987) @@ -67,20 +65,20 @@ class EdgeTest(unittest.TestCase): def test_tempo_with_bpm(self): # Some files have a string like "128 BPM" in the tempo field # rather than just a number. - f = beets.mediafile.MediaFile(os.path.join(_common.RSRC, b'bpm.mp3')) + f = mediafile.MediaFile(os.path.join(_common.RSRC, b'bpm.mp3')) self.assertEqual(f.bpm, 128) def test_discc_alternate_field(self): # Different taggers use different vorbis comments to reflect # the disc and disc count fields: ensure that the alternative # style works. - f = beets.mediafile.MediaFile(os.path.join(_common.RSRC, b'discc.ogg')) + f = mediafile.MediaFile(os.path.join(_common.RSRC, b'discc.ogg')) self.assertEqual(f.disc, 4) self.assertEqual(f.disctotal, 5) def test_old_ape_version_bitrate(self): media_file = os.path.join(_common.RSRC, b'oldape.ape') - f = beets.mediafile.MediaFile(media_file) + f = mediafile.MediaFile(media_file) self.assertEqual(f.bitrate, 0) def test_only_magic_bytes_jpeg(self): @@ -92,13 +90,13 @@ class EdgeTest(unittest.TestCase): with open(magic_bytes_file, 'rb') as f: jpg_data = f.read() self.assertEqual( - beets.mediafile._imghdr_what_wrapper(jpg_data), 'jpeg') + mediafile._imghdr_what_wrapper(jpg_data), 'jpeg') def test_soundcheck_non_ascii(self): # Make sure we don't crash when the iTunes SoundCheck field contains # non-ASCII binary data. - f = beets.mediafile.MediaFile(os.path.join(_common.RSRC, - b'soundcheck-nonascii.m4a')) + f = mediafile.MediaFile(os.path.join(_common.RSRC, + b'soundcheck-nonascii.m4a')) self.assertEqual(f.rg_track_gain, 0.0) @@ -146,7 +144,7 @@ class InvalidValueToleranceTest(unittest.TestCase): self.assertEqual(v, 1.0) -class SafetyTest(unittest.TestCase, TestHelper): +class SafetyTest(unittest.TestCase, _common.TempDirMixin): def setUp(self): self.create_temp_dir() @@ -158,35 +156,35 @@ class SafetyTest(unittest.TestCase, TestHelper): with open(fn, 'w') as f: f.write(data) try: - self.assertRaises(exc, beets.mediafile.MediaFile, fn) + self.assertRaises(exc, mediafile.MediaFile, fn) finally: os.unlink(fn) # delete the temporary file def test_corrupt_mp3_raises_unreadablefileerror(self): # Make sure we catch Mutagen reading errors appropriately. - self._exccheck(b'corrupt.mp3', beets.mediafile.UnreadableFileError) + self._exccheck(b'corrupt.mp3', mediafile.UnreadableFileError) def test_corrupt_mp4_raises_unreadablefileerror(self): - self._exccheck(b'corrupt.m4a', beets.mediafile.UnreadableFileError) + self._exccheck(b'corrupt.m4a', mediafile.UnreadableFileError) def test_corrupt_flac_raises_unreadablefileerror(self): - self._exccheck(b'corrupt.flac', beets.mediafile.UnreadableFileError) + self._exccheck(b'corrupt.flac', mediafile.UnreadableFileError) def test_corrupt_ogg_raises_unreadablefileerror(self): - self._exccheck(b'corrupt.ogg', beets.mediafile.UnreadableFileError) + self._exccheck(b'corrupt.ogg', mediafile.UnreadableFileError) def test_invalid_ogg_header_raises_unreadablefileerror(self): - self._exccheck(b'corrupt.ogg', beets.mediafile.UnreadableFileError, + self._exccheck(b'corrupt.ogg', mediafile.UnreadableFileError, 'OggS\x01vorbis') def test_corrupt_monkeys_raises_unreadablefileerror(self): - self._exccheck(b'corrupt.ape', beets.mediafile.UnreadableFileError) + self._exccheck(b'corrupt.ape', mediafile.UnreadableFileError) def test_invalid_extension_raises_filetypeerror(self): - self._exccheck(b'something.unknown', beets.mediafile.FileTypeError) + self._exccheck(b'something.unknown', mediafile.FileTypeError) def test_magic_xml_raises_unreadablefileerror(self): - self._exccheck(b'nothing.xml', beets.mediafile.UnreadableFileError, + self._exccheck(b'nothing.xml', mediafile.UnreadableFileError, "ftyp") @unittest.skipUnless(_common.HAVE_SYMLINK, u'platform lacks symlink') @@ -194,8 +192,8 @@ class SafetyTest(unittest.TestCase, TestHelper): fn = os.path.join(_common.RSRC, b'brokenlink') os.symlink('does_not_exist', fn) try: - self.assertRaises(beets.mediafile.UnreadableFileError, - beets.mediafile.MediaFile, fn) + self.assertRaises(mediafile.UnreadableFileError, + mediafile.MediaFile, fn) finally: os.unlink(fn) @@ -206,19 +204,19 @@ class SideEffectsTest(unittest.TestCase): def test_opening_tagless_file_leaves_untouched(self): old_mtime = os.stat(self.empty).st_mtime - beets.mediafile.MediaFile(self.empty) + mediafile.MediaFile(self.empty) new_mtime = os.stat(self.empty).st_mtime self.assertEqual(old_mtime, new_mtime) -class MP4EncodingTest(unittest.TestCase, TestHelper): +class MP4EncodingTest(unittest.TestCase, _common.TempDirMixin): def setUp(self): self.create_temp_dir() src = os.path.join(_common.RSRC, b'full.m4a') self.path = os.path.join(self.temp_dir, b'test.m4a') shutil.copy(src, self.path) - self.mf = beets.mediafile.MediaFile(self.path) + self.mf = mediafile.MediaFile(self.path) def tearDown(self): self.remove_temp_dir() @@ -226,18 +224,18 @@ class MP4EncodingTest(unittest.TestCase, TestHelper): def test_unicode_label_in_m4a(self): self.mf.label = u'foo\xe8bar' self.mf.save() - new_mf = beets.mediafile.MediaFile(self.path) + new_mf = mediafile.MediaFile(self.path) self.assertEqual(new_mf.label, u'foo\xe8bar') -class MP3EncodingTest(unittest.TestCase, TestHelper): +class MP3EncodingTest(unittest.TestCase, _common.TempDirMixin): def setUp(self): self.create_temp_dir() src = os.path.join(_common.RSRC, b'full.mp3') self.path = os.path.join(self.temp_dir, b'test.mp3') shutil.copy(src, self.path) - self.mf = beets.mediafile.MediaFile(self.path) + self.mf = mediafile.MediaFile(self.path) def test_comment_with_latin1_encoding(self): # Set up the test file with a Latin1-encoded COMM frame. The encoding @@ -250,7 +248,7 @@ class MP3EncodingTest(unittest.TestCase, TestHelper): self.mf.save() -class ZeroLengthMediaFile(beets.mediafile.MediaFile): +class ZeroLengthMediaFile(mediafile.MediaFile): @property def length(self): return 0.0 @@ -271,7 +269,7 @@ class TypeTest(unittest.TestCase): def setUp(self): super(TypeTest, self).setUp() path = os.path.join(_common.RSRC, b'full.mp3') - self.mf = beets.mediafile.MediaFile(path) + self.mf = mediafile.MediaFile(path) def test_year_integer_in_string(self): self.mf.year = u'2009' @@ -303,45 +301,45 @@ class TypeTest(unittest.TestCase): class SoundCheckTest(unittest.TestCase): def test_round_trip(self): - data = beets.mediafile._sc_encode(1.0, 1.0) - gain, peak = beets.mediafile._sc_decode(data) + data = mediafile._sc_encode(1.0, 1.0) + gain, peak = mediafile._sc_decode(data) self.assertEqual(gain, 1.0) self.assertEqual(peak, 1.0) def test_decode_zero(self): data = b' 80000000 80000000 00000000 00000000 00000000 00000000 ' \ b'00000000 00000000 00000000 00000000' - gain, peak = beets.mediafile._sc_decode(data) + gain, peak = mediafile._sc_decode(data) self.assertEqual(gain, 0.0) self.assertEqual(peak, 0.0) def test_malformatted(self): - gain, peak = beets.mediafile._sc_decode(b'foo') + gain, peak = mediafile._sc_decode(b'foo') self.assertEqual(gain, 0.0) self.assertEqual(peak, 0.0) def test_special_characters(self): - gain, peak = beets.mediafile._sc_decode(u'caf\xe9'.encode('utf-8')) + gain, peak = mediafile._sc_decode(u'caf\xe9'.encode('utf-8')) self.assertEqual(gain, 0.0) self.assertEqual(peak, 0.0) def test_decode_handles_unicode(self): # Most of the time, we expect to decode the raw bytes. But some formats # might give us text strings, which we need to handle. - gain, peak = beets.mediafile._sc_decode(u'caf\xe9') + gain, peak = mediafile._sc_decode(u'caf\xe9') self.assertEqual(gain, 0.0) self.assertEqual(peak, 0.0) -class ID3v23Test(unittest.TestCase, TestHelper): - def _make_test(self, ext='mp3', id3v23=False): +class ID3v23Test(unittest.TestCase, _common.TempDirMixin): + def _make_test(self, ext=b'mp3', id3v23=False): self.create_temp_dir() src = os.path.join(_common.RSRC, - bytestring_path('full.{0}'.format(ext))) + b'full.' + ext) self.path = os.path.join(self.temp_dir, - bytestring_path('test.{0}'.format(ext))) + b'test.' + ext) shutil.copy(src, self.path) - return beets.mediafile.MediaFile(self.path, id3v23=id3v23) + return mediafile.MediaFile(self.path, id3v23=id3v23) def _delete_test(self): self.remove_temp_dir() @@ -369,7 +367,7 @@ class ID3v23Test(unittest.TestCase, TestHelper): self._delete_test() def test_v23_on_non_mp3_is_noop(self): - mf = self._make_test('m4a', id3v23=True) + mf = self._make_test(b'm4a', id3v23=True) try: mf.year = 2013 mf.save() @@ -386,9 +384,9 @@ class ID3v23Test(unittest.TestCase, TestHelper): mf = self._make_test(id3v23=v23) try: mf.images = [ - beets.mediafile.Image(b'data', desc=u""), - beets.mediafile.Image(b'data', desc=u"foo"), - beets.mediafile.Image(b'data', desc=u"\u0185"), + mediafile.Image(b'data', desc=u""), + mediafile.Image(b'data', desc=u"foo"), + mediafile.Image(b'data', desc=u"\u0185"), ] mf.save() apic_frames = mf.mgfile.tags.getall('APIC') diff --git a/test/test_metasync.py b/test/test_metasync.py index 60f63c413..ef16c57a2 100644 --- a/test/test_metasync.py +++ b/test/test_metasync.py @@ -21,9 +21,9 @@ import time from datetime import datetime from beets.library import Item from beets.util import py3_path +import unittest from test import _common -from test._common import unittest from test.helper import TestHelper diff --git a/test/test_mpdstats.py b/test/test_mpdstats.py index febf292d8..7452be86b 100644 --- a/test/test_mpdstats.py +++ b/test/test_mpdstats.py @@ -15,8 +15,8 @@ from __future__ import division, absolute_import, print_function +import unittest from mock import Mock, patch, call, ANY -from test._common import unittest from test.helper import TestHelper from beets.library import Item diff --git a/test/test_permissions.py b/test/test_permissions.py index 040839110..ed84798f1 100644 --- a/test/test_permissions.py +++ b/test/test_permissions.py @@ -6,9 +6,9 @@ from __future__ import division, absolute_import, print_function import os import platform +import unittest from mock import patch, Mock -from test._common import unittest from test.helper import TestHelper from beets.util import displayable_path from beetsplug.permissions import (check_permissions, diff --git a/test/test_pipeline.py b/test/test_pipeline.py index 4d3bde4d0..228deb50e 100644 --- a/test/test_pipeline.py +++ b/test/test_pipeline.py @@ -18,8 +18,8 @@ from __future__ import division, absolute_import, print_function import six +import unittest -from test._common import unittest from beets.util import pipeline diff --git a/test/test_play.py b/test/test_play.py index c05e24c51..9d66d7466 100644 --- a/test/test_play.py +++ b/test/test_play.py @@ -19,9 +19,9 @@ from __future__ import division, absolute_import, print_function import os +import unittest from mock import patch, ANY -from test._common import unittest from test.helper import TestHelper, control_stdin from beets.ui import UserError diff --git a/test/test_player.py b/test/test_player.py index c8fec446b..0a576f0ba 100644 --- a/test/test_player.py +++ b/test/test_player.py @@ -17,7 +17,7 @@ """ from __future__ import division, absolute_import, print_function -from test._common import unittest +import unittest from beetsplug import bpd diff --git a/test/test_plexupdate.py b/test/test_plexupdate.py index 2a2b1cbaf..f8b0bdcd9 100644 --- a/test/test_plexupdate.py +++ b/test/test_plexupdate.py @@ -2,9 +2,9 @@ from __future__ import division, absolute_import, print_function -from test._common import unittest from test.helper import TestHelper from beetsplug.plexupdate import get_music_section, update_plex +import unittest import responses diff --git a/test/test_plugin_mediafield.py b/test/test_plugin_mediafield.py new file mode 100644 index 000000000..d9d2b0a1f --- /dev/null +++ b/test/test_plugin_mediafield.py @@ -0,0 +1,107 @@ +# -*- coding: utf-8 -*- +# This file is part of beets. +# Copyright 2016, 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 the facility that lets plugins add custom field to MediaFile. +""" +from __future__ import division, absolute_import, print_function + +import os +import six +import shutil + +from test import _common +from beets.library import Item +from beets import mediafile +from beets.plugins import BeetsPlugin +from beets.util import bytestring_path + + +field_extension = mediafile.MediaField( + mediafile.MP3DescStorageStyle(u'customtag'), + mediafile.MP4StorageStyle('----:com.apple.iTunes:customtag'), + mediafile.StorageStyle('customtag'), + mediafile.ASFStorageStyle('customtag'), +) + + +class ExtendedFieldTestMixin(_common.TestCase): + + def _mediafile_fixture(self, name, extension='mp3'): + name = bytestring_path(name + '.' + extension) + src = os.path.join(_common.RSRC, name) + target = os.path.join(self.temp_dir, name) + shutil.copy(src, target) + return mediafile.MediaFile(target) + + def test_extended_field_write(self): + plugin = BeetsPlugin() + plugin.add_media_field('customtag', field_extension) + + try: + mf = self._mediafile_fixture('empty') + mf.customtag = u'F#' + mf.save() + + mf = mediafile.MediaFile(mf.path) + self.assertEqual(mf.customtag, u'F#') + + finally: + delattr(mediafile.MediaFile, 'customtag') + Item._media_fields.remove('customtag') + + def test_write_extended_tag_from_item(self): + plugin = BeetsPlugin() + plugin.add_media_field('customtag', field_extension) + + try: + mf = self._mediafile_fixture('empty') + self.assertIsNone(mf.customtag) + + item = Item(path=mf.path, customtag=u'Gb') + item.write() + mf = mediafile.MediaFile(mf.path) + self.assertEqual(mf.customtag, u'Gb') + + finally: + delattr(mediafile.MediaFile, 'customtag') + Item._media_fields.remove('customtag') + + def test_read_flexible_attribute_from_file(self): + plugin = BeetsPlugin() + plugin.add_media_field('customtag', field_extension) + + try: + mf = self._mediafile_fixture('empty') + mf.update({'customtag': u'F#'}) + mf.save() + + item = Item.from_path(mf.path) + self.assertEqual(item['customtag'], u'F#') + + finally: + delattr(mediafile.MediaFile, 'customtag') + Item._media_fields.remove('customtag') + + def test_invalid_descriptor(self): + with self.assertRaises(ValueError) as cm: + mediafile.MediaFile.add_field('somekey', True) + self.assertIn(u'must be an instance of MediaField', + six.text_type(cm.exception)) + + def test_overwrite_property(self): + with self.assertRaises(ValueError) as cm: + mediafile.MediaFile.add_field('artist', mediafile.MediaField()) + self.assertIn(u'property "artist" already exists', + six.text_type(cm.exception)) diff --git a/test/test_plugins.py b/test/test_plugins.py index d45e317ca..c4a5792ff 100644 --- a/test/test_plugins.py +++ b/test/test_plugins.py @@ -19,6 +19,7 @@ import os from mock import patch, Mock, ANY import shutil import itertools +import unittest from beets.importer import SingletonImportTask, SentinelImportTask, \ ArchiveImportTask, action @@ -30,7 +31,7 @@ from beets.util import displayable_path, bytestring_path from test.test_importer import ImportHelper, AutotagStub from test.test_ui_importer import TerminalImportSessionSetup -from test._common import unittest, RSRC +from test._common import RSRC from test import helper diff --git a/test/test_query.py b/test/test_query.py index e0bbebfe1..41672ab35 100644 --- a/test/test_query.py +++ b/test/test_query.py @@ -21,9 +21,9 @@ from functools import partial from mock import patch import os import sys +import unittest from test import _common -from test._common import unittest from test import helper import beets.library diff --git a/test/test_replaygain.py b/test/test_replaygain.py index d2a8aa9ba..6ea21ecb6 100644 --- a/test/test_replaygain.py +++ b/test/test_replaygain.py @@ -16,14 +16,15 @@ from __future__ import division, absolute_import, print_function -from test._common import unittest +import unittest +import six + from test.helper import TestHelper, has_program from beets import config from beets.mediafile import MediaFile from beetsplug.replaygain import (FatalGstreamerPluginReplayGainError, GStreamerBackend) -import six try: import gi diff --git a/test/test_smartplaylist.py b/test/test_smartplaylist.py index 5e1fed084..af60501a4 100644 --- a/test/test_smartplaylist.py +++ b/test/test_smartplaylist.py @@ -18,6 +18,7 @@ from __future__ import division, absolute_import, print_function from os import path, remove from tempfile import mkdtemp from shutil import rmtree +import unittest from mock import Mock, MagicMock @@ -29,7 +30,6 @@ from beets.util import syspath, bytestring_path, py3_path, CHAR_REPLACE from beets.ui import UserError from beets import config -from test._common import unittest from test.helper import TestHelper diff --git a/test/test_sort.py b/test/test_sort.py index d10bd5759..d611ff2d6 100644 --- a/test/test_sort.py +++ b/test/test_sort.py @@ -17,8 +17,8 @@ """ from __future__ import division, absolute_import, print_function +import unittest from test import _common -from test._common import unittest import beets.library from beets import dbcore from beets import config diff --git a/test/test_spotify.py b/test/test_spotify.py index 20101d76c..17f3ef42f 100644 --- a/test/test_spotify.py +++ b/test/test_spotify.py @@ -6,9 +6,9 @@ from __future__ import division, absolute_import, print_function import os import responses +import unittest from test import _common -from test._common import unittest from beets import config from beets.library import Item from beetsplug import spotify diff --git a/test/test_template.py b/test/test_template.py index 67d5d97fe..1cbe9be0c 100644 --- a/test/test_template.py +++ b/test/test_template.py @@ -17,9 +17,9 @@ """ from __future__ import division, absolute_import, print_function -from test._common import unittest -from beets.util import functemplate +import unittest import six +from beets.util import functemplate def _normexpr(expr): diff --git a/test/test_the.py b/test/test_the.py index 422723386..72e9bf161 100644 --- a/test/test_the.py +++ b/test/test_the.py @@ -4,7 +4,7 @@ from __future__ import division, absolute_import, print_function -from test._common import unittest +import unittest from test import _common from beets import config from beetsplug.the import ThePlugin, PATTERN_A, PATTERN_THE, FORMAT diff --git a/test/test_thumbnails.py b/test/test_thumbnails.py index fe1c80f4a..d287c073a 100644 --- a/test/test_thumbnails.py +++ b/test/test_thumbnails.py @@ -19,8 +19,8 @@ import os.path from mock import Mock, patch, call from tempfile import mkdtemp from shutil import rmtree +import unittest -from test._common import unittest from test.helper import TestHelper from beets.util import bytestring_path diff --git a/test/test_types_plugin.py b/test/test_types_plugin.py index 826dd5ea7..541bd30df 100644 --- a/test/test_types_plugin.py +++ b/test/test_types_plugin.py @@ -17,8 +17,8 @@ from __future__ import division, absolute_import, print_function import time from datetime import datetime +import unittest -from test._common import unittest from test.helper import TestHelper from beets.util.confit import ConfigValueError diff --git a/test/test_ui.py b/test/test_ui.py index 99459603a..681900ae4 100644 --- a/test/test_ui.py +++ b/test/test_ui.py @@ -24,10 +24,10 @@ import subprocess import platform from copy import deepcopy import six +import unittest from mock import patch, Mock from test import _common -from test._common import unittest from test.helper import capture_stdout, has_program, TestHelper, control_stdin from beets import library diff --git a/test/test_ui_commands.py b/test/test_ui_commands.py index 5cd1131eb..764d1b009 100644 --- a/test/test_ui_commands.py +++ b/test/test_ui_commands.py @@ -20,9 +20,9 @@ from __future__ import division, absolute_import, print_function import os import shutil +import unittest from test import _common -from test._common import unittest from beets import library from beets import ui diff --git a/test/test_ui_importer.py b/test/test_ui_importer.py index cc2938ad3..48a66dc5f 100644 --- a/test/test_ui_importer.py +++ b/test/test_ui_importer.py @@ -20,8 +20,9 @@ test_importer module. But here the test importer inherits from """ from __future__ import division, absolute_import, print_function +import unittest -from test._common import unittest, DummyIO +from test._common import DummyIO from test import test_importer from beets.ui.commands import TerminalImportSession from beets import importer diff --git a/test/test_ui_init.py b/test/test_ui_init.py index 0628d3879..3a146b0c0 100644 --- a/test/test_ui_init.py +++ b/test/test_ui_init.py @@ -18,8 +18,8 @@ from __future__ import division, absolute_import, print_function +import unittest from test import _common -from test._common import unittest from beets import ui diff --git a/test/test_util.py b/test/test_util.py index 96d02f955..0d0bbd7b0 100644 --- a/test/test_util.py +++ b/test/test_util.py @@ -20,10 +20,10 @@ import sys import re import os import subprocess +import unittest from mock import patch, Mock -from test._common import unittest from test import _common from beets import util import six diff --git a/test/test_vfs.py b/test/test_vfs.py index 0b25a1031..b94acfd09 100644 --- a/test/test_vfs.py +++ b/test/test_vfs.py @@ -16,8 +16,8 @@ """Tests for the virtual filesystem builder..""" from __future__ import division, absolute_import, print_function +import unittest from test import _common -from test._common import unittest from beets import library from beets import vfs diff --git a/test/test_web.py b/test/test_web.py index 7d477227c..e72ecf33d 100644 --- a/test/test_web.py +++ b/test/test_web.py @@ -4,9 +4,9 @@ from __future__ import division, absolute_import, print_function +import unittest from six import assertCountEqual -from test._common import unittest from test import _common import json from beets.library import Item, Album diff --git a/test/test_zero.py b/test/test_zero.py index 7b90e22b3..5842d9ed4 100644 --- a/test/test_zero.py +++ b/test/test_zero.py @@ -4,7 +4,7 @@ from __future__ import division, absolute_import, print_function -from test._common import unittest +import unittest from test.helper import TestHelper from beets.library import Item diff --git a/test/testall.py b/test/testall.py index aebda21ee..88eb70117 100755 --- a/test/testall.py +++ b/test/testall.py @@ -20,8 +20,7 @@ from __future__ import division, absolute_import, print_function import os import re import sys - -from test._common import unittest +import unittest pkgpath = os.path.dirname(__file__) or '.' sys.path.append(pkgpath) diff --git a/tox.ini b/tox.ini index 28fd3636e..8115395c6 100644 --- a/tox.ini +++ b/tox.ini @@ -4,7 +4,7 @@ # and then run "tox" from this directory. [tox] -envlist = py27-test, py27-flake8, docs +envlist = py27-test, py35-test, py27-flake8, docs # The exhaustive list of environments is: # envlist = py{27,34,35}-{test,cov}, py{27,34,35}-flake8, docs