Merge remote-tracking branch 'upstream/master' into mbid

This commit is contained in:
Diego Moreda 2016-01-19 21:58:10 +01:00
commit c12e974852
16 changed files with 174 additions and 16 deletions

View file

@ -86,8 +86,8 @@ class PathQuery(dbcore.FieldQuery):
colon = query_part.find(':')
if colon != -1:
query_part = query_part[:colon]
return (os.sep in query_part
and os.path.exists(syspath(normpath(query_part))))
return (os.sep in query_part and
os.path.exists(syspath(normpath(query_part))))
def match(self, item):
path = item.path if self.case_sensitive else item.path.lower()

View file

@ -1236,7 +1236,10 @@ def show_stats(lib, query, exact):
for item in items:
if exact:
total_size += os.path.getsize(item.path)
try:
total_size += os.path.getsize(item.path)
except OSError as exc:
log.info('could not get size of {}: {}', item.path, exc)
else:
total_size += int(item.length * item.bitrate / 8)
total_time += item.length

View file

@ -72,9 +72,9 @@ def _logged_get(log, *args, **kwargs):
else:
message = 'getting URL'
req = requests.Request('GET', *args, **req_kwargs)
req = requests.Request(b'GET', *args, **req_kwargs)
with requests.Session() as s:
s.headers = {'User-Agent': 'beets'}
s.headers = {b'User-Agent': b'beets'}
prepped = s.prepare_request(req)
log.debug('{}: {}', message, prepped.url)
return s.send(prepped, **send_kwargs)

View file

@ -119,6 +119,25 @@ def print_data(data, item=None, fmt=None):
ui.print_(lineformat.format(field, value))
def print_data_keys(data, item=None):
"""Print only the keys (field names) for an item.
"""
path = displayable_path(item.path) if item else None
formatted = []
for key, value in data.iteritems():
formatted.append(key)
if len(formatted) == 0:
return
line_format = u'{0}{{0}}'.format(u' ' * 4)
if path:
ui.print_(displayable_path(path))
for field in sorted(formatted):
ui.print_(line_format.format(field))
class InfoPlugin(BeetsPlugin):
def commands(self):
@ -131,6 +150,8 @@ class InfoPlugin(BeetsPlugin):
cmd.parser.add_option('-i', '--include-keys', default=[],
action='append', dest='included_keys',
help='comma separated list of keys to show')
cmd.parser.add_option('-k', '--keys-only', action='store_true',
help='show only the keys')
cmd.parser.add_format_option(target='item')
return [cmd]
@ -173,7 +194,10 @@ class InfoPlugin(BeetsPlugin):
else:
if not first:
ui.print_()
print_data(data, item, opts.format)
if opts.keys_only:
print_data_keys(data, item)
else:
print_data(data, item, opts.format)
first = False
if opts.summarize:

View file

@ -18,6 +18,8 @@ New:
* A new :doc:`/plugins/mbsubmit` lets you print the tracks of an album in a
format parseable by MusicBrainz track parser during an interactive import
session. :bug:`1779`
* :doc:`/plugins/info`: A new option will print only fields' names and not
their values. Thanks to :user:`GuilhermeHideki`. :bug:`1812`
.. _AcousticBrainz: http://acousticbrainz.org/
@ -45,6 +47,10 @@ Fixes:
* :doc:`/plugins/lyrics`: The Genius backend has been re-enabled.
* :doc:`/plugins/edit`: Editing metadata now moves files, when appropriate
(like the :ref:`modify-cmd` command). :bug:`1804`
* The :ref:`stats-cmd` command no longer crashes when files are missing or
inaccessible. :bug:`1806`
* :doc:`/plugins/fetchart`: Possibly fix a Unicode-related crash when using
some versions of pyOpenSSL. :bug:`1805`
.. _beets.io: http://beets.io/
.. _Beetbox: https://github.com/beetbox

View file

@ -25,7 +25,7 @@ Here's an example::
ihate:
warn:
- artist:rnb
- genre: soul
- genre:soul
# Only warn about tribute albums in rock genre.
- genre:rock album:tribute
skip:

View file

@ -39,7 +39,7 @@ Additional command-line options include:
* ``--format`` or ``-f``: Specify a specific format with which to print every
item. This uses the same template syntax as beets :doc:`path formats
</reference/pathformat>`.
* ``--keys-only`` or ``-k``: Show the name of the tags without the values.
.. _id3v2: http://id3v2.sourceforge.net
.. _mp3info: http://www.ibiblio.org/mp3info/

View file

@ -346,3 +346,11 @@ def system_mock(name):
yield
finally:
platform.system = old_system
def slow_test(unused=None):
def _id(obj):
return obj
if 'SKIP_SLOW_TESTS' in os.environ:
return unittest.skip('test is slow')
return _id

