Merge master back in to fork

This commit is contained in:
Jack Wilsdon 2016-04-28 04:15:02 +01:00
commit 7c9440cf13
40 changed files with 322 additions and 143 deletions

View file

@ -1,11 +1,15 @@
.. image:: https://travis-ci.org/beetbox/beets.svg?branch=master
:target: https://travis-ci.org/beetbox/beets
.. image:: http://img.shields.io/pypi/v/beets.svg
:target: https://pypi.python.org/pypi/beets
.. image:: https://img.shields.io/pypi/dw/beets.svg
:target: https://pypi.python.org/pypi/beets#downloads
.. image:: http://img.shields.io/codecov/c/github/beetbox/beets.svg
:target: https://codecov.io/github/beetbox/beets
.. image:: http://img.shields.io/pypi/v/beets.svg
:target: https://pypi.python.org/pypi/beets
.. image:: https://travis-ci.org/beetbox/beets.svg?branch=master
:target: https://travis-ci.org/beetbox/beets
Beets is the media library management system for obsessive-compulsive music
geeks.

View file

@ -297,7 +297,7 @@ class Distance(object):
self._penalties = {}
@LazyClassProperty
def _weights(cls):
def _weights(cls): # noqa
"""A dictionary from keys to floating-point weights.
"""
weights_view = config['match']['distance_weights']

View file

@ -209,13 +209,13 @@ class Model(object):
# Essential field accessors.
@classmethod
def _type(self, key):
def _type(cls, key):
"""Get the type of a field, a `Type` instance.
If the field has no explicit type, it is given the base `Type`,
which does no conversion.
"""
return self._fields.get(key) or self._types.get(key) or types.DEFAULT
return cls._fields.get(key) or cls._types.get(key) or types.DEFAULT
def __getitem__(self, key):
"""Get the value for a field. Raise a KeyError if the field is
@ -274,11 +274,11 @@ class Model(object):
return base_keys
@classmethod
def all_keys(self):
def all_keys(cls):
"""Get a list of available keys for objects of this type.
Includes fixed and computed fields.
"""
return list(self._fields) + self._getters().keys()
return list(cls._fields) + cls._getters().keys()
# Act like a dictionary.

View file

@ -146,9 +146,9 @@ class NoneQuery(FieldQuery):
return self.field + " IS NULL", ()
@classmethod
def match(self, item):
def match(cls, item):
try:
return item[self.field] is None
return item[cls.field] is None
except KeyError:
return True
@ -841,7 +841,7 @@ class SlowFieldSort(FieldSort):
class NullSort(Sort):
"""No sorting. Leave results unsorted."""
def sort(items):
def sort(self, items):
return items
def __nonzero__(self):

View file

@ -1466,6 +1466,35 @@ class DefaultTemplateFunctions(object):
self.lib._memotable[memokey] = res
return res
@staticmethod
def tmpl_first(s, count=1, skip=0, sep=u'; ', join_str=u'; '):
""" Gets the item(s) from x to y in a string separated by something
and join then with something
:param s: the string
:param count: The number of items included
:param skip: The number of items skipped
:param sep: the separator. Usually is '; ' (default) or '/ '
:param join_str: the string which will join the items, default '; '.
"""
skip = int(skip)
count = skip + int(count)
return join_str.join(s.split(sep)[skip:count])
def tmpl_ifdef(self, field, trueval=u'', falseval=u''):
""" If field exists return trueval or the field (default)
otherwise, emit return falseval (if provided).
:param field: The name of the field
:param trueval: The string if the condition is true
:param falseval: The string if the condition is false
:return: The string, based on condition
"""
if self.item.formatted().get(field):
return trueval if trueval else self.item.formatted().get(field)
else:
return falseval
# Get the name of tmpl_* functions in the above class.
DefaultTemplateFunctions._func_names = \

View file

@ -126,7 +126,7 @@ my_manager = copy(Logger.manager)
my_manager.loggerClass = BeetsLogger
def getLogger(name=None):
def getLogger(name=None): # noqa
if name:
return my_manager.getLogger(name)
else:

View file

@ -71,6 +71,15 @@ class UserError(Exception):
# Encoding utilities.
def _in_encoding():
"""Get the encoding to use for *inputting* strings to the console.
"""
try:
return sys.stdin.encoding or 'utf-8'
except LookupError:
# TODO: create user config
return 'utf-8'
def _out_encoding():
"""Get the encoding to use for *outputting* strings to the console.
@ -193,7 +202,7 @@ def input_(prompt=None):
except EOFError:
raise UserError(u'stdin stream ended while input required')
return resp.decode(sys.stdin.encoding or 'utf8', 'ignore')
return resp.decode(_in_encoding(), 'ignore')
def input_options(options, require=False, prompt=None, fallback_prompt=None,

View file

@ -149,15 +149,15 @@ class Shareable(type):
lazily-created shared instance of ``MyClass`` while calling
``MyClass()`` to construct a new object works as usual.
"""
def __init__(cls, name, bases, dict):
super(Shareable, cls).__init__(name, bases, dict)
cls._instance = None
def __init__(self, name, bases, dict):
super(Shareable, self).__init__(name, bases, dict)
self._instance = None
@property
def shared(cls):
if cls._instance is None:
cls._instance = cls()
return cls._instance
def shared(self):
if self._instance is None:
self._instance = self()
return self._instance
class ArtResizer(object):
@ -218,18 +218,18 @@ class ArtResizer(object):
@staticmethod
def _check_method():
"""Return a tuple indicating an available method and its version."""
version = has_IM()
version = get_im_version()
if version:
return IMAGEMAGICK, version
version = has_PIL()
version = get_pil_version()
if version:
return PIL, version
return WEBPROXY, (0)
def has_IM():
def get_im_version():
"""Return Image Magick version or None if it is unavailable
Try invoking ImageMagick's "convert"."""
try:
@ -248,7 +248,7 @@ def has_IM():
return None
def has_PIL():
def get_pil_version():
"""Return Image Magick version or None if it is unavailable
Try importing PIL."""
try:

View file

@ -136,7 +136,7 @@ class ConfigSource(dict):
)
@classmethod
def of(self, value):
def of(cls, value):
"""Given either a dictionary or a `ConfigSource` object, return
a `ConfigSource` object. This lets a function accept either type
of object as an argument.

View file

@ -26,6 +26,9 @@ from itertools import tee, izip
from beets import plugins, ui
ASCII_DIGITS = string.digits + string.ascii_lowercase
class BucketError(Exception):
pass
@ -155,23 +158,23 @@ def build_alpha_spans(alpha_spans_str, alpha_regexs):
[from...to]
"""
spans = []
ASCII_DIGITS = string.digits + string.ascii_lowercase
for elem in alpha_spans_str:
if elem in alpha_regexs:
spans.append(re.compile(alpha_regexs[elem]))
else:
bucket = sorted([x for x in elem.lower() if x.isalnum()])
if bucket:
beginIdx = ASCII_DIGITS.index(bucket[0])
endIdx = ASCII_DIGITS.index(bucket[-1])
begin_index = ASCII_DIGITS.index(bucket[0])
end_index = ASCII_DIGITS.index(bucket[-1])
else:
raise ui.UserError(u"invalid range defined for alpha bucket "
u"'%s': no alphanumeric character found" %
elem)
spans.append(
re.compile(
"^[" + ASCII_DIGITS[beginIdx:endIdx + 1] +
ASCII_DIGITS[beginIdx:endIdx + 1].upper() + "]"
"^[" + ASCII_DIGITS[begin_index:end_index + 1] +
ASCII_DIGITS[begin_index:end_index + 1].upper() + "]"
)
)
return spans

