diff --git a/README.rst b/README.rst index 1d865ec793..ad477beee1 100644 --- a/README.rst +++ b/README.rst @@ -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. diff --git a/beets/autotag/hooks.py b/beets/autotag/hooks.py index 26b7f58191..5c8e0e2c50 100644 --- a/beets/autotag/hooks.py +++ b/beets/autotag/hooks.py @@ -297,7 +297,7 @@ def __init__(self): self._penalties = {} @LazyClassProperty - def _weights(cls): + def _weights(cls): # noqa """A dictionary from keys to floating-point weights. """ weights_view = config['match']['distance_weights'] diff --git a/beets/dbcore/db.py b/beets/dbcore/db.py index 09a96d8c2e..ba67dd6d1f 100644 --- a/beets/dbcore/db.py +++ b/beets/dbcore/db.py @@ -209,13 +209,13 @@ def _check_db(self, need_id=True): # 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 @@ def keys(self, computed=False): 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. diff --git a/beets/dbcore/query.py b/beets/dbcore/query.py index 8e3b7ca16e..d344f9fef5 100644 --- a/beets/dbcore/query.py +++ b/beets/dbcore/query.py @@ -146,9 +146,9 @@ def col_clause(self): 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 @@ def is_slow(self): class NullSort(Sort): """No sorting. Leave results unsorted.""" - def sort(items): + def sort(self, items): return items def __nonzero__(self): diff --git a/beets/library.py b/beets/library.py index f2c3875b02..ed19386718 100644 --- a/beets/library.py +++ b/beets/library.py @@ -1466,6 +1466,35 @@ def tmpl_aunique(self, keys=None, disam=None): 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 = \ diff --git a/beets/logging.py b/beets/logging.py index e4e628d698..a94da1c62c 100644 --- a/beets/logging.py +++ b/beets/logging.py @@ -126,7 +126,7 @@ class BeetsLogger(ThreadLocalLevelLogger, StrFormatLogger): my_manager.loggerClass = BeetsLogger -def getLogger(name=None): +def getLogger(name=None): # noqa if name: return my_manager.getLogger(name) else: diff --git a/beets/ui/__init__.py b/beets/ui/__init__.py index ac919d7d23..9096a87daa 100644 --- a/beets/ui/__init__.py +++ b/beets/ui/__init__.py @@ -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, diff --git a/beets/util/artresizer.py b/beets/util/artresizer.py index 9d2ee5952b..6970a7da13 100644 --- a/beets/util/artresizer.py +++ b/beets/util/artresizer.py @@ -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 @@ def _can_compare(self): @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: diff --git a/beets/util/confit.py b/beets/util/confit.py index 0b29928c8e..28847e760f 100644 --- a/beets/util/confit.py +++ b/beets/util/confit.py @@ -136,7 +136,7 @@ def __repr__(self): ) @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. diff --git a/beetsplug/bucket.py b/beetsplug/bucket.py index cfd4106d42..21acb1f1e8 100644 --- a/beetsplug/bucket.py +++ b/beetsplug/bucket.py @@ -26,6 +26,9 @@ 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 diff --git a/beetsplug/export.py b/beetsplug/export.py index 93362550f5..641b9fefce 100644 --- a/beetsplug/export.py +++ b/beetsplug/export.py @@ -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) diff --git a/beetsplug/fetchart.py b/beetsplug/fetchart.py index c001246237..a18e362e5d 100644 --- a/beetsplug/fetchart.py +++ b/beetsplug/fetchart.py @@ -197,7 +197,7 @@ def get(self, album, extra): 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 @@ def get(self, album, extra): 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: + SPARQL_QUERY = u'''PREFIX rdf: PREFIX dbpprop: PREFIX owl: PREFIX rdfs: @@ -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 @@ def __init__(self): '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 @@ def __init__(self): 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 @@ def fetch_art(self, session, task): 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 @@ def batch_fetch_art(self, lib, albums, force): 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') diff --git a/beetsplug/fuzzy.py b/beetsplug/fuzzy.py index 4a631a8878..3decdc6027 100644 --- a/beetsplug/fuzzy.py +++ b/beetsplug/fuzzy.py @@ -26,13 +26,13 @@ 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): diff --git a/beetsplug/lyrics.py b/beetsplug/lyrics.py index 6a6bc77297..afd5363945 100644 --- a/beetsplug/lyrics.py +++ b/beetsplug/lyrics.py @@ -334,8 +334,8 @@ def fetch(self, artist, title): # Get the HTML fragment inside the appropriate HTML element and then # extract the text from it. - html_frag = extract_text_in(unescape(html), u"
") - lyrics = scrape_lyrics_from_html(html_frag) + html_frag = extract_text_in(html, u"
") + 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 @@ def fetch_item_lyrics(self, lib, item, write, force): lyrics = u"\n\n---\n\n".join([l for l in lyrics if l]) - if lyrics: - self._log.info(u'fetched lyrics: {0}', item) - if self.config['bing_client_secret'].get(): + 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 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 @@ def fetch_item_lyrics(self, lib, item, write, force): item.lyrics = lyrics if write: item.try_write() - print(lyrics) item.store() def get_lyrics(self, artist, title): diff --git a/beetsplug/thumbnails.py b/beetsplug/thumbnails.py index da4a4027b8..0e7fbc6e02 100644 --- a/beetsplug/thumbnails.py +++ b/beetsplug/thumbnails.py @@ -34,7 +34,7 @@ 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 @@ def _check_local_ok(self): 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) diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 0000000000..d01e442e5c --- /dev/null +++ b/codecov.yml @@ -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 diff --git a/docs/changelog.rst b/docs/changelog.rst index cea89921da..3c7ea6ee28 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -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) diff --git a/docs/plugins/fetchart.rst b/docs/plugins/fetchart.rst index 591dbc4b46..2812c77458 100644 --- a/docs/plugins/fetchart.rst +++ b/docs/plugins/fetchart.rst @@ -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 ------------------- diff --git a/docs/plugins/lyrics.rst b/docs/plugins/lyrics.rst index b922b747f1..8b88c7ef8e 100644 --- a/docs/plugins/lyrics.rst +++ b/docs/plugins/lyrics.rst @@ -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 diff --git a/docs/reference/cli.rst b/docs/reference/cli.rst index fcef6d23f5..faae67ee65 100644 --- a/docs/reference/cli.rst +++ b/docs/reference/cli.rst @@ -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:: diff --git a/docs/reference/pathformat.rst b/docs/reference/pathformat.rst index 8efe54cd27..b5d754bd43 100644 --- a/docs/reference/pathformat.rst +++ b/docs/reference/pathformat.rst @@ -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 diff --git a/test/_common.py b/test/_common.py index c64ff0b714..33928fb665 100644 --- a/test/_common.py +++ b/test/_common.py @@ -167,11 +167,11 @@ def tearDown(self): 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))) diff --git a/test/test_art.py b/test/test_art.py index 2e7db8439e..44b3b9bd43 100644 --- a/test/test_art.py +++ b/test/test_art.py @@ -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 @@ def test_fanarttv_returns_no_result_with_malformed_response(self): 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 @@ def tearDown(self): 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 @@ def _assertImageIsValidArt(self, image_file, should_exist): 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) diff --git a/test/test_autotag.py b/test/test_autotag.py index 30d60f8c4a..71fbdad732 100644 --- a/test/test_autotag.py +++ b/test/test_autotag.py @@ -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(): diff --git a/test/test_convert.py b/test/test_convert.py index 20ed6c743e..679b70a4f8 100644 --- a/test/test_convert.py +++ b/test/test_convert.py @@ -40,7 +40,7 @@ def tagged_copy_cmd(self, tag): 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 @@ def assertFileTag(self, path, tag): 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`. """ diff --git a/test/test_datequery.py b/test/test_datequery.py index 6c9818b776..b978d73047 100644 --- a/test/test_datequery.py +++ b/test/test_datequery.py @@ -62,14 +62,14 @@ def test_unbounded_endpoints(self): 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) diff --git a/test/test_edit.py b/test/test_edit.py index ab0ae046ad..94bb2d9376 100644 --- a/test/test_edit.py +++ b/test/test_edit.py @@ -71,7 +71,7 @@ def replace_contents(self, filename, log): 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 @@ def tearDown(self): 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) diff --git a/test/test_importadded.py b/test/test_importadded.py index 9fa30f5233..6e003ed0f8 100644 --- a/test/test_importadded.py +++ b/test/test_importadded.py @@ -67,7 +67,7 @@ def tearDown(self): 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 @@ def findMediaFile(self, item): 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 @@ def test_import_album_with_preserved_mtimes(self): 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 @@ def test_import_singletons_with_added_dates(self): 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 @@ def test_import_singletons_with_preserved_mtimes(self): 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), diff --git a/test/test_importer.py b/test/test_importer.py index d1ef6ef18e..ff2020ef81 100644 --- a/test/test_importer.py +++ b/test/test_importer.py @@ -125,14 +125,14 @@ def _make_album_match(self, artist, album, tracks, distance=0, missing=0): 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, diff --git a/test/test_lastgenre.py b/test/test_lastgenre.py index 82396f7c71..b397d2c215 100644 --- a/test/test_lastgenre.py +++ b/test/test_lastgenre.py @@ -167,16 +167,16 @@ def get_top_tags(self): 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 @@ def mock_fetch_artist_genre(self, obj): 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 @@ def mock_fetch_artist_genre(self, obj): 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')) diff --git a/test/test_library.py b/test/test_library.py index d57566f5f4..4d6a51b937 100644 --- a/test/test_library.py +++ b/test/test_library.py @@ -617,6 +617,46 @@ def test_nonexistent_function(self): 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): diff --git a/test/test_lyrics.py b/test/test_lyrics.py index 2658232ae2..13bffacdbd 100644 --- a/test/test_lyrics.py +++ b/test/test_lyrics.py @@ -365,14 +365,14 @@ def test_is_page_candidate_fuzzy_match(self): 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): diff --git a/test/test_mediafile.py b/test/test_mediafile.py index 71fd796bde..ad83573daa 100644 --- a/test/test_mediafile.py +++ b/test/test_mediafile.py @@ -168,7 +168,7 @@ def test_guess_cover(self): 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 @@ def assertExtendedImageAttributes(self, image, **kwargs): 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 @@ def test_delete_year(self): 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: diff --git a/test/test_mpdstats.py b/test/test_mpdstats.py index 0fa800dfd0..4ad9d00c4e 100644 --- a/test/test_mpdstats.py +++ b/test/test_mpdstats.py @@ -43,14 +43,14 @@ def test_update_rating(self): 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 @@ def test_get_item(self): {'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() diff --git a/test/test_permissions.py b/test/test_permissions.py index 6dcf4d01c6..a1b7ddd19a 100644 --- a/test/test_permissions.py +++ b/test/test_permissions.py @@ -38,7 +38,7 @@ def test_permissions_on_item_imported(self): 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 @@ def get_stat(v): 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]))) diff --git a/test/test_query.py b/test/test_query.py index 7dd659313d..5fb717c9c6 100644 --- a/test/test_query.py +++ b/test/test_query.py @@ -35,11 +35,11 @@ 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 diff --git a/test/test_smartplaylist.py b/test/test_smartplaylist.py index c30ba24d01..25f6727f01 100644 --- a/test/test_smartplaylist.py +++ b/test/test_smartplaylist.py @@ -88,15 +88,15 @@ def test_build_queries_with_sorts(self): 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() diff --git a/test/test_thumbnails.py b/test/test_thumbnails.py index 5dd9551b7f..30c5bb2343 100644 --- a/test/test_thumbnails.py +++ b/test/test_thumbnails.py @@ -66,8 +66,8 @@ def test_add_tags(self, mock_stat, _): @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): diff --git a/test/test_ui.py b/test/test_ui.py index fdc16aff90..4de5e9ed59 100644 --- a/test/test_ui.py +++ b/test/test_ui.py @@ -233,18 +233,18 @@ def test_reset_mtime_with_no_write(self): 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 diff --git a/tox.ini b/tox.ini index cd29693a1f..6eba8395d7 100644 --- a/tox.ini +++ b/tox.ini @@ -48,4 +48,5 @@ commands = deps = flake8 flake8-future-import + pep8-naming commands = flake8 beets beetsplug beet test setup.py docs