View file

@ -209,10 +209,10 @@ class AAOTest(UseThePlugin):
def test_aao_scraper_finds_image(self):
body = b"""
<br />
<a href="TARGET_URL" title="View larger image"
class="thickbox" style="color: #7E9DA2; text-decoration:none;">
<img src="http://www.albumart.org/images/zoom-icon.jpg"
alt="View larger image" width="17" height="15" border="0"/></a>
<a href=\"TARGET_URL\" title=\"View larger image\"
class=\"thickbox\" style=\"color: #7E9DA2; text-decoration:none;\">
<img src=\"http://www.albumart.org/images/zoom-icon.jpg\"
alt=\"View larger image\" width=\"17\" height=\"15\" border=\"0\"/></a>
"""
self.mock_response(self.AAO_URL, body)
album = _common.Bag(asin=self.ASIN)
@ -261,6 +261,7 @@ class GoogleImageTest(UseThePlugin):
self.assertEqual(list(result_url), [])
@_common.slow_test()
class ArtImporterTest(UseThePlugin):
def setUp(self):
super(ArtImporterTest, self).setUp()

View file

@ -64,6 +64,7 @@ class TestHelper(helper.TestHelper):
.format(path, tag))
@_common.slow_test()
class ImportConvertTest(unittest.TestCase, TestHelper):
def setUp(self):
@ -99,6 +100,7 @@ class ImportConvertTest(unittest.TestCase, TestHelper):
self.assertTrue(os.path.isfile(item.path))
@_common.slow_test()
class ConvertCliTest(unittest.TestCase, TestHelper):
def setUp(self):
@ -186,6 +188,7 @@ class ConvertCliTest(unittest.TestCase, TestHelper):
self.assertFalse(os.path.exists(converted))
@_common.slow_test()
class NeverConvertLossyFilesTest(unittest.TestCase, TestHelper):
"""Test the effect of the `never_convert_lossy_files` option.
"""

View file