View file

@ -119,7 +119,7 @@ class ExportFormat(object):
"""The output format type"""
@classmethod
def factory(self, type, **kwargs):
def factory(cls, type, **kwargs):
if type == "json":
if kwargs['file_path']:
return JsonFileFormat(**kwargs)

View file

@ -197,7 +197,7 @@ class ArtSource(RequestMixin):
raise NotImplementedError()
def _candidate(self, **kwargs):
return Candidate(source=self.NAME, log=self._log, **kwargs)
return Candidate(source=self, log=self._log, **kwargs)
def fetch_image(self, candidate, extra):
raise NotImplementedError()
@ -395,12 +395,12 @@ class FanartTV(RemoteArtSource):
return
matches = []
# can there be more than one releasegroupid per responce?
for mb_releasegroupid in data.get(u'albums', dict()):
if album.mb_releasegroupid == mb_releasegroupid:
# note: there might be more art referenced, e.g. cdart
matches.extend(
data[u'albums'][mb_releasegroupid][u'albumcover'])
# can there be more than one releasegroupid per response?
for mbid, art in data.get(u'albums', dict()).items():
# there might be more art referenced, e.g. cdart, and an albumcover
# might not be present, even if the request was succesful
if album.mb_releasegroupid == mbid and u'albumcover' in art:
matches.extend(art[u'albumcover'])
# can this actually occur?
else:
self._log.debug(u'fanart.tv: unexpected mb_releasegroupid in '
@ -454,7 +454,7 @@ class Wikipedia(RemoteArtSource):
NAME = u"Wikipedia (queried through DBpedia)"
DBPEDIA_URL = 'http://dbpedia.org/sparql'
WIKIPEDIA_URL = 'http://en.wikipedia.org/w/api.php'
SPARQL_QUERY = '''PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
SPARQL_QUERY = u'''PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
PREFIX dbpprop: <http://dbpedia.org/property/>
PREFIX owl: <http://dbpedia.org/ontology/>
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
@ -660,9 +660,9 @@ class FetchArtPlugin(plugins.BeetsPlugin, RequestMixin):
def __init__(self):
super(FetchArtPlugin, self).__init__()
# Holds paths to downloaded images between fetching them and
# placing them in the filesystem.
self.art_paths = {}
# Holds candidates corresponding to downloaded images between
# fetching them and placing them in the filesystem.
self.art_candidates = {}
self.config.add({
'auto': True,
@ -675,7 +675,8 @@ class FetchArtPlugin(plugins.BeetsPlugin, RequestMixin):
'coverart', 'itunes', 'amazon', 'albumart'],
'google_key': None,
'google_engine': u'001442825323518660753:hrh5ch1gjzm',
'fanarttv_key': None
'fanarttv_key': None,
'store_source': False,
})
self.config['google_key'].redact = True
self.config['fanarttv_key'].redact = True
@ -703,6 +704,7 @@ class FetchArtPlugin(plugins.BeetsPlugin, RequestMixin):
cover_names = self.config['cover_names'].as_str_seq()
self.cover_names = map(util.bytestring_path, cover_names)
self.cautious = self.config['cautious'].get(bool)
self.store_source = self.config['store_source'].get(bool)
self.src_removed = (config['import']['delete'].get(bool) or
config['import']['move'].get(bool))
@ -753,19 +755,28 @@ class FetchArtPlugin(plugins.BeetsPlugin, RequestMixin):
candidate = self.art_for_album(task.album, task.paths, local)
if candidate:
self.art_paths[task] = candidate.path
self.art_candidates[task] = candidate
def _set_art(self, album, candidate, delete=False):
album.set_art(candidate.path, delete)
if self.store_source:
# store the source of the chosen artwork in a flexible field
self._log.debug(
u"Storing art_source for {0.albumartist} - {0.album}",
album)
album.art_source = SOURCE_NAMES[type(candidate.source)]
album.store()
# Synchronous; after music files are put in place.
def assign_art(self, session, task):
"""Place the discovered art in the filesystem."""
if task in self.art_paths:
path = self.art_paths.pop(task)
if task in self.art_candidates:
candidate = self.art_candidates.pop(task)
self._set_art(task.album, candidate, not self.src_removed)
album = task.album
album.set_art(path, not self.src_removed)
album.store()
if self.src_removed:
task.prune(path)
task.prune(candidate.path)
# Manual album art fetching.
def commands(self):
@ -842,8 +853,7 @@ class FetchArtPlugin(plugins.BeetsPlugin, RequestMixin):
candidate = self.art_for_album(album, local_paths)
if candidate:
album.set_art(candidate.path, False)
album.store()
self._set_art(album, candidate)
message = ui.colorize('text_success', u'found album art')
else:
message = ui.colorize('text_error', u'no art found')

View file

@ -26,13 +26,13 @@ import difflib
class FuzzyQuery(StringFieldQuery):
@classmethod
def string_match(self, pattern, val):
def string_match(cls, pattern, val):
# smartcase
if pattern.islower():
val = val.lower()
queryMatcher = difflib.SequenceMatcher(None, pattern, val)
query_matcher = difflib.SequenceMatcher(None, pattern, val)
threshold = config['fuzzy']['threshold'].as_number()
return queryMatcher.quick_ratio() >= threshold
return query_matcher.quick_ratio() >= threshold
class FuzzyPlugin(BeetsPlugin):

View file

@ -334,8 +334,8 @@ class LyricsWiki(SymbolsReplaced):
# Get the HTML fragment inside the appropriate HTML element and then
# extract the text from it.
html_frag = extract_text_in(unescape(html), u"<div class='lyricbox'>")
lyrics = scrape_lyrics_from_html(html_frag)
html_frag = extract_text_in(html, u"<div class='lyricbox'>")
lyrics = _scrape_strip_cruft(html_frag, True)
if lyrics and 'Unfortunately, we are not licensed' not in lyrics:
return lyrics
@ -415,7 +415,12 @@ def scrape_lyrics_from_html(html):
"""Scrape lyrics from a URL. If no lyrics can be found, return None
instead.
"""
from bs4 import SoupStrainer, BeautifulSoup
try:
from bs4 import SoupStrainer, BeautifulSoup
except ImportError:
# TODO: refactor the plugin to get access to a logger here and log
# a warning
return None
if not html:
return None
@ -670,11 +675,19 @@ class LyricsPlugin(plugins.BeetsPlugin):
lyrics = u"\n\n---\n\n".join([l for l in lyrics if l])
has_langdetect = False
if self.config['bing_client_secret'].get():
try:
from langdetect import detect
has_langdetect = True
except ImportError:
self._log.warn(u'To use bing translations, you need to '
u'install the langdetect module. See the '
u'documentation for further details.')
if lyrics:
self._log.info(u'fetched lyrics: {0}', item)
if self.config['bing_client_secret'].get():
from langdetect import detect
if has_langdetect:
lang_from = detect(lyrics)
if self.config['bing_lang_to'].get() != lang_from and (
not self.config['bing_lang_from'] or (
@ -692,7 +705,6 @@ class LyricsPlugin(plugins.BeetsPlugin):
item.lyrics = lyrics
if write:
item.try_write()
print(lyrics)
item.store()
def get_lyrics(self, artist, title):

View file

@ -34,7 +34,7 @@ from xdg import BaseDirectory
from beets.plugins import BeetsPlugin
from beets.ui import Subcommand, decargs
from beets import util
from beets.util.artresizer import ArtResizer, has_IM, has_PIL
from beets.util.artresizer import ArtResizer, get_im_version, get_pil_version
BASE_DIR = os.path.join(BaseDirectory.xdg_cache_home, "thumbnails")
@ -92,11 +92,11 @@ class ThumbnailsPlugin(BeetsPlugin):
if not os.path.exists(dir):
os.makedirs(dir)
if has_IM():
if get_im_version():
self.write_metadata = write_metadata_im
tool = "IM"
else:
assert has_PIL() # since we're local
assert get_pil_version() # since we're local
self.write_metadata = write_metadata_pil
tool = "PIL"
self._log.debug(u"using {0} to write metadata", tool)

12
codecov.yml Normal file
View file

@ -0,0 +1,12 @@
# Don't post a comment on pull requests.
comment: off
# I think this disables commit statuses?
coverage:
status:
project:
enabled: no
patch:
enabled: no
changes:
enabled: no

View file

@ -19,8 +19,11 @@ New features:
* :doc:`/plugins/fetchart`: The ``enforce_ratio`` option was enhanced and now
allows specifying a certain deviation that a valid image may have from being
exactly square.
* :doc:`/plugins/fetchart`: The plugin can now optionally save the artwork's
source in a flexible field; for a usecase see the documentation.
* :doc:`/plugins/export`: A new plugin to export the data from queries to a
json format. Thanks to :user:`GuilhermeHideki`.
* :doc:`/reference/pathformat`: new functions: %first{} and %ifdef{}
* New :doc:`/plugins/hook` that allows commands to be executed when an event is
emitted by beets. :bug:`1561` :bug:`1603`
@ -41,6 +44,8 @@ Fixes:
guess the URL for lyrics. :bug:`1880`
* :doc:`/plugins/edit`: Fail gracefully when the configured text editor
command can't be invoked. :bug:`1927`
* :doc:`/plugins/fetchart`: Fix a crash in the Wikipedia backend on non-ASCII
artist and album names. :bug:`1960`
1.3.17 (February 7, 2016)

View file

@ -62,6 +62,9 @@ file. The available options are:
Default: The `beets custom search engine`_, which searches the entire web.
**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.
Default: ``no``.
Note: ``minwidth`` and ``enforce_ratio`` options require either `ImageMagick`_
or `Pillow`_.
@ -182,6 +185,19 @@ personal key will give you earlier access to new art.
.. _on their blog: https://fanart.tv/2015/01/personal-api-keys/
Storing the Artwork's Source
----------------------------
Storing the current artwork's source might be used to narrow down
``fetchart`` commands. For example, if some albums have artwork placed
manually in their directories that should not be replaced by a forced
album art fetch, you could do
``beet fetchart -f ^art_source:filesystem``
The values written to ``art_source`` are the same names used in the ``sources``
configuration value.
Embedding Album Art
-------------------

View file

@ -133,9 +133,9 @@ using `pip`_ by typing::
pip install langdetect
You also need to register for a Microsoft Azure Marketplace free account and
to the `Microsoft Translator API`_. Follow the four steps process, specifically
at step 3 enter `beets`` as *Client ID* and copy/paste the generated
*Client secret*. into your ``bing_client_secret`` configuration, alongside
to the `Microsoft Translator API`_. Follow the four steps process, specifically
at step 3 enter ``beets`` as *Client ID* and copy/paste the generated
*Client secret* into your ``bing_client_secret`` configuration, alongside
``bing_lang_to`` target `language code`_.
.. _langdetect: https://pypi.python.org/pypi/langdetect

View file

@ -435,7 +435,10 @@ later on you will want to re-generate the script.
zsh
```
If you use zsh, take a look at the included `completion script`_.
If you use zsh, take a look at the included `completion script`_. The script
should be placed in a directory that is part of your ``fpath``, and `not`
sourced in your ``.zshrc``. Running ``echo $fpath`` will give you a list of
valid directories.
Another approach is to use zsh's bash completion compatibility. This snippet
defines some bash-specific functions to make this work without errors::

View file

@ -76,6 +76,16 @@ These functions are built in to beets:
* ``%time{date_time,format}``: Return the date and time in any format accepted
by `strftime`_. For example, to get the year some music was added to your
library, use ``%time{$added,%Y}``.
* ``%first{text}``: Returns the first item, separated by ``; ``.
You can use ``%first{text,count,skip}``, where ``count`` is the number of
items (default 1) and ``skip`` is number to skip (default 0). You can also use
``%first{text,count,skip,sep,join}`` where ``sep`` is the separator, like
``;`` or ``/`` and join is the text to concatenate the items.
For example,
* ``%ifdef{field}``, ``%ifdef{field,truetext}`` or
``%ifdef{field,truetext,falsetext}``: If ``field`` exists, then return
``truetext`` or ``field`` (default). Otherwise, returns ``falsetext``.
The ``field`` should be entered without ``$``.
.. _unidecode module: http://pypi.python.org/pypi/Unidecode
.. _strftime: http://docs.python.org/2/library/time.html#time.strftime

View file

@ -167,11 +167,11 @@ class TestCase(unittest.TestCase):
beets.config.clear()
beets.config._materialized = False
def assertExists(self, path):
def assertExists(self, path): # noqa
self.assertTrue(os.path.exists(path),
u'file does not exist: {!r}'.format(path))
def assertNotExists(self, path):
def assertNotExists(self, path): # noqa
self.assertFalse(os.path.exists(path),
u'file exists: {!r}'.format((path)))

View file

@ -315,6 +315,23 @@ class FanartTVTest(UseThePlugin):
}
}
}"""
RESPONSE_NO_ART = u"""{
"name": "artistname",
"mbid_id": "artistid",
"albums": {
"thereleasegroupid": {
"cdart": [
{
"id": "123",
"url": "http://example.com/4.jpg",
"likes": "0",
"disc": "1",
"size": "1000"
}
]
}
}
}"""
RESPONSE_ERROR = u"""{
"status": "error",
"error message": "the error message"
@ -355,6 +372,14 @@ class FanartTVTest(UseThePlugin):
with self.assertRaises(StopIteration):
next(self.source.get(album, self.extra))
def test_fanarttv_only_other_images(self):
# The source used to fail when there were images present, but no cover
album = _common.Bag(mb_releasegroupid=u'thereleasegroupid')
self.mock_response(fetchart.FanartTV.API_ALBUMS + u'thereleasegroupid',
self.RESPONSE_NO_ART)
with self.assertRaises(StopIteration):
next(self.source.get(album, self.extra))
@_common.slow_test()
class ArtImporterTest(UseThePlugin):
@ -493,7 +518,7 @@ class ArtForAlbumTest(UseThePlugin):
fetchart.FileSystem.get = self.old_fs_source_get
super(ArtForAlbumTest, self).tearDown()
def _assertImageIsValidArt(self, image_file, should_exist):
def _assertImageIsValidArt(self, image_file, should_exist): # noqa
self.assertExists(image_file)
self.image_file = image_file
@ -506,7 +531,7 @@ class ArtForAlbumTest(UseThePlugin):
else:
self.assertIsNone(candidate)
def _assertImageResized(self, image_file, should_resize):
def _assertImageResized(self, image_file, should_resize): # noqa
self.image_file = image_file
with patch.object(ArtResizer.shared, 'resize') as mock_resize:
self.plugin.art_for_album(self.album, [''], True)

View file

@ -940,13 +940,13 @@ 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)
OrderedEnumClass = match.OrderedEnum('OrderedEnumTest', ['a', 'b', 'c']) # noqa
self.assertLess(OrderedEnumClass.a, OrderedEnumClass.b)
self.assertLess(OrderedEnumClass.a, OrderedEnumClass.c)
self.assertLess(OrderedEnumClass.b, OrderedEnumClass.c)
self.assertGreater(OrderedEnumClass.b, OrderedEnumClass.a)
self.assertGreater(OrderedEnumClass.c, OrderedEnumClass.a)
self.assertGreater(OrderedEnumClass.c, OrderedEnumClass.b)
def suite():