@ -19,8 +19,10 @@ from __future__ import (division, absolute_import, print_function,
unicode_literals)
import os
import shutil
import sqlite3
from test import _common
from test._common import unittest
from beets import dbcore
from tempfile import mkstemp
@ -116,15 +118,28 @@ class TestDatabaseTwoModels(dbcore.Database):
pass
class TestModelWithGetters(dbcore.Model):
@classmethod
def _getters(cls):
return {'aComputedField': (lambda s: 'thing')}
def _template_funcs(self):
return {}
@_common.slow_test()
class MigrationTest(unittest.TestCase):
"""Tests the ability to change the database schema between
versions.
"""
def setUp(self):
handle, self.libfile = mkstemp('db')
@classmethod
def setUpClass(cls):
handle, cls.orig_libfile = mkstemp('orig_db')
os.close(handle)
# Set up a database with the two-field schema.
old_lib = TestDatabase2(self.libfile)
old_lib = TestDatabase2(cls.orig_libfile)
# Add an item to the old library.
old_lib._connection().execute(
@ -133,6 +148,15 @@ class MigrationTest(unittest.TestCase):
old_lib._connection().commit()
del old_lib
@classmethod
def tearDownClass(cls):
os.remove(cls.orig_libfile)
def setUp(self):
handle, self.libfile = mkstemp('db')
os.close(handle)
shutil.copyfile(self.orig_libfile, self.libfile)
def tearDown(self):
os.remove(self.libfile)
@ -274,6 +298,40 @@ class ModelTest(unittest.TestCase):
model2.load()
self.assertNotIn('flex_field', model2)
def test_check_db_fails(self):
with self.assertRaisesRegexp(ValueError, 'no database'):
dbcore.Model()._check_db()
with self.assertRaisesRegexp(ValueError, 'no id'):
TestModel1(self.db)._check_db()
dbcore.Model(self.db)._check_db(need_id=False)
def test_missing_field(self):
with self.assertRaises(AttributeError):
TestModel1(self.db).nonExistingKey
def test_computed_field(self):
model = TestModelWithGetters()
self.assertEqual(model.aComputedField, 'thing')
with self.assertRaisesRegexp(KeyError, 'computed field .+ deleted'):
del model.aComputedField
def test_items(self):
model = TestModel1(self.db)
model.id = 5
self.assertEqual({('id', 5), ('field_one', None)},
set(model.items()))
def test_delete_internal_field(self):
model = dbcore.Model()
del model._db
with self.assertRaises(AttributeError):
model._db
def test_parse_nonstring(self):
with self.assertRaisesRegexp(TypeError, "must be a string"):
dbcore.Model._parse(None, 42)
class FormatTest(unittest.TestCase):
def test_format_fixed_field(self):
@ -588,6 +646,15 @@ class ResultsIteratorTest(unittest.TestCase):
objs = self.db._fetch(TestModel1)
self.assertEqual(len(objs), 2)
def test_out_of_range(self):
objs = self.db._fetch(TestModel1)
with self.assertRaises(IndexError):
objs[100]
def test_no_results(self):
self.assertIsNone(self.db._fetch(
TestModel1, dbcore.query.FalseQuery()).get())
def suite():
return unittest.TestLoader().loadTestsFromName(__name__)

View file

@ -17,6 +17,7 @@ from __future__ import (division, absolute_import, print_function,
import codecs
from mock import patch
from test import _common
from test._common import unittest
from test.helper import TestHelper, control_stdin
@ -62,6 +63,7 @@ class ModifyFileMocker(object):
f.write(contents)
@_common.slow_test()
class EditCommandTest(unittest.TestCase, TestHelper):
""" Black box tests for `beetsplug.edit`. Command line interaction is
simulated using `test.helper.control_stdin()`, and yaml editing via an

View file

@ -236,6 +236,7 @@ class ImportHelper(TestHelper):
self.assertEqual(len(os.listdir(self.libdir)), 0)
@_common.slow_test()
class NonAutotaggedImportTest(_common.TestCase, ImportHelper):
def setUp(self):
self.setup_beets(disk=True)

View file

@ -12,6 +12,7 @@ from StringIO import StringIO
import beets.logging as blog
from beets import plugins, ui
import beetsplug
from test import _common
from test._common import unittest, TestCase
from test import helper
@ -163,6 +164,7 @@ class LoggingLevelTest(unittest.TestCase, helper.TestHelper):
self.assertIn('dummy: debug import_stage', logs)
@_common.slow_test()
class ConcurrentEventsTest(TestCase, helper.TestHelper):
"""Similar to LoggingLevelTest but lower-level and focused on multiple
events interaction. Since this is a bit heavy we don't do it in

View file

@ -17,7 +17,7 @@ from __future__ import (division, absolute_import, print_function,
unicode_literals)
from mock import Mock
from mock import Mock, patch, call, ANY
from test._common import unittest
from test.helper import TestHelper
@ -44,6 +44,44 @@ class MPDStatsTest(unittest.TestCase, TestHelper):
self.assertFalse(mpdstats.update_rating(item, True))
self.assertFalse(mpdstats.update_rating(None, True))
def test_get_item(self):
ITEM_PATH = '/foo/bar.flac'
item = Item(title='title', path=ITEM_PATH, id=1)
item.add(self.lib)
log = Mock()
mpdstats = MPDStats(self.lib, log)
self.assertEqual(str(mpdstats.get_item(ITEM_PATH)), str(item))
self.assertIsNone(mpdstats.get_item('/some/non-existing/path'))
self.assertIn('item not found:', log.info.call_args[0][0])
FAKE_UNKNOWN_STATE = 'some-unknown-one'
STATUSES = [{'state': FAKE_UNKNOWN_STATE},
{'state': 'pause'},
{'state': 'play', 'songid': 1, 'time': '0:1'},
{'state': 'stop'}]
EVENTS = [["player"]] * (len(STATUSES) - 1) + [KeyboardInterrupt]
ITEM_PATH = '/foo/bar.flac'
@patch("beetsplug.mpdstats.MPDClientWrapper", return_value=Mock(**{
"events.side_effect": EVENTS, "status.side_effect": STATUSES,
"playlist.return_value": {1: ITEM_PATH}}))
def test_run_MPDStats(self, mpd_mock):
item = Item(title='title', path=self.ITEM_PATH, id=1)
item.add(self.lib)
log = Mock()
try:
MPDStats(self.lib, log).run()
except KeyboardInterrupt:
pass
log.debug.assert_has_calls(
[call(u'unhandled status "{0}"', ANY)])
log.info.assert_has_calls(
[call(u'pause'), call(u'playing {0}', ANY), call(u'stop')])
def suite():
return unittest.TestLoader().loadTestsFromName(__name__)

View file

@ -594,6 +594,7 @@ class InputTest(_common.TestCase):
self.assertEqual(album, u'\xc2me')
@_common.slow_test()
class ConfigTest(unittest.TestCase, TestHelper):
def setUp(self):
self.setup_beets()
@ -1035,6 +1036,7 @@ class PathFormatTest(_common.TestCase):
self.assertEqual(pf[1:], default_formats)
@_common.slow_test()
class PluginTest(_common.TestCase):
def test_plugin_command_from_pluginpath(self):
config['pluginpath'] = [os.path.join(_common.RSRC, 'beetsplug')]
@ -1042,6 +1044,7 @@ class PluginTest(_common.TestCase):
ui._raw_main(['test'])
@_common.slow_test()
class CompletionTest(_common.TestCase):
def test_completion(self):
# Load plugin commands