View file

@ -40,7 +40,7 @@ class TestHelper(helper.TestHelper):
return u'sh -c "cp \'$source\' \'$dest\'; ' \
u'printf {0} >> \'$dest\'"'.format(tag)
def assertFileTag(self, path, tag):
def assertFileTag(self, path, tag): # noqa
"""Assert that the path is a file and the files content ends with `tag`.
"""
self.assertTrue(os.path.isfile(path),
@ -50,7 +50,7 @@ class TestHelper(helper.TestHelper):
self.assertEqual(f.read(), tag,
u'{0} is not tagged with {1}'.format(path, tag))
def assertNoFileTag(self, path, tag):
def assertNoFileTag(self, path, tag): # noqa
"""Assert that the path is a file and the files content does not
end with `tag`.
"""

View file

@ -62,14 +62,14 @@ class DateIntervalTest(unittest.TestCase):
self.assertContains('..', date=datetime.min)
self.assertContains('..', '1000-01-01T00:00:00')
def assertContains(self, interval_pattern, date_pattern=None, date=None):
def assertContains(self, interval_pattern, date_pattern=None, date=None): # noqa
if date is None:
date = _date(date_pattern)
(start, end) = _parse_periods(interval_pattern)
interval = DateInterval.from_periods(start, end)
self.assertTrue(interval.contains(date))
def assertExcludes(self, interval_pattern, date_pattern):
def assertExcludes(self, interval_pattern, date_pattern): # noqa
date = _date(date_pattern)
(start, end) = _parse_periods(interval_pattern)
interval = DateInterval.from_periods(start, end)

View file

@ -71,7 +71,7 @@ class ModifyFileMocker(object):
class EditMixin(object):
"""Helper containing some common functionality used for the Edit tests."""
def assertItemFieldsModified(self, library_items, items, fields=[],
def assertItemFieldsModified(self, library_items, items, fields=[], # noqa
allowed=['path']):
"""Assert that items in the library (`lib_items`) have different values
on the specified `fields` (and *only* on those fields), compared to
@ -133,7 +133,7 @@ class EditCommandTest(unittest.TestCase, TestHelper, EditMixin):
self.teardown_beets()
self.unload_plugins()
def assertCounts(self, album_count=ALBUM_COUNT, track_count=TRACK_COUNT,
def assertCounts(self, album_count=ALBUM_COUNT, track_count=TRACK_COUNT, # noqa
write_call_count=TRACK_COUNT, title_starts_with=''):
"""Several common assertions on Album, Track and call counts."""
self.assertEqual(len(self.lib.albums()), album_count)

View file

@ -67,7 +67,7 @@ class ImportAddedTest(unittest.TestCase, ImportHelper):
self.teardown_beets()
self.matcher.restore()
def findMediaFile(self, item):
def find_media_file(self, item):
"""Find the pre-import MediaFile for an Item"""
for m in self.media_files:
if m.title.replace('Tag', 'Applied') == item.title:
@ -75,11 +75,11 @@ class ImportAddedTest(unittest.TestCase, ImportHelper):
raise AssertionError(u"No MediaFile found for Item " +
util.displayable_path(item.path))
def assertEqualTimes(self, first, second, msg=None):
def assertEqualTimes(self, first, second, msg=None): # noqa
"""For comparing file modification times at a sufficient precision"""
self.assertAlmostEqual(first, second, places=4, msg=msg)
def assertAlbumImport(self):
def assertAlbumImport(self): # noqa
self.importer.run()
album = self.lib.albums().get()
self.assertEqual(album.added, self.min_mtime)
@ -102,7 +102,7 @@ class ImportAddedTest(unittest.TestCase, ImportHelper):
self.assertEqual(album.added, self.min_mtime)
for item in album.items():
self.assertEqualTimes(item.added, self.min_mtime)
mediafile_mtime = os.path.getmtime(self.findMediaFile(item).path)
mediafile_mtime = os.path.getmtime(self.find_media_file(item).path)
self.assertEqualTimes(item.mtime, mediafile_mtime)
self.assertEqualTimes(os.path.getmtime(item.path),
mediafile_mtime)
@ -133,7 +133,7 @@ class ImportAddedTest(unittest.TestCase, ImportHelper):
self.config['import']['singletons'] = True
self.importer.run()
for item in self.lib.items():
mfile = self.findMediaFile(item)
mfile = self.find_media_file(item)
self.assertEqualTimes(item.added, os.path.getmtime(mfile.path))
def test_import_singletons_with_preserved_mtimes(self):
@ -141,7 +141,7 @@ class ImportAddedTest(unittest.TestCase, ImportHelper):
self.config['importadded']['preserve_mtimes'] = True
self.importer.run()
for item in self.lib.items():
mediafile_mtime = os.path.getmtime(self.findMediaFile(item).path)
mediafile_mtime = os.path.getmtime(self.find_media_file(item).path)
self.assertEqualTimes(item.added, mediafile_mtime)
self.assertEqualTimes(item.mtime, mediafile_mtime)
self.assertEqualTimes(os.path.getmtime(item.path),

View file

@ -125,14 +125,14 @@ class AutotagStub(object):
artist = artist.replace('Tag', 'Applied') + id
album = album.replace('Tag', 'Applied') + id
trackInfos = []
track_infos = []
for i in range(tracks - missing):
trackInfos.append(self._make_track_match(artist, album, i + 1))
track_infos.append(self._make_track_match(artist, album, i + 1))
return AlbumInfo(
artist=artist,
album=album,
tracks=trackInfos,
tracks=track_infos,
va=False,
album_id=u'albumid' + id,
artist_id=u'artistid' + id,

View file

@ -167,16 +167,16 @@ class LastGenrePluginTest(unittest.TestCase, TestHelper):
self.assertEqual(res, [u'pop'])
def test_get_genre(self):
MOCK_GENRES = {'track': u'1', 'album': u'2', 'artist': u'3'}
mock_genres = {'track': u'1', 'album': u'2', 'artist': u'3'}
def mock_fetch_track_genre(self, obj=None):
return MOCK_GENRES['track']
return mock_genres['track']
def mock_fetch_album_genre(self, obj):
return MOCK_GENRES['album']
return mock_genres['album']
def mock_fetch_artist_genre(self, obj):
return MOCK_GENRES['artist']
return mock_genres['artist']
lastgenre.LastGenrePlugin.fetch_track_genre = mock_fetch_track_genre
lastgenre.LastGenrePlugin.fetch_album_genre = mock_fetch_album_genre
@ -184,7 +184,7 @@ class LastGenrePluginTest(unittest.TestCase, TestHelper):
self._setup_config(whitelist=False)
item = _common.item()
item.genre = MOCK_GENRES['track']
item.genre = mock_genres['track']
config['lastgenre'] = {'force': False}
res = self.plugin._get_genre(item)
@ -192,17 +192,17 @@ class LastGenrePluginTest(unittest.TestCase, TestHelper):
config['lastgenre'] = {'force': True, 'source': u'track'}
res = self.plugin._get_genre(item)
self.assertEqual(res, (MOCK_GENRES['track'], u'track'))
self.assertEqual(res, (mock_genres['track'], u'track'))
config['lastgenre'] = {'source': u'album'}
res = self.plugin._get_genre(item)
self.assertEqual(res, (MOCK_GENRES['album'], u'album'))
self.assertEqual(res, (mock_genres['album'], u'album'))
config['lastgenre'] = {'source': u'artist'}
res = self.plugin._get_genre(item)
self.assertEqual(res, (MOCK_GENRES['artist'], u'artist'))
self.assertEqual(res, (mock_genres['artist'], u'artist'))
MOCK_GENRES['artist'] = None
mock_genres['artist'] = None
res = self.plugin._get_genre(item)
self.assertEqual(res, (item.genre, u'original'))

View file

@ -617,6 +617,46 @@ class DestinationFunctionTest(_common.TestCase, PathFormattingMixin):
self._setf(u'%foo{bar}')
self._assert_dest('/base/%foo{bar}')
def test_if_def_field_return_self(self):
self.i.bar = 3
self._setf(u'%ifdef{bar}')
self._assert_dest('/base/3')
def test_if_def_field_not_defined(self):
self._setf(u' %ifdef{bar}/$artist')
self._assert_dest('/base/the artist')
def test_if_def_field_not_defined_2(self):
self._setf(u'$artist/%ifdef{bar}')
self._assert_dest('/base/the artist')
def test_if_def_true(self):
self._setf(u'%ifdef{artist,cool}')
self._assert_dest('/base/cool')
def test_if_def_true_complete(self):
self.i.series = "Now"
self._setf(u'%ifdef{series,$series Series,Albums}/$album')
self._assert_dest('/base/Now Series/the album')
def test_if_def_false_complete(self):
self._setf(u'%ifdef{plays,$plays,not_played}')
self._assert_dest('/base/not_played')
def test_first(self):
self.i.genres = "Pop; Rock; Classical Crossover"
self._setf(u'%first{$genres}')
self._assert_dest('/base/Pop')
def test_first_skip(self):
self.i.genres = "Pop; Rock; Classical Crossover"
self._setf(u'%first{$genres,1,2}')
self._assert_dest('/base/Classical Crossover')
def test_first_different_sep(self):
self._setf(u'%first{Alice / Bob / Eve,2,0, / , & }')
self._assert_dest('/base/Alice & Bob')
class DisambiguationTest(_common.TestCase, PathFormattingMixin):
def setUp(self):

View file

@ -365,14 +365,14 @@ class LyricsGooglePluginTest(unittest.TestCase):
not present in the title."""
s = self.source
url = s['url'] + s['path']
urlTitle = u'example.com | Beats song by John doe'
url_title = u'example.com | Beats song by John doe'
# very small diffs (typo) are ok eg 'beats' vs 'beets' with same artist
self.assertEqual(google.is_page_candidate(url, urlTitle, s['title'],
self.assertEqual(google.is_page_candidate(url, url_title, s['title'],
s['artist']), True, url)
# reject different title
urlTitle = u'example.com | seets bong lyrics by John doe'
self.assertEqual(google.is_page_candidate(url, urlTitle, s['title'],
url_title = u'example.com | seets bong lyrics by John doe'
self.assertEqual(google.is_page_candidate(url, url_title, s['title'],
s['artist']), False, url)
def test_is_page_candidate_special_chars(self):

View file

@ -168,7 +168,7 @@ class ImageStructureTestMixin(ArtTestMixin):
self.assertEqual(cover.desc, u'album cover')
self.assertEqual(mediafile.art, cover.data)
def assertExtendedImageAttributes(self, image, **kwargs):
def assertExtendedImageAttributes(self, image, **kwargs): # noqa
"""Ignore extended image attributes in the base tests.
"""
pass
@ -177,7 +177,7 @@ class ImageStructureTestMixin(ArtTestMixin):
class ExtendedImageStructureTestMixin(ImageStructureTestMixin):
"""Checks for additional attributes in the image structure."""
def assertExtendedImageAttributes(self, image, desc=None, type=None):
def assertExtendedImageAttributes(self, image, desc=None, type=None): # noqa
self.assertEqual(image.desc, desc)
self.assertEqual(image.type, type)
@ -660,7 +660,7 @@ class ReadWriteTestBase(ArtTestMixin, GenreListTestMixin,
self.assertIsNone(mediafile.date)
self.assertIsNone(mediafile.year)
def assertTags(self, mediafile, tags):
def assertTags(self, mediafile, tags): # noqa
errors = []
for key, value in tags.items():
try:

View file

@ -43,14 +43,14 @@ class MPDStatsTest(unittest.TestCase, TestHelper):
self.assertFalse(mpdstats.update_rating(None, True))
def test_get_item(self):
ITEM_PATH = '/foo/bar.flac'
item = Item(title=u'title', path=ITEM_PATH, id=1)
item_path = '/foo/bar.flac'
item = Item(title=u'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.assertEqual(str(mpdstats.get_item(item_path)), str(item))
self.assertIsNone(mpdstats.get_item('/some/non-existing/path'))
self.assertIn(u'item not found:', log.info.call_args[0][0])
@ -60,13 +60,13 @@ class MPDStatsTest(unittest.TestCase, TestHelper):
{'state': u'play', 'songid': 1, 'time': u'0:1'},
{'state': u'stop'}]
EVENTS = [["player"]] * (len(STATUSES) - 1) + [KeyboardInterrupt]
ITEM_PATH = '/foo/bar.flac'
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=u'title', path=self.ITEM_PATH, id=1)
"playlist.return_value": {1: item_path}}))
def test_run_mpdstats(self, mpd_mock):
item = Item(title=u'title', path=self.item_path, id=1)
item.add(self.lib)
log = Mock()

View file

@ -38,7 +38,7 @@ class PermissionsPluginTest(unittest.TestCase, TestHelper):
def test_failing_to_set_permissions(self):
self.do_thing(False)
def do_thing(self, expectSuccess):
def do_thing(self, expect_success):
def get_stat(v):
return os.stat(
os.path.join(self.temp_dir, 'import', *v)).st_mode & 0o777
@ -53,14 +53,14 @@ class PermissionsPluginTest(unittest.TestCase, TestHelper):
self.importer.run()
item = self.lib.items().get()
self.assertPerms(item.path, 'file', expectSuccess)
self.assertPerms(item.path, 'file', expect_success)
for path in dirs_in_library(self.lib.directory, item.path):
self.assertPerms(path, 'dir', expectSuccess)
self.assertPerms(path, 'dir', expect_success)
def assertPerms(self, path, typ, expectSuccess):
for x in [(True, self.exp_perms[expectSuccess][typ], '!='),
(False, self.exp_perms[not expectSuccess][typ], '==')]:
def assertPerms(self, path, typ, expect_success): # noqa
for x in [(True, self.exp_perms[expect_success][typ], '!='),
(False, self.exp_perms[not expect_success][typ], '==')]:
self.assertEqual(x[0], check_permissions(path, x[1]),
msg=u'{} : {} {} {}'.format(
path, oct(os.stat(path).st_mode), x[2], oct(x[1])))

View file

@ -35,11 +35,11 @@ from beets.library import Library, Item
class TestHelper(helper.TestHelper):
def assertInResult(self, item, results):
def assertInResult(self, item, results): # noqa
result_ids = map(lambda i: i.id, results)
self.assertIn(item.id, result_ids)
def assertNotInResult(self, item, results):
def assertNotInResult(self, item, results): # noqa
result_ids = map(lambda i: i.id, results)
self.assertNotIn(item.id, result_ids)
@ -805,7 +805,7 @@ class NotQueryTest(DummyDataTestCase):
- `test_type_xxx`: tests for the negation of a particular XxxQuery class.
- `test_get_yyy`: tests on query strings (similar to `GetTest`)
"""
def assertNegationProperties(self, q):
def assertNegationProperties(self, q): # noqa
"""Given a Query `q`, assert that:
- q OR not(q) == all items
- q AND not(q) == 0

View file

@ -88,15 +88,15 @@ class SmartPlaylistTest(unittest.TestCase):
for name, (_, sort), _ in spl._unmatched_playlists)
asseq = self.assertEqual # less cluttered code
S = FixedFieldSort # short cut since we're only dealing with this
sort = FixedFieldSort # short cut since we're only dealing with this
asseq(sorts["no_sort"], NullSort())
asseq(sorts["one_sort"], S(u'year'))
asseq(sorts["one_sort"], sort(u'year'))
asseq(sorts["only_empty_sorts"], None)
asseq(sorts["one_non_empty_sort"], S(u'year'))
asseq(sorts["one_non_empty_sort"], sort(u'year'))
asseq(sorts["multiple_sorts"],
MultipleSort([S('year'), S(u'genre', False)]))
MultipleSort([sort('year'), sort(u'genre', False)]))
asseq(sorts["mixed"],
MultipleSort([S('year'), S(u'genre'), S(u'id', False)]))
MultipleSort([sort('year'), sort(u'genre'), sort(u'id', False)]))
def test_matches(self):
spl = SmartPlaylistPlugin()

View file

@ -66,8 +66,8 @@ class ThumbnailsTest(unittest.TestCase, TestHelper):
@patch('beetsplug.thumbnails.os')
@patch('beetsplug.thumbnails.ArtResizer')
@patch('beetsplug.thumbnails.has_IM')
@patch('beetsplug.thumbnails.has_PIL')
@patch('beetsplug.thumbnails.get_im_version')
@patch('beetsplug.thumbnails.get_pil_version')
@patch('beetsplug.thumbnails.GioURI')
def test_check_local_ok(self, mock_giouri, mock_pil, mock_im,
mock_artresizer, mock_os):

View file

@ -233,18 +233,18 @@ class ModifyTest(unittest.TestCase, TestHelper):
def test_selective_modify(self):
title = u"Tracktitle"
album = u"album"
origArtist = u"composer"
newArtist = u"coverArtist"
original_artist = u"composer"
new_artist = u"coverArtist"
for i in range(0, 10):
self.add_item_fixture(title=u"{0}{1}".format(title, i),
artist=origArtist,
artist=original_artist,
album=album)
self.modify_inp('s\ny\ny\ny\nn\nn\ny\ny\ny\ny\nn',
title, u"artist={0}".format(newArtist))
origItems = self.lib.items(u"artist:{0}".format(origArtist))
newItems = self.lib.items(u"artist:{0}".format(newArtist))
self.assertEqual(len(list(origItems)), 3)
self.assertEqual(len(list(newItems)), 7)
title, u"artist={0}".format(new_artist))
original_items = self.lib.items(u"artist:{0}".format(original_artist))
new_items = self.lib.items(u"artist:{0}".format(new_artist))
self.assertEqual(len(list(original_items)), 3)
self.assertEqual(len(list(new_items)), 7)
# Album Tests

View file

@ -48,4 +48,5 @@ commands =
deps =
flake8
flake8-future-import
pep8-naming
commands = flake8 beets beetsplug beet test setup.py docs