diff --git a/beets/autotag/__init__.py b/beets/autotag/__init__.py index f8233be61..040a5144a 100644 --- a/beets/autotag/__init__.py +++ b/beets/autotag/__init__.py @@ -25,6 +25,7 @@ from beets import config from .hooks import AlbumInfo, TrackInfo, AlbumMatch, TrackMatch # noqa from .match import tag_item, tag_album # noqa from .match import Recommendation # noqa +import six # Global logger. log = logging.getLogger('beets') @@ -52,7 +53,7 @@ def apply_metadata(album_info, mapping): """Set the items' metadata to match an AlbumInfo object using a mapping from Items to TrackInfo objects. """ - for item, track_info in mapping.iteritems(): + for item, track_info in six.iteritems(mapping): # Album, artist, track count. if track_info.artist: item.artist = track_info.artist diff --git a/beets/autotag/hooks.py b/beets/autotag/hooks.py index 0b8a6f6bf..e0f543870 100644 --- a/beets/autotag/hooks.py +++ b/beets/autotag/hooks.py @@ -27,6 +27,7 @@ from beets.util import as_string from beets.autotag import mb from jellyfish import levenshtein_distance from unidecode import unidecode +import six log = logging.getLogger('beets') @@ -205,8 +206,8 @@ def _string_dist_basic(str1, str2): transliteration/lowering to ASCII characters. Normalized by string length. """ - assert isinstance(str1, unicode) - assert isinstance(str2, unicode) + assert isinstance(str1, six.text_type) + assert isinstance(str2, six.text_type) str1 = as_string(unidecode(str1)) str2 = as_string(unidecode(str2)) str1 = re.sub(r'[^a-z0-9]', '', str1.lower()) @@ -291,6 +292,7 @@ class LazyClassProperty(object): @total_ordering +@six.python_2_unicode_compatible class Distance(object): """Keeps track of multiple distance penalties. Provides a single weighted distance for all penalties as well as a weighted distance @@ -326,7 +328,7 @@ class Distance(object): """Return the maximum distance penalty (normalization factor). """ dist_max = 0.0 - for key, penalty in self._penalties.iteritems(): + for key, penalty in six.iteritems(self._penalties): dist_max += len(penalty) * self._weights[key] return dist_max @@ -335,7 +337,7 @@ class Distance(object): """Return the raw (denormalized) distance. """ dist_raw = 0.0 - for key, penalty in self._penalties.iteritems(): + for key, penalty in six.iteritems(self._penalties): dist_raw += sum(penalty) * self._weights[key] return dist_raw @@ -377,7 +379,9 @@ class Distance(object): def __rsub__(self, other): return other - self.distance - def __unicode__(self): + # Behave like a string + + def __str__(self): return "{0:.2f}".format(self.distance) # Behave like a dict. @@ -407,7 +411,7 @@ class Distance(object): raise ValueError( u'`dist` must be a Distance object, not {0}'.format(type(dist)) ) - for key, penalties in dist._penalties.iteritems(): + for key, penalties in six.iteritems(dist._penalties): self._penalties.setdefault(key, []).extend(penalties) # Adding components. diff --git a/beets/autotag/match.py b/beets/autotag/match.py index ba657cedb..136c7f527 100644 --- a/beets/autotag/match.py +++ b/beets/autotag/match.py @@ -30,6 +30,7 @@ from beets.util import plurality from beets.autotag import hooks from beets.util.enumeration import OrderedEnum from functools import reduce +import six # Artist signals that indicate "various artists". These are used at the # album level to determine whether a given release is likely a VA @@ -238,7 +239,7 @@ def distance(items, album_info, mapping): # Tracks. dist.tracks = {} - for item, track in mapping.iteritems(): + for item, track in six.iteritems(mapping): dist.tracks[track] = track_distance(item, track, album_info.va) dist.add('tracks', dist.tracks[track].distance) @@ -312,10 +313,10 @@ def _recommendation(results): keys = set(min_dist.keys()) if isinstance(results[0], hooks.AlbumMatch): for track_dist in min_dist.tracks.values(): - keys.update(track_dist.keys()) + keys.update(list(track_dist.keys())) max_rec_view = config['match']['max_rec'] for key in keys: - if key in max_rec_view.keys(): + if key in list(max_rec_view.keys()): max_rec = max_rec_view[key].as_choice({ 'strong': Recommendation.strong, 'medium': Recommendation.medium, @@ -443,7 +444,7 @@ def tag_album(items, search_artist=None, search_album=None, _add_candidate(items, candidates, info) # Sort and get the recommendation. - candidates = sorted(candidates.itervalues()) + candidates = sorted(six.itervalues(candidates)) rec = _recommendation(candidates) return cur_artist, cur_album, candidates, rec @@ -471,16 +472,16 @@ def tag_item(item, search_artist=None, search_title=None, candidates[track_info.track_id] = \ hooks.TrackMatch(dist, track_info) # If this is a good match, then don't keep searching. - rec = _recommendation(sorted(candidates.itervalues())) + rec = _recommendation(sorted(six.itervalues(candidates))) if rec == Recommendation.strong and \ not config['import']['timid']: log.debug(u'Track ID match.') - return sorted(candidates.itervalues()), rec + return sorted(six.itervalues(candidates)), rec # If we're searching by ID, don't proceed. if search_ids: if candidates: - return sorted(candidates.itervalues()), rec + return sorted(six.itervalues(candidates)), rec else: return [], Recommendation.none @@ -496,6 +497,6 @@ def tag_item(item, search_artist=None, search_title=None, # Sort by distance and return with recommendation. log.debug(u'Found {0} candidates.', len(candidates)) - candidates = sorted(candidates.itervalues()) + candidates = sorted(six.itervalues(candidates)) rec = _recommendation(candidates) return candidates, rec diff --git a/beets/autotag/mb.py b/beets/autotag/mb.py index 2e8109fb9..fdbb8e9e7 100644 --- a/beets/autotag/mb.py +++ b/beets/autotag/mb.py @@ -20,13 +20,14 @@ from __future__ import division, absolute_import, print_function import musicbrainzngs import re import traceback -from urlparse import urljoin +from six.moves.urllib.parse import urljoin from beets import logging import beets.autotag.hooks import beets from beets import util from beets import config +import six VARIOUS_ARTISTS_ID = '89ad4ac3-39f7-470e-963a-56509c546377' BASE_URL = 'http://musicbrainz.org/' @@ -69,7 +70,8 @@ def configure(): """Set up the python-musicbrainz-ngs module according to settings from the beets configuration. This should be called at startup. """ - musicbrainzngs.set_hostname(config['musicbrainz']['host'].get(unicode)) + hostname = config['musicbrainz']['host'].get(six.text_type) + musicbrainzngs.set_hostname(hostname) musicbrainzngs.set_rate_limit( config['musicbrainz']['ratelimit_interval'].as_number(), config['musicbrainz']['ratelimit'].get(int), @@ -108,7 +110,7 @@ def _flatten_artist_credit(credit): artist_sort_parts = [] artist_credit_parts = [] for el in credit: - if isinstance(el, basestring): + if isinstance(el, six.string_types): # Join phrase. artist_parts.append(el) artist_credit_parts.append(el) @@ -260,7 +262,7 @@ def album_info(release): ) info.va = info.artist_id == VARIOUS_ARTISTS_ID if info.va: - info.artist = config['va_name'].get(unicode) + info.artist = config['va_name'].get(six.text_type) info.asin = release.get('asin') info.releasegroup_id = release['release-group']['id'] info.country = release.get('country') @@ -329,10 +331,10 @@ def match_album(artist, album, tracks=None): # Various Artists search. criteria['arid'] = VARIOUS_ARTISTS_ID if tracks is not None: - criteria['tracks'] = unicode(tracks) + criteria['tracks'] = six.text_type(tracks) # Abort if we have no search terms. - if not any(criteria.itervalues()): + if not any(six.itervalues(criteria)): return try: @@ -358,7 +360,7 @@ def match_track(artist, title): 'recording': title.lower().strip(), } - if not any(criteria.itervalues()): + if not any(six.itervalues(criteria)): return try: diff --git a/beets/dbcore/db.py b/beets/dbcore/db.py index a07a19997..24bf6bfe8 100644 --- a/beets/dbcore/db.py +++ b/beets/dbcore/db.py @@ -29,6 +29,7 @@ import beets from beets.util.functemplate import Template from beets.dbcore import types from .query import MatchQuery, NullSort, TrueQuery +import six class FormattedMapping(collections.Mapping): @@ -69,7 +70,7 @@ class FormattedMapping(collections.Mapping): value = value.decode('utf8', 'ignore') if self.for_path: - sep_repl = beets.config['path_sep_replace'].get(unicode) + sep_repl = beets.config['path_sep_replace'].get(six.text_type) for sep in (os.path.sep, os.path.altsep): if sep: value = value.replace(sep, sep_repl) @@ -176,9 +177,9 @@ class Model(object): ordinary construction are bypassed. """ obj = cls(db) - for key, value in fixed_values.iteritems(): + for key, value in six.iteritems(fixed_values): obj._values_fixed[key] = cls._type(key).from_sql(value) - for key, value in flex_values.iteritems(): + for key, value in six.iteritems(flex_values): obj._values_flex[key] = cls._type(key).from_sql(value) return obj @@ -452,7 +453,7 @@ class Model(object): separators will be added to the template. """ # Perform substitution. - if isinstance(template, basestring): + if isinstance(template, six.string_types): template = Template(template) return template.substitute(self.formatted(for_path), self._template_funcs()) @@ -463,7 +464,7 @@ class Model(object): def _parse(cls, key, string): """Parse a string as a value for the given key. """ - if not isinstance(string, basestring): + if not isinstance(string, six.string_types): raise TypeError(u"_parse() argument must be a string") return cls._type(key).parse(string) diff --git a/beets/dbcore/query.py b/beets/dbcore/query.py index 8e2075f37..f6dadd686 100644 --- a/beets/dbcore/query.py +++ b/beets/dbcore/query.py @@ -23,6 +23,10 @@ from beets import util from datetime import datetime, timedelta import unicodedata from functools import reduce +import six + +if not six.PY2: + buffer = memoryview # sqlite won't accept memoryview in python 2 class ParsingError(ValueError): @@ -229,7 +233,7 @@ class BooleanQuery(MatchQuery): """ def __init__(self, field, pattern, fast=True): super(BooleanQuery, self).__init__(field, pattern, fast) - if isinstance(pattern, basestring): + if isinstance(pattern, six.string_types): self.pattern = util.str2bool(pattern) self.pattern = int(self.pattern) @@ -243,11 +247,11 @@ class BytesQuery(MatchQuery): def __init__(self, field, pattern): super(BytesQuery, self).__init__(field, pattern) - # Use a buffer representation of the pattern for SQLite + # Use a buffer/memoryview representation of the pattern for SQLite # matching. This instructs SQLite to treat the blob as binary # rather than encoded Unicode. - if isinstance(self.pattern, (unicode, bytes)): - if isinstance(self.pattern, unicode): + if isinstance(self.pattern, (six.text_type, bytes)): + if isinstance(self.pattern, six.text_type): self.pattern = self.pattern.encode('utf8') self.buf_pattern = buffer(self.pattern) elif isinstance(self.pattern, buffer): @@ -302,7 +306,7 @@ class NumericQuery(FieldQuery): if self.field not in item: return False value = item[self.field] - if isinstance(value, basestring): + if isinstance(value, six.string_types): value = self._convert(value) if self.point is not None: @@ -793,7 +797,7 @@ class FieldSort(Sort): def key(item): field_val = item.get(self.field, '') - if self.case_insensitive and isinstance(field_val, unicode): + if self.case_insensitive and isinstance(field_val, six.text_type): field_val = field_val.lower() return field_val diff --git a/beets/dbcore/types.py b/beets/dbcore/types.py index 2726969dd..ef583672e 100644 --- a/beets/dbcore/types.py +++ b/beets/dbcore/types.py @@ -19,6 +19,10 @@ from __future__ import division, absolute_import, print_function from . import query from beets.util import str2bool +import six + +if not six.PY2: + buffer = memoryview # sqlite won't accept memoryview in python 2 # Abstract base. @@ -37,7 +41,7 @@ class Type(object): """The `Query` subclass to be used when querying the field. """ - model_type = unicode + model_type = six.text_type """The Python type that is used to represent the value in the model. The model is guaranteed to return a value of this type if the field @@ -63,7 +67,7 @@ class Type(object): if isinstance(value, bytes): value = value.decode('utf8', 'ignore') - return unicode(value) + return six.text_type(value) def parse(self, string): """Parse a (possibly human-written) string and return the @@ -97,12 +101,12 @@ class Type(object): https://docs.python.org/2/library/sqlite3.html#sqlite-and-python-types Flexible fields have the type affinity `TEXT`. This means the - `sql_value` is either a `buffer` or a `unicode` object` and the - method must handle these in addition. + `sql_value` is either a `buffer`/`memoryview` or a `unicode` object` + and the method must handle these in addition. """ if isinstance(sql_value, buffer): sql_value = bytes(sql_value).decode('utf8', 'ignore') - if isinstance(sql_value, unicode): + if isinstance(sql_value, six.text_type): return self.parse(sql_value) else: return self.normalize(sql_value) @@ -194,7 +198,7 @@ class Boolean(Type): model_type = bool def format(self, value): - return unicode(bool(value)) + return six.text_type(bool(value)) def parse(self, string): return str2bool(string) diff --git a/beets/importer.py b/beets/importer.py index 072c2ad5b..598dd36d7 100644 --- a/beets/importer.py +++ b/beets/importer.py @@ -14,6 +14,7 @@ # included in all copies or substantial portions of the Software. from __future__ import division, absolute_import, print_function +import six """Provides the basic, interface-agnostic workflow for importing and autotagging music files. @@ -640,7 +641,7 @@ class ImportTask(BaseImportTask): changes['comp'] = False else: # VA. - changes['albumartist'] = config['va_name'].get(unicode) + changes['albumartist'] = config['va_name'].get(six.text_type) changes['comp'] = True elif self.choice_flag in (action.APPLY, action.RETAG): diff --git a/beets/library.py b/beets/library.py index 3450a35a8..2c5ff47b5 100644 --- a/beets/library.py +++ b/beets/library.py @@ -22,6 +22,7 @@ import sys import unicodedata import time import re +import six from unidecode import unidecode from beets import logging @@ -34,6 +35,8 @@ from beets import dbcore from beets.dbcore import types import beets +if not six.PY2: + buffer = memoryview # sqlite won't accept memoryview in python 2 log = logging.getLogger('beets') @@ -123,14 +126,15 @@ class DateType(types.Float): query = dbcore.query.DateQuery def format(self, value): - return time.strftime(beets.config['time_format'].get(unicode), + return time.strftime(beets.config['time_format'].get(six.text_type), time.localtime(value or 0)) def parse(self, string): try: # Try a formatted date string. return time.mktime( - time.strptime(string, beets.config['time_format'].get(unicode)) + time.strptime(string, + beets.config['time_format'].get(six.text_type)) ) except ValueError: # Fall back to a plain timestamp number. @@ -152,13 +156,13 @@ class PathType(types.Type): return normpath(bytestring_path(string)) def normalize(self, value): - if isinstance(value, unicode): + if isinstance(value, six.text_type): # Paths stored internally as encoded bytes. return bytestring_path(value) elif isinstance(value, buffer): - # SQLite must store bytestings as buffers to avoid decoding. - # We unwrap buffers to bytes. + # SQLite must store bytestings as buffers/memoryview + # to avoid decoding. We unwrap buffers to bytes. return bytes(value) else: @@ -260,7 +264,7 @@ PF_KEY_DEFAULT = 'default' # Exceptions. - +@six.python_2_unicode_compatible class FileOperationError(Exception): """Indicates an error when interacting with a file on disk. Possibilities include an unsupported media type, a permissions @@ -274,35 +278,39 @@ class FileOperationError(Exception): self.path = path self.reason = reason - def __unicode__(self): + def text(self): """Get a string representing the error. Describes both the underlying reason and the file path in question. """ return u'{0}: {1}'.format( util.displayable_path(self.path), - unicode(self.reason) + six.text_type(self.reason) ) - def __str__(self): - return unicode(self).encode('utf8') + # define __str__ as text to avoid infinite loop on super() calls + # with @six.python_2_unicode_compatible + __str__ = text +@six.python_2_unicode_compatible class ReadError(FileOperationError): """An error while reading a file (i.e. in `Item.read`). """ - def __unicode__(self): - return u'error reading ' + super(ReadError, self).__unicode__() + def __str__(self): + return u'error reading ' + super(ReadError, self).text() +@six.python_2_unicode_compatible class WriteError(FileOperationError): """An error while writing a file (i.e. in `Item.write`). """ - def __unicode__(self): - return u'error writing ' + super(WriteError, self).__unicode__() + def __str__(self): + return u'error writing ' + super(WriteError, self).text() # Item and Album model classes. +@six.python_2_unicode_compatible class LibModel(dbcore.Model): """Shared concrete functionality for Items and Albums. """ @@ -330,7 +338,7 @@ class LibModel(dbcore.Model): def __format__(self, spec): if not spec: - spec = beets.config[self._format_config_key].get(unicode) + spec = beets.config[self._format_config_key].get(six.text_type) result = self.evaluate_template(spec) if isinstance(spec, bytes): # if spec is a byte string then we must return a one as well @@ -339,9 +347,6 @@ class LibModel(dbcore.Model): return result def __str__(self): - return format(self).encode('utf8') - - def __unicode__(self): return format(self) @@ -516,7 +521,7 @@ class Item(LibModel): """ # Encode unicode paths and read buffers. if key == 'path': - if isinstance(value, unicode): + if isinstance(value, six.text_type): value = bytestring_path(value) elif isinstance(value, buffer): value = bytes(value) @@ -565,7 +570,7 @@ class Item(LibModel): for key in self._media_fields: value = getattr(mediafile, key) - if isinstance(value, (int, long)): + if isinstance(value, six.integer_types): if value.bit_length() > 63: value = 0 self[key] = value @@ -1060,7 +1065,8 @@ class Album(LibModel): image = bytestring_path(image) item_dir = item_dir or self.item_dir() - filename_tmpl = Template(beets.config['art_filename'].get(unicode)) + filename_tmpl = Template( + beets.config['art_filename'].get(six.text_type)) subpath = self.evaluate_template(filename_tmpl, True) if beets.config['asciify_paths']: subpath = unidecode(subpath) @@ -1178,7 +1184,8 @@ def parse_query_string(s, model_cls): The string is split into components using shell-like syntax. """ - assert isinstance(s, unicode), u"Query is not unicode: {0!r}".format(s) + message = u"Query is not unicode: {0!r}".format(s) + assert isinstance(s, six.text_type), message try: parts = util.shlex_split(s) except ValueError as exc: @@ -1254,7 +1261,7 @@ class Library(dbcore.Database): # Parse the query, if necessary. try: parsed_sort = None - if isinstance(query, basestring): + if isinstance(query, six.string_types): query, parsed_sort = parse_query_string(query, model_cls) elif isinstance(query, (list, tuple)): query, parsed_sort = parse_query_parts(query, model_cls) @@ -1404,7 +1411,7 @@ class DefaultTemplateFunctions(object): def tmpl_time(s, fmt): """Format a time value using `strftime`. """ - cur_fmt = beets.config['time_format'].get(unicode) + cur_fmt = beets.config['time_format'].get(six.text_type) return time.strftime(fmt, time.strptime(s, cur_fmt)) def tmpl_aunique(self, keys=None, disam=None): diff --git a/beets/logging.py b/beets/logging.py index a94da1c62..f7b46bd60 100644 --- a/beets/logging.py +++ b/beets/logging.py @@ -27,6 +27,7 @@ from copy import copy from logging import * # noqa import subprocess import threading +import six def logsafe(val): @@ -42,7 +43,7 @@ def logsafe(val): example. """ # Already Unicode. - if isinstance(val, unicode): + if isinstance(val, six.text_type): return val # Bytestring: needs decoding. @@ -56,7 +57,7 @@ def logsafe(val): # A "problem" object: needs a workaround. elif isinstance(val, subprocess.CalledProcessError): try: - return unicode(val) + return six.text_type(val) except UnicodeDecodeError: # An object with a broken __unicode__ formatter. Use __str__ # instead. diff --git a/beets/mediafile.py b/beets/mediafile.py index 556b41bb8..dbe035d5a 100644 --- a/beets/mediafile.py +++ b/beets/mediafile.py @@ -59,6 +59,7 @@ import enum from beets import logging from beets.util import displayable_path, syspath, as_string +import six __all__ = ['UnreadableFileError', 'FileTypeError', 'MediaFile'] @@ -130,8 +131,8 @@ def _safe_cast(out_type, val): return int(val) else: # Process any other type as a string. - if not isinstance(val, basestring): - val = unicode(val) + if not isinstance(val, six.string_types): + val = six.text_type(val) # Get a number from the front of the string. val = re.match(r'[0-9]*', val.strip()).group(0) if not val: @@ -146,13 +147,13 @@ def _safe_cast(out_type, val): except ValueError: return False - elif out_type == unicode: + elif out_type == six.text_type: if isinstance(val, bytes): return val.decode('utf8', 'ignore') - elif isinstance(val, unicode): + elif isinstance(val, six.text_type): return val else: - return unicode(val) + return six.text_type(val) elif out_type == float: if isinstance(val, int) or isinstance(val, float): @@ -161,7 +162,7 @@ def _safe_cast(out_type, val): if isinstance(val, bytes): val = val.decode('utf8', 'ignore') else: - val = unicode(val) + val = six.text_type(val) match = re.match(r'[\+-]?([0-9]+\.?[0-9]*|[0-9]*\.[0-9]+)', val.strip()) if match: @@ -220,7 +221,7 @@ def _sc_decode(soundcheck): """ # We decode binary data. If one of the formats gives us a text # string, interpret it as UTF-8. - if isinstance(soundcheck, unicode): + if isinstance(soundcheck, six.text_type): soundcheck = soundcheck.encode('utf8') # SoundCheck tags consist of 10 numbers, each represented by 8 @@ -407,7 +408,8 @@ class StorageStyle(object): """List of mutagen classes the StorageStyle can handle. """ - def __init__(self, key, as_type=unicode, suffix=None, float_places=2): + def __init__(self, key, as_type=six.text_type, suffix=None, + float_places=2): """Create a basic storage strategy. Parameters: - `key`: The key on the Mutagen file object used to access the @@ -426,8 +428,8 @@ class StorageStyle(object): self.float_places = float_places # Convert suffix to correct string type. - if self.suffix and self.as_type is unicode \ - and not isinstance(self.suffix, unicode): + if self.suffix and self.as_type is six.text_type \ + and not isinstance(self.suffix, six.text_type): self.suffix = self.suffix.decode('utf8') # Getter. @@ -450,7 +452,7 @@ class StorageStyle(object): """Given a raw value stored on a Mutagen object, decode and return the represented value. """ - if self.suffix and isinstance(mutagen_value, unicode) \ + if self.suffix and isinstance(mutagen_value, six.text_type) \ and mutagen_value.endswith(self.suffix): return mutagen_value[:-len(self.suffix)] else: @@ -472,17 +474,17 @@ class StorageStyle(object): """Convert the external Python value to a type that is suitable for storing in a Mutagen file object. """ - if isinstance(value, float) and self.as_type is unicode: + if isinstance(value, float) and self.as_type is six.text_type: value = u'{0:.{1}f}'.format(value, self.float_places) value = self.as_type(value) - elif self.as_type is unicode: + elif self.as_type is six.text_type: if isinstance(value, bool): # Store bools as 1/0 instead of True/False. - value = unicode(int(bool(value))) + value = six.text_type(int(bool(value))) elif isinstance(value, bytes): value = value.decode('utf8', 'ignore') else: - value = unicode(value) + value = six.text_type(value) else: value = self.as_type(value) @@ -592,7 +594,7 @@ class MP4StorageStyle(StorageStyle): def serialize(self, value): value = super(MP4StorageStyle, self).serialize(value) - if self.key.startswith('----:') and isinstance(value, unicode): + if self.key.startswith('----:') and isinstance(value, six.text_type): value = value.encode('utf8') return value @@ -807,7 +809,7 @@ class MP3SlashPackStorageStyle(MP3StorageStyle): def _fetch_unpacked(self, mutagen_file): data = self.fetch(mutagen_file) if data: - items = unicode(data).split('/') + items = six.text_type(data).split('/') else: items = [] packing_length = 2 @@ -823,7 +825,7 @@ class MP3SlashPackStorageStyle(MP3StorageStyle): items[0] = '' if items[1] is None: items.pop() # Do not store last value - self.store(mutagen_file, '/'.join(map(unicode, items))) + self.store(mutagen_file, '/'.join(map(six.text_type, items))) def delete(self, mutagen_file): if self.pack_pos == 0: @@ -1074,7 +1076,7 @@ class MediaField(object): getting this property. """ - self.out_type = kwargs.get('out_type', unicode) + self.out_type = kwargs.get('out_type', six.text_type) self._styles = styles def styles(self, mutagen_file): @@ -1113,7 +1115,7 @@ class MediaField(object): return 0.0 elif self.out_type == bool: return False - elif self.out_type == unicode: + elif self.out_type == six.text_type: return u'' @@ -1194,9 +1196,9 @@ class DateField(MediaField): """ # Get the underlying data and split on hyphens and slashes. datestring = super(DateField, self).__get__(mediafile, None) - if isinstance(datestring, basestring): - datestring = re.sub(r'[Tt ].*$', '', unicode(datestring)) - items = re.split('[-/]', unicode(datestring)) + if isinstance(datestring, six.string_types): + datestring = re.sub(r'[Tt ].*$', '', six.text_type(datestring)) + items = re.split('[-/]', six.text_type(datestring)) else: items = [] @@ -1233,7 +1235,7 @@ class DateField(MediaField): date.append(u'{0:02d}'.format(int(month))) if month and day: date.append(u'{0:02d}'.format(int(day))) - date = map(unicode, date) + date = map(six.text_type, date) super(DateField, self).__set__(mediafile, u'-'.join(date)) if hasattr(self, '_year_field'): @@ -1360,7 +1362,7 @@ class MediaFile(object): try: self.mgfile = mutagen.File(path) except unreadable_exc as exc: - log.debug(u'header parsing failed: {0}', unicode(exc)) + log.debug(u'header parsing failed: {0}', six.text_type(exc)) raise UnreadableFileError(path) except IOError as exc: if type(exc) == IOError: diff --git a/beets/plugins.py b/beets/plugins.py index 239f64fbb..8fe4c6d0c 100755 --- a/beets/plugins.py +++ b/beets/plugins.py @@ -27,6 +27,7 @@ from functools import wraps import beets from beets import logging from beets import mediafile +import six PLUGIN_NAMESPACE = 'beetsplug' @@ -54,10 +55,10 @@ class PluginLogFilter(logging.Filter): def filter(self, record): if hasattr(record.msg, 'msg') and isinstance(record.msg.msg, - basestring): + six.string_types): # A _LogMessage from our hacked-up Logging replacement. record.msg.msg = self.prefix + record.msg.msg - elif isinstance(record.msg, basestring): + elif isinstance(record.msg, six.string_types): record.msg = self.prefix + record.msg return True diff --git a/beets/ui/__init__.py b/beets/ui/__init__.py index af7fe9aff..ce3c46f31 100644 --- a/beets/ui/__init__.py +++ b/beets/ui/__init__.py @@ -31,6 +31,7 @@ import re import struct import traceback import os.path +from six.moves import input from beets import logging from beets import library @@ -41,6 +42,7 @@ from beets import config from beets.util import confit, as_string from beets.autotag import mb from beets.dbcore import query as db_query +import six # On Windows platforms, use colorama to support "ANSI" terminal colors. if sys.platform == 'win32': @@ -139,7 +141,7 @@ def print_(*strings, **kwargs): """ end = kwargs.get('end') - if not strings or isinstance(strings[0], unicode): + if not strings or isinstance(strings[0], six.text_type): txt = u' '.join(strings) txt += u'\n' if end is None else end else: @@ -147,7 +149,7 @@ def print_(*strings, **kwargs): txt += b'\n' if end is None else end # Always send bytes to the stdout stream. - if isinstance(txt, unicode): + if isinstance(txt, six.text_type): txt = txt.encode(_out_encoding(), 'replace') sys.stdout.write(txt) @@ -193,7 +195,7 @@ def should_move(move_opt=None): # Input prompts. def input_(prompt=None): - """Like `raw_input`, but decodes the result to a Unicode string. + """Like `input`, but decodes the result to a Unicode string. Raises a UserError if stdin is not available. The prompt is sent to stdout rather than stderr. A printed between the prompt and the input cursor. @@ -205,7 +207,7 @@ def input_(prompt=None): print_(prompt, end=' ') try: - resp = raw_input() + resp = input() except EOFError: raise UserError(u'stdin stream ended while input required') @@ -261,7 +263,7 @@ def input_options(options, require=False, prompt=None, fallback_prompt=None, # Mark the option's shortcut letter for display. if not require and ( (default is None and not numrange and first) or - (isinstance(default, basestring) and + (isinstance(default, six.string_types) and found_letter.lower() == default.lower())): # The first option is the default; mark it. show_letter = '[%s]' % found_letter.upper() @@ -297,11 +299,11 @@ def input_options(options, require=False, prompt=None, fallback_prompt=None, prompt_part_lengths = [] if numrange: if isinstance(default, int): - default_name = unicode(default) + default_name = six.text_type(default) default_name = colorize('action_default', default_name) tmpl = '# selection (default %s)' prompt_parts.append(tmpl % default_name) - prompt_part_lengths.append(len(tmpl % unicode(default))) + prompt_part_lengths.append(len(tmpl % six.text_type(default))) else: prompt_parts.append('# selection') prompt_part_lengths.append(len(prompt_parts[-1])) @@ -521,7 +523,8 @@ def colorize(color_name, text): if config['ui']['color']: global COLORS if not COLORS: - COLORS = dict((name, config['ui']['colors'][name].get(unicode)) + COLORS = dict((name, + config['ui']['colors'][name].get(six.text_type)) for name in COLOR_NAMES) # In case a 3rd party plugin is still passing the actual color ('red') # instead of the abstract color name ('text_error') @@ -541,10 +544,11 @@ def _colordiff(a, b, highlight='text_highlight', highlighted intelligently to show differences; other values are stringified and highlighted in their entirety. """ - if not isinstance(a, basestring) or not isinstance(b, basestring): + if not isinstance(a, six.string_types) \ + or not isinstance(b, six.string_types): # Non-strings: use ordinary equality. - a = unicode(a) - b = unicode(b) + a = six.text_type(a) + b = six.text_type(b) if a == b: return a, b else: @@ -592,7 +596,7 @@ def colordiff(a, b, highlight='text_highlight'): if config['ui']['color']: return _colordiff(a, b, highlight) else: - return unicode(a), unicode(b) + return six.text_type(a), six.text_type(b) def get_path_formats(subview=None): @@ -603,7 +607,7 @@ def get_path_formats(subview=None): subview = subview or config['paths'] for query, view in subview.items(): query = PF_KEY_QUERIES.get(query, query) # Expand common queries. - path_formats.append((query, Template(view.get(unicode)))) + path_formats.append((query, Template(view.get(six.text_type)))) return path_formats @@ -671,7 +675,7 @@ def _field_diff(field, old, new): # For strings, highlight changes. For others, colorize the whole # thing. - if isinstance(oldval, basestring): + if isinstance(oldval, six.string_types): oldstr, newstr = colordiff(oldval, newstr) else: oldstr = colorize('text_error', oldstr) @@ -864,7 +868,7 @@ class CommonOptionsParser(optparse.OptionParser, object): """ kwargs = {} if target: - if isinstance(target, basestring): + if isinstance(target, six.string_types): target = {'item': library.Item, 'album': library.Album}[target] kwargs['target'] = target diff --git a/beets/ui/commands.py b/beets/ui/commands.py index d2270c662..c352fe53d 100644 --- a/beets/ui/commands.py +++ b/beets/ui/commands.py @@ -38,6 +38,7 @@ from beets import library from beets import config from beets import logging from beets.util.confit import _package_path +import six VARIOUS_ARTISTS = u'Various Artists' PromptChoice = namedtuple('ExtraChoice', ['short', 'long', 'callback']) @@ -163,7 +164,7 @@ def disambig_string(info): else: disambig.append(info.media) if info.year: - disambig.append(unicode(info.year)) + disambig.append(six.text_type(info.year)) if info.country: disambig.append(info.country) if info.label: @@ -236,9 +237,9 @@ def show_change(cur_artist, cur_album, match): if mediums > 1: return u'{0}-{1}'.format(medium, medium_index) else: - return unicode(medium_index) + return six.text_type(medium_index) else: - return unicode(index) + return six.text_type(index) # Identify the album in question. if cur_artist != match.info.artist or \ @@ -806,7 +807,7 @@ class TerminalImportSession(importer.ImportSession): if search_id: candidates, rec = autotag.tag_item( task.item, search_ids=search_id.split()) - elif choice in extra_ops.keys(): + elif choice in list(extra_ops.keys()): # Allow extra ops to automatically set the post-choice. post_choice = extra_ops[choice](self, task) if isinstance(post_choice, importer.action): diff --git a/beets/util/__init__.py b/beets/util/__init__.py index 0628f324b..56504ec3f 100644 --- a/beets/util/__init__.py +++ b/beets/util/__init__.py @@ -27,6 +27,7 @@ import subprocess import platform import shlex from beets.util import hidden +import six MAX_FILENAME_LENGTH = 200 @@ -65,14 +66,14 @@ class HumanReadableException(Exception): def _reasonstr(self): """Get the reason as a string.""" - if isinstance(self.reason, unicode): + if isinstance(self.reason, six.text_type): return self.reason elif isinstance(self.reason, bytes): return self.reason.decode('utf8', 'ignore') elif hasattr(self.reason, 'strerror'): # i.e., EnvironmentError return self.reason.strerror else: - return u'"{0}"'.format(unicode(self.reason)) + return u'"{0}"'.format(six.text_type(self.reason)) def get_message(self): """Create the human-readable description of the error, sans @@ -346,11 +347,11 @@ def displayable_path(path, separator=u'; '): """ if isinstance(path, (list, tuple)): return separator.join(displayable_path(p) for p in path) - elif isinstance(path, unicode): + elif isinstance(path, six.text_type): return path elif not isinstance(path, bytes): # A non-string object: just get its unicode representation. - return unicode(path) + return six.text_type(path) try: return path.decode(_fsencoding(), 'ignore') @@ -369,7 +370,7 @@ def syspath(path, prefix=True): if os.path.__name__ != 'ntpath': return path - if not isinstance(path, unicode): + if not isinstance(path, six.text_type): # Beets currently represents Windows paths internally with UTF-8 # arbitrarily. But earlier versions used MBCS because it is # reported as the FS encoding by Windows. Try both. @@ -632,14 +633,18 @@ def as_string(value): """Convert a value to a Unicode object for matching with a query. None becomes the empty string. Bytestrings are silently decoded. """ + buffer_types = memoryview + if six.PY2: + buffer_types = (buffer, memoryview) + if value is None: return u'' - elif isinstance(value, buffer): + elif isinstance(value, buffer_types): return bytes(value).decode('utf8', 'ignore') elif isinstance(value, bytes): return value.decode('utf8', 'ignore') else: - return unicode(value) + return six.text_type(value) def plurality(objs): @@ -765,7 +770,7 @@ def shlex_split(s): # Shlex works fine. return shlex.split(s) - elif isinstance(s, unicode): + elif isinstance(s, six.text_type): # Work around a Python bug. # http://bugs.python.org/issue6988 bs = s.encode('utf8') @@ -801,7 +806,7 @@ def _windows_long_path_name(short_path): """Use Windows' `GetLongPathNameW` via ctypes to get the canonical, long path given a short filename. """ - if not isinstance(short_path, unicode): + if not isinstance(short_path, six.text_type): short_path = short_path.decode(_fsencoding()) import ctypes diff --git a/beets/util/artresizer.py b/beets/util/artresizer.py index 8f7ae7514..820bc4ed1 100644 --- a/beets/util/artresizer.py +++ b/beets/util/artresizer.py @@ -18,14 +18,14 @@ public resizing proxy if neither is available. """ from __future__ import division, absolute_import, print_function -import urllib import subprocess import os import re from tempfile import NamedTemporaryFile - +from six.moves.urllib.parse import urlencode from beets import logging from beets import util +import six # Resizing methods PIL = 1 @@ -41,7 +41,7 @@ def resize_url(url, maxwidth): """Return a proxied image URL that resizes the original image to maxwidth (preserving aspect ratio). """ - return '{0}?{1}'.format(PROXY_URL, urllib.urlencode({ + return '{0}?{1}'.format(PROXY_URL, urlencode({ 'url': url.replace('http://', ''), 'w': bytes(maxwidth), })) @@ -160,10 +160,9 @@ class Shareable(type): return self._instance -class ArtResizer(object): +class ArtResizer(six.with_metaclass(Shareable, object)): """A singleton class that performs image resizes. """ - __metaclass__ = Shareable def __init__(self): """Create a resizer object with an inferred method. diff --git a/beets/util/bluelet.py b/beets/util/bluelet.py index d81c2919a..48dd7bd94 100644 --- a/beets/util/bluelet.py +++ b/beets/util/bluelet.py @@ -9,6 +9,7 @@ Bluelet: easy concurrency without all the messy parallelism. """ from __future__ import division, absolute_import, print_function +import six import socket import select import sys @@ -19,20 +20,6 @@ import time import collections -# A little bit of "six" (Python 2/3 compatibility): cope with PEP 3109 syntax -# changes. - -PY3 = sys.version_info[0] == 3 -if PY3: - def _reraise(typ, exc, tb): - raise exc.with_traceback(tb) -else: - exec(""" -def _reraise(typ, exc, tb): - raise typ, exc, tb -""") - - # Basic events used for thread scheduling. class Event(object): @@ -214,7 +201,7 @@ class ThreadException(Exception): self.exc_info = exc_info def reraise(self): - _reraise(self.exc_info[0], self.exc_info[1], self.exc_info[2]) + six.reraise(self.exc_info[0], self.exc_info[1], self.exc_info[2]) SUSPENDED = Event() # Special sentinel placeholder for suspended threads. diff --git a/beets/util/confit.py b/beets/util/confit.py index e4a3d8f8e..238f806ca 100644 --- a/beets/util/confit.py +++ b/beets/util/confit.py @@ -25,6 +25,7 @@ import yaml import collections import re from collections import OrderedDict +import six UNIX_DIR_VAR = 'XDG_CONFIG_HOME' UNIX_DIR_FALLBACK = '~/.config' @@ -44,7 +45,7 @@ REDACTED_TOMBSTONE = 'REDACTED' # Utilities. PY3 = sys.version_info[0] == 3 -STRING = str if PY3 else unicode # noqa +STRING = str if PY3 else six.text_type # noqa BASESTRING = str if PY3 else basestring # noqa NUMERIC_TYPES = (int, float) if PY3 else (int, float, long) # noqa diff --git a/beets/util/functemplate.py b/beets/util/functemplate.py index 05f0892c2..2b32971e2 100644 --- a/beets/util/functemplate.py +++ b/beets/util/functemplate.py @@ -34,7 +34,7 @@ import ast import dis import types -from .confit import NUMERIC_TYPES +import six SYMBOL_DELIM = u'$' FUNC_DELIM = u'%' @@ -74,11 +74,11 @@ def ex_literal(val): """ if val is None: return ast.Name('None', ast.Load()) - elif isinstance(val, NUMERIC_TYPES): + elif isinstance(val, six.integer_types): return ast.Num(val) elif isinstance(val, bool): return ast.Name(bytes(val), ast.Load()) - elif isinstance(val, basestring): + elif isinstance(val, six.string_types): return ast.Str(val) raise TypeError(u'no literal for {0}'.format(type(val))) @@ -97,7 +97,7 @@ def ex_call(func, args): function may be an expression or the name of a function. Each argument may be an expression or a value to be used as a literal. """ - if isinstance(func, basestring): + if isinstance(func, six.string_types): func = ex_rvalue(func) args = list(args) @@ -190,8 +190,8 @@ class Call(object): except Exception as exc: # Function raised exception! Maybe inlining the name of # the exception will help debug. - return u'<%s>' % unicode(exc) - return unicode(out) + return u'<%s>' % six.text_type(exc) + return six.text_type(out) else: return self.original @@ -242,11 +242,11 @@ class Expression(object): """ out = [] for part in self.parts: - if isinstance(part, basestring): + if isinstance(part, six.string_types): out.append(part) else: out.append(part.evaluate(env)) - return u''.join(map(unicode, out)) + return u''.join(map(six.text_type, out)) def translate(self): """Compile the expression to a list of Python AST expressions, a @@ -256,7 +256,7 @@ class Expression(object): varnames = set() funcnames = set() for part in self.parts: - if isinstance(part, basestring): + if isinstance(part, six.string_types): expressions.append(ex_literal(part)) else: e, v, f = part.translate() @@ -508,7 +508,8 @@ class Template(object): def __init__(self, template): self.expr = _parse(template) self.original = template - self.compiled = self.translate() + if six.PY2: + self.compiled = self.translate() def __eq__(self, other): return self.original == other.original @@ -524,9 +525,12 @@ class Template(object): def substitute(self, values={}, functions={}): """Evaluate the template given the values and functions. """ - try: - res = self.compiled(values, functions) - except: # Handle any exceptions thrown by compiled version. + if six.PY2: + try: + res = self.compiled(values, functions) + except: # Handle any exceptions thrown by compiled version. + res = self.interpret(values, functions) + else: res = self.interpret(values, functions) return res @@ -563,7 +567,7 @@ if __name__ == '__main__': import timeit _tmpl = Template(u'foo $bar %baz{foozle $bar barzle} $bar') _vars = {'bar': 'qux'} - _funcs = {'baz': unicode.upper} + _funcs = {'baz': six.text_type.upper} interp_time = timeit.timeit('_tmpl.interpret(_vars, _funcs)', 'from __main__ import _tmpl, _vars, _funcs', number=10000) diff --git a/beets/util/hidden.py b/beets/util/hidden.py index 262d371ea..11e3691d6 100644 --- a/beets/util/hidden.py +++ b/beets/util/hidden.py @@ -20,6 +20,7 @@ import os import stat import ctypes import sys +import six def _is_hidden_osx(path): @@ -74,7 +75,7 @@ def is_hidden(path): work out if a file is hidden. """ # Convert the path to unicode if it is not already. - if not isinstance(path, unicode): + if not isinstance(path, six.text_type): path = path.decode('utf-8') # Run platform specific functions depending on the platform diff --git a/beets/util/pipeline.py b/beets/util/pipeline.py index b5f777336..1a0cd4b5c 100644 --- a/beets/util/pipeline.py +++ b/beets/util/pipeline.py @@ -34,9 +34,10 @@ in place of any single coroutine. from __future__ import division, absolute_import, print_function -import Queue +from six.moves import queue from threading import Thread, Lock import sys +import six BUBBLE = '__PIPELINE_BUBBLE__' POISON = '__PIPELINE_POISON__' @@ -75,13 +76,13 @@ def _invalidate_queue(q, val=None, sync=True): q.mutex.release() -class CountedQueue(Queue.Queue): +class CountedQueue(queue.Queue): """A queue that keeps track of the number of threads that are still feeding into it. The queue is poisoned when all threads are finished with the queue. """ def __init__(self, maxsize=0): - Queue.Queue.__init__(self, maxsize) + queue.Queue.__init__(self, maxsize) self.nthreads = 0 self.poisoned = False @@ -431,7 +432,7 @@ class Pipeline(object): exc_info = thread.exc_info if exc_info: # Make the exception appear as it was raised originally. - raise exc_info[0], exc_info[1], exc_info[2] + six.reraise(exc_info[0], exc_info[1], exc_info[2]) def pull(self): """Yield elements from the end of the pipeline. Runs the stages diff --git a/beetsplug/badfiles.py b/beetsplug/badfiles.py index f9704d484..bb14c7762 100644 --- a/beetsplug/badfiles.py +++ b/beetsplug/badfiles.py @@ -27,6 +27,7 @@ import shlex import os import errno import sys +import six class BadFiles(BeetsPlugin): @@ -97,7 +98,7 @@ class BadFiles(BeetsPlugin): if not checker: continue path = item.path - if not isinstance(path, unicode): + if not isinstance(path, six.text_type): path = item.path.decode(sys.getfilesystemencoding()) status, errors, output = checker(path) if status > 0: diff --git a/beetsplug/beatport.py b/beetsplug/beatport.py index c1425ad9e..5949589c3 100644 --- a/beetsplug/beatport.py +++ b/beetsplug/beatport.py @@ -18,6 +18,7 @@ from __future__ import division, absolute_import, print_function import json import re +import six from datetime import datetime, timedelta from requests_oauthlib import OAuth1Session @@ -42,15 +43,15 @@ class BeatportAPIError(Exception): class BeatportObject(object): def __init__(self, data): self.beatport_id = data['id'] - self.name = unicode(data['name']) + self.name = six.text_type(data['name']) if 'releaseDate' in data: self.release_date = datetime.strptime(data['releaseDate'], '%Y-%m-%d') if 'artists' in data: - self.artists = [(x['id'], unicode(x['name'])) + self.artists = [(x['id'], six.text_type(x['name'])) for x in data['artists']] if 'genres' in data: - self.genres = [unicode(x['name']) + self.genres = [six.text_type(x['name']) for x in data['genres']] @@ -196,8 +197,9 @@ class BeatportClient(object): return response.json()['results'] +@six.python_2_unicode_compatible class BeatportRelease(BeatportObject): - def __unicode__(self): + def __str__(self): if len(self.artists) < 4: artist_str = ", ".join(x[1] for x in self.artists) else: @@ -209,7 +211,7 @@ class BeatportRelease(BeatportObject): ) def __repr__(self): - return unicode(self).encode('utf8') + return six.text_type(self).encode('utf8') def __init__(self, data): BeatportObject.__init__(self, data) @@ -224,21 +226,22 @@ class BeatportRelease(BeatportObject): data['slug'], data['id']) +@six.python_2_unicode_compatible class BeatportTrack(BeatportObject): - def __unicode__(self): + def __str__(self): artist_str = ", ".join(x[1] for x in self.artists) return (u"" .format(artist_str, self.name, self.mix_name)) def __repr__(self): - return unicode(self).encode('utf8') + return six.text_type(self).encode('utf8') def __init__(self, data): BeatportObject.__init__(self, data) if 'title' in data: - self.title = unicode(data['title']) + self.title = six.text_type(data['title']) if 'mixName' in data: - self.mix_name = unicode(data['mixName']) + self.mix_name = six.text_type(data['mixName']) self.length = timedelta(milliseconds=data.get('lengthMs', 0) or 0) if not self.length: try: @@ -266,8 +269,8 @@ class BeatportPlugin(BeetsPlugin): self.register_listener('import_begin', self.setup) def setup(self, session=None): - c_key = self.config['apikey'].get(unicode) - c_secret = self.config['apisecret'].get(unicode) + c_key = self.config['apikey'].get(six.text_type) + c_secret = self.config['apisecret'].get(six.text_type) # Get the OAuth token from a file or log in. try: diff --git a/beetsplug/bpd/__init__.py b/beetsplug/bpd/__init__.py index ea1b0082e..5d06028df 100644 --- a/beetsplug/bpd/__init__.py +++ b/beetsplug/bpd/__init__.py @@ -35,6 +35,7 @@ from beets.util import bluelet from beets.library import Item from beets import dbcore from beets.mediafile import MediaFile +import six PROTOCOL_VERSION = '0.13.0' BUFSIZE = 1024 @@ -305,12 +306,12 @@ class BaseServer(object): playlist, playlistlength, and xfade. """ yield ( - u'volume: ' + unicode(self.volume), - u'repeat: ' + unicode(int(self.repeat)), - u'random: ' + unicode(int(self.random)), - u'playlist: ' + unicode(self.playlist_version), - u'playlistlength: ' + unicode(len(self.playlist)), - u'xfade: ' + unicode(self.crossfade), + u'volume: ' + six.text_type(self.volume), + u'repeat: ' + six.text_type(int(self.repeat)), + u'random: ' + six.text_type(int(self.random)), + u'playlist: ' + six.text_type(self.playlist_version), + u'playlistlength: ' + six.text_type(len(self.playlist)), + u'xfade: ' + six.text_type(self.crossfade), ) if self.current_index == -1: @@ -323,8 +324,8 @@ class BaseServer(object): if self.current_index != -1: # i.e., paused or playing current_id = self._item_id(self.playlist[self.current_index]) - yield u'song: ' + unicode(self.current_index) - yield u'songid: ' + unicode(current_id) + yield u'song: ' + six.text_type(self.current_index) + yield u'songid: ' + six.text_type(current_id) if self.error: yield u'error: ' + self.error @@ -468,8 +469,8 @@ class BaseServer(object): Also a dummy implementation. """ for idx, track in enumerate(self.playlist): - yield u'cpos: ' + unicode(idx) - yield u'Id: ' + unicode(track.id) + yield u'cpos: ' + six.text_type(idx) + yield u'Id: ' + six.text_type(track.id) def cmd_currentsong(self, conn): """Sends information about the currently-playing song. @@ -569,11 +570,11 @@ class Connection(object): added after every string. Returns a Bluelet event that sends the data. """ - if isinstance(lines, basestring): + if isinstance(lines, six.string_types): lines = [lines] out = NEWLINE.join(lines) + NEWLINE log.debug('{}', out[:-1]) # Don't log trailing newline. - if isinstance(out, unicode): + if isinstance(out, six.text_type): out = out.encode('utf8') return self.sock.sendall(out) @@ -771,28 +772,28 @@ class Server(BaseServer): def _item_info(self, item): info_lines = [ u'file: ' + item.destination(fragment=True), - u'Time: ' + unicode(int(item.length)), + u'Time: ' + six.text_type(int(item.length)), u'Title: ' + item.title, u'Artist: ' + item.artist, u'Album: ' + item.album, u'Genre: ' + item.genre, ] - track = unicode(item.track) + track = six.text_type(item.track) if item.tracktotal: - track += u'/' + unicode(item.tracktotal) + track += u'/' + six.text_type(item.tracktotal) info_lines.append(u'Track: ' + track) - info_lines.append(u'Date: ' + unicode(item.year)) + info_lines.append(u'Date: ' + six.text_type(item.year)) try: pos = self._id_to_index(item.id) - info_lines.append(u'Pos: ' + unicode(pos)) + info_lines.append(u'Pos: ' + six.text_type(pos)) except ArgumentNotFoundError: # Don't include position if not in playlist. pass - info_lines.append(u'Id: ' + unicode(item.id)) + info_lines.append(u'Id: ' + six.text_type(item.id)) return info_lines @@ -852,7 +853,7 @@ class Server(BaseServer): for name, itemid in iter(sorted(node.files.items())): item = self.lib.get_item(itemid) yield self._item_info(item) - for name, _ in iter(sorted(node.dirs.iteritems())): + for name, _ in iter(sorted(six.iteritems(node.dirs))): dirpath = self._path_join(path, name) if dirpath.startswith(u"/"): # Strip leading slash (libmpc rejects this). @@ -872,12 +873,12 @@ class Server(BaseServer): yield u'file: ' + basepath else: # List a directory. Recurse into both directories and files. - for name, itemid in sorted(node.files.iteritems()): + for name, itemid in sorted(six.iteritems(node.files)): newpath = self._path_join(basepath, name) # "yield from" for v in self._listall(newpath, itemid, info): yield v - for name, subdir in sorted(node.dirs.iteritems()): + for name, subdir in sorted(six.iteritems(node.dirs)): newpath = self._path_join(basepath, name) yield u'directory: ' + newpath for v in self._listall(newpath, subdir, info): @@ -902,11 +903,11 @@ class Server(BaseServer): yield self.lib.get_item(node) else: # Recurse into a directory. - for name, itemid in sorted(node.files.iteritems()): + for name, itemid in sorted(six.iteritems(node.files)): # "yield from" for v in self._all_items(itemid): yield v - for name, subdir in sorted(node.dirs.iteritems()): + for name, subdir in sorted(six.iteritems(node.dirs)): for v in self._all_items(subdir): yield v @@ -917,7 +918,7 @@ class Server(BaseServer): for item in self._all_items(self._resolve_path(path)): self.playlist.append(item) if send_id: - yield u'Id: ' + unicode(item.id) + yield u'Id: ' + six.text_type(item.id) self.playlist_version += 1 def cmd_add(self, conn, path): @@ -938,11 +939,11 @@ class Server(BaseServer): if self.current_index > -1: item = self.playlist[self.current_index] - yield u'bitrate: ' + unicode(item.bitrate / 1000) + yield u'bitrate: ' + six.text_type(item.bitrate / 1000) # Missing 'audio'. (pos, total) = self.player.time() - yield u'time: ' + unicode(pos) + u':' + unicode(total) + yield u'time: ' + six.text_type(pos) + u':' + six.text_type(total) # Also missing 'updating_db'. @@ -957,13 +958,13 @@ class Server(BaseServer): artists, albums, songs, totaltime = tx.query(statement)[0] yield ( - u'artists: ' + unicode(artists), - u'albums: ' + unicode(albums), - u'songs: ' + unicode(songs), - u'uptime: ' + unicode(int(time.time() - self.startup_time)), + u'artists: ' + six.text_type(artists), + u'albums: ' + six.text_type(albums), + u'songs: ' + six.text_type(songs), + u'uptime: ' + six.text_type(int(time.time() - self.startup_time)), u'playtime: ' + u'0', # Missing. - u'db_playtime: ' + unicode(int(totaltime)), - u'db_update: ' + unicode(int(self.updated_time)), + u'db_playtime: ' + six.text_type(int(totaltime)), + u'db_update: ' + six.text_type(int(self.updated_time)), ) # Searching. @@ -1059,7 +1060,7 @@ class Server(BaseServer): rows = tx.query(statement, subvals) for row in rows: - yield show_tag_canon + u': ' + unicode(row[0]) + yield show_tag_canon + u': ' + six.text_type(row[0]) def cmd_count(self, conn, tag, value): """Returns the number and total time of songs matching the @@ -1071,8 +1072,8 @@ class Server(BaseServer): for item in self.lib.items(dbcore.query.MatchQuery(key, value)): songs += 1 playtime += item.length - yield u'songs: ' + unicode(songs) - yield u'playtime: ' + unicode(int(playtime)) + yield u'songs: ' + six.text_type(songs) + yield u'playtime: ' + six.text_type(int(playtime)) # "Outputs." Just a dummy implementation because we don't control # any outputs. @@ -1180,11 +1181,12 @@ class BPDPlugin(BeetsPlugin): ) def func(lib, opts, args): - host = args.pop(0) if args else self.config['host'].get(unicode) + host = self.config['host'].get(six.text_type) + host = args.pop(0) if args else host port = args.pop(0) if args else self.config['port'].get(int) if args: raise beets.ui.UserError(u'too many arguments') - password = self.config['password'].get(unicode) + password = self.config['password'].get(six.text_type) volume = self.config['volume'].get(int) debug = opts.debug or False self.start_bpd(lib, host, int(port), password, volume, debug) diff --git a/beetsplug/bpd/gstplayer.py b/beetsplug/bpd/gstplayer.py index 326739a7a..8fa0f7a81 100644 --- a/beetsplug/bpd/gstplayer.py +++ b/beetsplug/bpd/gstplayer.py @@ -19,12 +19,13 @@ music player. from __future__ import division, absolute_import, print_function +import six import sys import time -import thread +from six.moves import _thread import os import copy -import urllib +from six.moves import urllib from beets import ui import gi @@ -128,9 +129,9 @@ class GstPlayer(object): path. """ self.player.set_state(Gst.State.NULL) - if isinstance(path, unicode): + if isinstance(path, six.text_type): path = path.encode('utf8') - uri = 'file://' + urllib.quote(path) + uri = 'file://' + urllib.parse.quote(path) self.player.set_property("uri", uri) self.player.set_state(Gst.State.PLAYING) self.playing = True @@ -164,7 +165,7 @@ class GstPlayer(object): loop = GLib.MainLoop() loop.run() - thread.start_new_thread(start, ()) + _thread.start_new_thread(start, ()) def time(self): """Returns a tuple containing (position, length) where both diff --git a/beetsplug/bpm.py b/beetsplug/bpm.py index ba284c042..89424f30e 100644 --- a/beetsplug/bpm.py +++ b/beetsplug/bpm.py @@ -18,6 +18,7 @@ from __future__ import division, absolute_import, print_function import time +from six.moves import input from beets import ui from beets.plugins import BeetsPlugin @@ -31,7 +32,7 @@ def bpm(max_strokes): dt = [] for i in range(max_strokes): # Press enter to the rhythm... - s = raw_input() + s = input() if s == '': t1 = time.time() # Only start measuring at the second stroke diff --git a/beetsplug/bucket.py b/beetsplug/bucket.py index 7ce0d0d43..c4be2a3df 100644 --- a/beetsplug/bucket.py +++ b/beetsplug/bucket.py @@ -21,7 +21,8 @@ from __future__ import division, absolute_import, print_function from datetime import datetime import re import string -from itertools import tee, izip +from six.moves import zip +from itertools import tee from beets import plugins, ui @@ -37,7 +38,7 @@ def pairwise(iterable): "s -> (s0,s1), (s1,s2), (s2, s3), ..." a, b = tee(iterable) next(b, None) - return izip(a, b) + return zip(a, b) def span_from_str(span_str): diff --git a/beetsplug/chroma.py b/beetsplug/chroma.py index 148e9c20c..54ea54f4c 100644 --- a/beetsplug/chroma.py +++ b/beetsplug/chroma.py @@ -26,6 +26,7 @@ from beets.util import confit from beets.autotag import hooks import acoustid from collections import defaultdict +import six API_KEY = '1vOwZtEn' SCORE_THRESH = 0.5 @@ -121,7 +122,7 @@ def _all_releases(items): for release_id in release_ids: relcounts[release_id] += 1 - for release_id, count in relcounts.iteritems(): + for release_id, count in six.iteritems(relcounts): if float(count) / len(items) > COMMON_REL_THRESH: yield release_id @@ -181,7 +182,7 @@ class AcoustidPlugin(plugins.BeetsPlugin): def submit_cmd_func(lib, opts, args): try: - apikey = config['acoustid']['apikey'].get(unicode) + apikey = config['acoustid']['apikey'].get(six.text_type) except confit.NotFoundError: raise ui.UserError(u'no Acoustid user API key provided') submit_items(self._log, apikey, lib.items(ui.decargs(args))) diff --git a/beetsplug/convert.py b/beetsplug/convert.py index 21a348bab..63057b46f 100644 --- a/beetsplug/convert.py +++ b/beetsplug/convert.py @@ -29,6 +29,7 @@ from beets.plugins import BeetsPlugin from beets.util.confit import ConfigTypeError from beets import art from beets.util.artresizer import ArtResizer +import six _fs_lock = threading.Lock() _temp_files = [] # Keep track of temporary transcoded files for deletion. @@ -55,7 +56,7 @@ def get_format(fmt=None): """Return the command template and the extension from the config. """ if not fmt: - fmt = config['convert']['format'].get(unicode).lower() + fmt = config['convert']['format'].get(six.text_type).lower() fmt = ALIASES.get(fmt, fmt) try: @@ -74,14 +75,14 @@ def get_format(fmt=None): # Convenience and backwards-compatibility shortcuts. keys = config['convert'].keys() if 'command' in keys: - command = config['convert']['command'].get(unicode) + command = config['convert']['command'].get(six.text_type) elif 'opts' in keys: # Undocumented option for backwards compatibility with < 1.3.1. command = u'ffmpeg -i $source -y {0} $dest'.format( - config['convert']['opts'].get(unicode) + config['convert']['opts'].get(six.text_type) ) if 'extension' in keys: - extension = config['convert']['extension'].get(unicode) + extension = config['convert']['extension'].get(six.text_type) return (command.encode('utf8'), extension.encode('utf8')) @@ -389,7 +390,7 @@ class ConvertPlugin(BeetsPlugin): path_formats = ui.get_path_formats() if not opts.format: - opts.format = self.config['format'].get(unicode).lower() + opts.format = self.config['format'].get(six.text_type).lower() pretend = opts.pretend if opts.pretend is not None else \ self.config['pretend'].get(bool) @@ -422,7 +423,7 @@ class ConvertPlugin(BeetsPlugin): """Transcode a file automatically after it is imported into the library. """ - fmt = self.config['format'].get(unicode).lower() + fmt = self.config['format'].get(six.text_type).lower() if should_transcode(item, fmt): command, ext = get_format() diff --git a/beetsplug/discogs.py b/beetsplug/discogs.py index 85010d3a2..b33dec7ed 100644 --- a/beetsplug/discogs.py +++ b/beetsplug/discogs.py @@ -27,13 +27,14 @@ from beets.util import confit from discogs_client import Release, Client from discogs_client.exceptions import DiscogsAPIError from requests.exceptions import ConnectionError +from six.moves import http_client import beets import re import time import json import socket -import httplib import os +import six # Silence spurious INFO log lines generated by urllib3. @@ -43,7 +44,7 @@ urllib3_logger.setLevel(logging.CRITICAL) USER_AGENT = u'beets/{0} +http://beets.io/'.format(beets.__version__) # Exceptions that discogs_client should really handle but does not. -CONNECTION_ERRORS = (ConnectionError, socket.error, httplib.HTTPException, +CONNECTION_ERRORS = (ConnectionError, socket.error, http_client.HTTPException, ValueError, # JSON decoding raises a ValueError. DiscogsAPIError) @@ -66,8 +67,8 @@ class DiscogsPlugin(BeetsPlugin): def setup(self, session=None): """Create the `discogs_client` field. Authenticate if necessary. """ - c_key = self.config['apikey'].get(unicode) - c_secret = self.config['apisecret'].get(unicode) + c_key = self.config['apikey'].get(six.text_type) + c_secret = self.config['apisecret'].get(six.text_type) # Get the OAuth token from a file or log in. try: @@ -225,7 +226,7 @@ class DiscogsPlugin(BeetsPlugin): result.data['formats'][0].get('descriptions', [])) or None va = result.data['artists'][0]['name'].lower() == 'various' if va: - artist = config['va_name'].get(unicode) + artist = config['va_name'].get(six.text_type) year = result.data['year'] label = result.data['labels'][0]['name'] mediums = len(set(t.medium for t in tracks)) diff --git a/beetsplug/duplicates.py b/beetsplug/duplicates.py index 4f0397171..141e36927 100644 --- a/beetsplug/duplicates.py +++ b/beetsplug/duplicates.py @@ -23,6 +23,7 @@ from beets.plugins import BeetsPlugin from beets.ui import decargs, print_, vararg_callback, Subcommand, UserError from beets.util import command_output, displayable_path, subprocess from beets.library import Item, Album +import six PLUGIN = 'duplicates' @@ -264,7 +265,7 @@ class DuplicatesPlugin(BeetsPlugin): # between a bytes object and the empty Unicode # string ''. return v is not None and \ - (v != '' if isinstance(v, unicode) else True) + (v != '' if isinstance(v, six.text_type) else True) fields = kind.all_keys() key = lambda x: sum(1 for f in fields if truthy(getattr(x, f))) else: @@ -329,7 +330,7 @@ class DuplicatesPlugin(BeetsPlugin): """Generate triples of keys, duplicate counts, and constituent objects. """ offset = 0 if full else 1 - for k, objs in self._group_by(objs, keys, strict).iteritems(): + for k, objs in six.iteritems(self._group_by(objs, keys, strict)): if len(objs) > 1: objs = self._order(objs, tiebreak) if merge: diff --git a/beetsplug/edit.py b/beetsplug/edit.py index c8522849d..4a55e59ed 100644 --- a/beetsplug/edit.py +++ b/beetsplug/edit.py @@ -27,6 +27,7 @@ import subprocess import yaml from tempfile import NamedTemporaryFile import os +import six # These "safe" types can avoid the format/parse cycle that most fields go @@ -82,7 +83,7 @@ def load(s): # Convert all keys to strings. They started out as strings, # but the user may have inadvertently messed this up. - out.append({unicode(k): v for k, v in d.items()}) + out.append({six.text_type(k): v for k, v in d.items()}) except yaml.YAMLError as e: raise ParseError(u'invalid YAML: {}'.format(e)) @@ -141,7 +142,7 @@ def apply_(obj, data): else: # Either the field was stringified originally or the user changed # it from a safe type to an unsafe one. Parse it as a string. - obj.set_parse(key, unicode(value)) + obj.set_parse(key, six.text_type(value)) class EditPlugin(plugins.BeetsPlugin): diff --git a/beetsplug/embyupdate.py b/beetsplug/embyupdate.py index 1237762fe..73fa3dcc3 100644 --- a/beetsplug/embyupdate.py +++ b/beetsplug/embyupdate.py @@ -12,8 +12,8 @@ from __future__ import division, absolute_import, print_function from beets import config from beets.plugins import BeetsPlugin -from urllib import urlencode -from urlparse import urljoin, parse_qs, urlsplit, urlunsplit +from six.moves.urllib.parse import urlencode +from six.moves.urllib.parse import urljoin, parse_qs, urlsplit, urlunsplit import hashlib import requests diff --git a/beetsplug/fetchart.py b/beetsplug/fetchart.py index 4d4158ec1..2488144f1 100644 --- a/beetsplug/fetchart.py +++ b/beetsplug/fetchart.py @@ -33,6 +33,7 @@ from beets.mediafile import _image_mime_type from beets.util.artresizer import ArtResizer from beets.util import confit from beets.util import syspath, bytestring_path +import six try: import itunes @@ -600,7 +601,7 @@ class Wikipedia(RemoteArtSource): try: data = wikipedia_response.json() results = data['query']['pages'] - for _, result in results.iteritems(): + for _, result in six.iteritems(results): image_url = result['imageinfo'][0]['url'] yield self._candidate(url=image_url, match=Candidate.MATCH_EXACT) @@ -727,7 +728,7 @@ class FetchArtPlugin(plugins.BeetsPlugin, RequestMixin): confit.String(pattern=self.PAT_PERCENT)])) self.margin_px = None self.margin_percent = None - if type(self.enforce_ratio) is unicode: + if type(self.enforce_ratio) is six.text_type: if self.enforce_ratio[-1] == u'%': self.margin_percent = float(self.enforce_ratio[:-1]) / 100 elif self.enforce_ratio[-2:] == u'px': diff --git a/beetsplug/fromfilename.py b/beetsplug/fromfilename.py index e9c49bee3..648df6258 100644 --- a/beetsplug/fromfilename.py +++ b/beetsplug/fromfilename.py @@ -22,6 +22,7 @@ from beets import plugins from beets.util import displayable_path import os import re +import six # Filename field extraction patterns. @@ -132,7 +133,7 @@ def apply_matches(d): # Apply the title and track. for item in d: if bad_title(item.title): - item.title = unicode(d[item][title_field]) + item.title = six.text_type(d[item][title_field]) if 'track' in d[item] and item.track == 0: item.track = int(d[item]['track']) diff --git a/beetsplug/ftintitle.py b/beetsplug/ftintitle.py index eefdfcf15..4e415b48d 100644 --- a/beetsplug/ftintitle.py +++ b/beetsplug/ftintitle.py @@ -22,6 +22,7 @@ import re from beets import plugins from beets import ui from beets.util import displayable_path +import six def split_on_feat(artist): @@ -137,7 +138,7 @@ class FtInTitlePlugin(plugins.BeetsPlugin): # Only update the title if it does not already contain a featured # artist and if we do not drop featuring information. if not drop_feat and not contains_feat(item.title): - feat_format = self.config['format'].get(unicode) + feat_format = self.config['format'].get(six.text_type) new_format = feat_format.format(feat_part) new_title = u"{0} {1}".format(item.title, new_format) self._log.info(u'title: {0} -> {1}', item.title, new_title) diff --git a/beetsplug/fuzzy.py b/beetsplug/fuzzy.py index 3decdc602..1624ee1c1 100644 --- a/beetsplug/fuzzy.py +++ b/beetsplug/fuzzy.py @@ -22,6 +22,7 @@ from beets.plugins import BeetsPlugin from beets.dbcore.query import StringFieldQuery from beets import config import difflib +import six class FuzzyQuery(StringFieldQuery): @@ -44,5 +45,5 @@ class FuzzyPlugin(BeetsPlugin): }) def queries(self): - prefix = self.config['prefix'].get(basestring) + prefix = self.config['prefix'].get(six.string_types) return {prefix: FuzzyQuery} diff --git a/beetsplug/hook.py b/beetsplug/hook.py index 4f2b8f0e2..80daad68c 100644 --- a/beetsplug/hook.py +++ b/beetsplug/hook.py @@ -21,6 +21,7 @@ import subprocess from beets.plugins import BeetsPlugin from beets.ui import _arg_encoding from beets.util import shlex_split +import six class CodingFormatter(string.Formatter): @@ -79,8 +80,8 @@ class HookPlugin(BeetsPlugin): for hook_index in range(len(hooks)): hook = self.config['hooks'][hook_index] - hook_event = hook['event'].get(unicode) - hook_command = hook['command'].get(unicode) + hook_event = hook['event'].get(six.text_type) + hook_command = hook['command'].get(six.text_type) self.create_and_register_hook(hook_event, hook_command) diff --git a/beetsplug/importadded.py b/beetsplug/importadded.py index 77c7e7ab8..707f04abd 100644 --- a/beetsplug/importadded.py +++ b/beetsplug/importadded.py @@ -12,6 +12,7 @@ import os from beets import util from beets import importer from beets.plugins import BeetsPlugin +import six class ImportAddedPlugin(BeetsPlugin): @@ -62,7 +63,7 @@ class ImportAddedPlugin(BeetsPlugin): def record_reimported(self, task, session): self.reimported_item_ids = set(item.id for item, replaced_items - in task.replaced_items.iteritems() + in six.iteritems(task.replaced_items) if replaced_items) self.replaced_album_paths = set(task.replaced_albums.keys()) diff --git a/beetsplug/importfeeds.py b/beetsplug/importfeeds.py index 0d9e50616..e02c5793f 100644 --- a/beetsplug/importfeeds.py +++ b/beetsplug/importfeeds.py @@ -14,6 +14,7 @@ # included in all copies or substantial portions of the Software. from __future__ import division, absolute_import, print_function +import six """Write paths of imported files in various formats to ease later import in a music player. Also allow printing the new file locations to stdout in case @@ -119,7 +120,7 @@ class ImportFeedsPlugin(BeetsPlugin): if 'm3u' in formats: m3u_basename = bytestring_path( - self.config['m3u_name'].get(unicode)) + self.config['m3u_name'].get(six.text_type)) m3u_path = os.path.join(feedsdir, m3u_basename) _write_m3u(m3u_path, paths) diff --git a/beetsplug/info.py b/beetsplug/info.py index d29d9b45f..026c8d8e6 100644 --- a/beetsplug/info.py +++ b/beetsplug/info.py @@ -26,6 +26,7 @@ from beets import ui from beets import mediafile from beets.library import Item from beets.util import displayable_path, normpath, syspath +import six def tag_data(lib, args): @@ -73,7 +74,7 @@ def library_data_emitter(item): def update_summary(summary, tags): - for key, value in tags.iteritems(): + for key, value in six.iteritems(tags): if key not in summary: summary[key] = value elif summary[key] != value: @@ -96,7 +97,7 @@ def print_data(data, item=None, fmt=None): path = displayable_path(item.path) if item else None formatted = {} - for key, value in data.iteritems(): + for key, value in six.iteritems(data): if isinstance(value, list): formatted[key] = u'; '.join(value) if value is not None: @@ -123,7 +124,7 @@ def print_data_keys(data, item=None): """ path = displayable_path(item.path) if item else None formatted = [] - for key, value in data.iteritems(): + for key, value in six.iteritems(data): formatted.append(key) if len(formatted) == 0: diff --git a/beetsplug/inline.py b/beetsplug/inline.py index 6e3771f2a..5787b739e 100644 --- a/beetsplug/inline.py +++ b/beetsplug/inline.py @@ -22,6 +22,7 @@ import itertools from beets.plugins import BeetsPlugin from beets import config +import six FUNC_NAME = u'__INLINE_FUNC__' @@ -32,7 +33,7 @@ class InlineError(Exception): def __init__(self, code, exc): super(InlineError, self).__init__( (u"error in inline path field code:\n" - u"%s\n%s: %s") % (code, type(exc).__name__, unicode(exc)) + u"%s\n%s: %s") % (code, type(exc).__name__, six.text_type(exc)) ) @@ -64,14 +65,14 @@ class InlinePlugin(BeetsPlugin): for key, view in itertools.chain(config['item_fields'].items(), config['pathfields'].items()): self._log.debug(u'adding item field {0}', key) - func = self.compile_inline(view.get(unicode), False) + func = self.compile_inline(view.get(six.text_type), False) if func is not None: self.template_fields[key] = func # Album fields. for key, view in config['album_fields'].items(): self._log.debug(u'adding album field {0}', key) - func = self.compile_inline(view.get(unicode), True) + func = self.compile_inline(view.get(six.text_type), True) if func is not None: self.album_template_fields[key] = func diff --git a/beetsplug/keyfinder.py b/beetsplug/keyfinder.py index b6131a4b0..dad62487f 100644 --- a/beetsplug/keyfinder.py +++ b/beetsplug/keyfinder.py @@ -23,6 +23,7 @@ import subprocess from beets import ui from beets import util from beets.plugins import BeetsPlugin +import six class KeyFinderPlugin(BeetsPlugin): @@ -52,7 +53,7 @@ class KeyFinderPlugin(BeetsPlugin): def find_key(self, items, write=False): overwrite = self.config['overwrite'].get(bool) - bin = util.bytestring_path(self.config['bin'].get(unicode)) + bin = util.bytestring_path(self.config['bin'].get(six.text_type)) for item in items: if item['initial_key'] and not overwrite: diff --git a/beetsplug/lastgenre/__init__.py b/beetsplug/lastgenre/__init__.py index 97daafb6b..abda7ed6b 100644 --- a/beetsplug/lastgenre/__init__.py +++ b/beetsplug/lastgenre/__init__.py @@ -14,6 +14,7 @@ # included in all copies or substantial portions of the Software. from __future__ import division, absolute_import, print_function +import six """Gets genres for imported music based on Last.fm tags. @@ -71,7 +72,7 @@ def flatten_tree(elem, path, branches): for sub in elem: flatten_tree(sub, path, branches) else: - branches.append(path + [unicode(elem)]) + branches.append(path + [six.text_type(elem)]) def find_parents(candidate, branches): @@ -186,7 +187,7 @@ class LastGenrePlugin(plugins.BeetsPlugin): # the original tags list tags = [x.title() for x in tags if self._is_allowed(x)] - return self.config['separator'].get(unicode).join( + return self.config['separator'].get(six.text_type).join( tags[:self.config['count'].get(int)] ) @@ -221,7 +222,8 @@ class LastGenrePlugin(plugins.BeetsPlugin): if any(not s for s in args): return None - key = u'{0}.{1}'.format(entity, u'-'.join(unicode(a) for a in args)) + key = u'{0}.{1}'.format(entity, + u'-'.join(six.text_type(a) for a in args)) if key in self._genre_cache: return self._genre_cache[key] else: @@ -297,7 +299,7 @@ class LastGenrePlugin(plugins.BeetsPlugin): result = None if isinstance(obj, library.Item): result = self.fetch_artist_genre(obj) - elif obj.albumartist != config['va_name'].get(unicode): + elif obj.albumartist != config['va_name'].get(six.text_type): result = self.fetch_album_artist_genre(obj) else: # For "Various Artists", pick the most popular track genre. diff --git a/beetsplug/lastimport.py b/beetsplug/lastimport.py index 2d8cc7008..4d0557331 100644 --- a/beetsplug/lastimport.py +++ b/beetsplug/lastimport.py @@ -15,6 +15,7 @@ from __future__ import division, absolute_import, print_function +from six.moves import range import pylast from pylast import TopItem, _extract, _number from beets import ui @@ -22,6 +23,7 @@ from beets import dbcore from beets import config from beets import plugins from beets.dbcore import types +import six API_URL = 'http://ws.audioscrobbler.com/2.0/' @@ -110,7 +112,7 @@ class CustomUser(pylast.User): def import_lastfm(lib, log): - user = config['lastfm']['user'].get(unicode) + user = config['lastfm']['user'].get(six.text_type) per_page = config['lastimport']['per_page'].get(int) if not user: @@ -192,7 +194,7 @@ def process_tracks(lib, tracks, log): total_fails = 0 log.info(u'Received {0} tracks in this page, processing...', total) - for num in xrange(0, total): + for num in range(0, total): song = None trackid = tracks[num]['mbid'].strip() artist = tracks[num]['artist'].get('name', '').strip() diff --git a/beetsplug/lyrics.py b/beetsplug/lyrics.py index 1bce066c9..8aaebc672 100644 --- a/beetsplug/lyrics.py +++ b/beetsplug/lyrics.py @@ -24,9 +24,9 @@ import json import re import requests import unicodedata -import urllib import warnings -from HTMLParser import HTMLParseError +from six.moves import urllib +import six try: from bs4 import SoupStrainer, BeautifulSoup @@ -40,6 +40,15 @@ try: except ImportError: HAS_LANGDETECT = False +try: + # PY3: HTMLParseError was removed in 3.5 as strict mode + # was deprecated in 3.3. + # https://docs.python.org/3.3/library/html.parser.html + from six.moves.html_parser import HTMLParseError +except ImportError: + class HTMLParseError(Exception): + pass + from beets import plugins from beets import ui @@ -177,11 +186,11 @@ class Backend(object): @staticmethod def _encode(s): """Encode the string for inclusion in a URL""" - if isinstance(s, unicode): + if isinstance(s, six.text_type): for char, repl in URL_CHARACTERS.items(): s = s.replace(char, repl) s = s.encode('utf8', 'ignore') - return urllib.quote(s) + return urllib.parse.quote(s) def build_url(self, artist, title): return self.URL_PATTERN % (self._encode(artist.title()), @@ -223,7 +232,7 @@ class SymbolsReplaced(Backend): @classmethod def _encode(cls, s): - for old, new in cls.REPLACEMENTS.iteritems(): + for old, new in six.iteritems(cls.REPLACEMENTS): s = re.sub(old, new, s) return super(SymbolsReplaced, cls)._encode(s) @@ -250,13 +259,13 @@ class Genius(Backend): """Fetch lyrics from Genius via genius-api.""" def __init__(self, config, log): super(Genius, self).__init__(config, log) - self.api_key = config['genius_api_key'].get(unicode) + self.api_key = config['genius_api_key'].get(six.text_type) self.headers = {'Authorization': "Bearer %s" % self.api_key} def search_genius(self, artist, title): query = u"%s %s" % (artist, title) url = u'https://api.genius.com/search?q=%s' \ - % (urllib.quote(query.encode('utf8'))) + % (urllib.parse.quote(query.encode('utf8'))) self._log.debug(u'genius: requesting search {}', url) try: @@ -461,8 +470,8 @@ class Google(Backend): """Fetch lyrics from Google search results.""" def __init__(self, config, log): super(Google, self).__init__(config, log) - self.api_key = config['google_API_key'].get(unicode) - self.engine_id = config['google_engine_ID'].get(unicode) + self.api_key = config['google_API_key'].get(six.text_type) + self.engine_id = config['google_engine_ID'].get(six.text_type) def is_lyrics(self, text, artist=None): """Determine whether the text seems to be valid lyrics. @@ -503,7 +512,7 @@ class Google(Backend): try: text = unicodedata.normalize('NFKD', text).encode('ascii', 'ignore') - text = unicode(re.sub('[-\s]+', ' ', text.decode('utf-8'))) + text = six.text_type(re.sub('[-\s]+', ' ', text.decode('utf-8'))) except UnicodeDecodeError: self._log.exception(u"Failing to normalize '{0}'", text) return text @@ -542,9 +551,9 @@ class Google(Backend): query = u"%s %s" % (artist, title) url = u'https://www.googleapis.com/customsearch/v1?key=%s&cx=%s&q=%s' \ % (self.api_key, self.engine_id, - urllib.quote(query.encode('utf8'))) + urllib.parse.quote(query.encode('utf8'))) - data = urllib.urlopen(url) + data = urllib.request.urlopen(url) data = json.load(data) if 'error' in data: reason = data['error']['errors'][0]['reason'] @@ -643,7 +652,7 @@ class LyricsPlugin(plugins.BeetsPlugin): oauth_url = 'https://datamarket.accesscontrol.windows.net/v2/OAuth2-13' oauth_token = json.loads(requests.post( oauth_url, - data=urllib.urlencode(params)).content) + data=urllib.parse.urlencode(params)).content) if 'access_token' in oauth_token: return "Bearer " + oauth_token['access_token'] else: diff --git a/beetsplug/mbcollection.py b/beetsplug/mbcollection.py index b95ba6fed..0760be0e5 100644 --- a/beetsplug/mbcollection.py +++ b/beetsplug/mbcollection.py @@ -22,6 +22,7 @@ from beets import config import musicbrainzngs import re +import six SUBMISSION_CHUNK_SIZE = 200 UUID_REGEX = r'^[a-f0-9]{8}(-[a-f0-9]{4}){3}-[a-f0-9]{12}$' @@ -57,8 +58,8 @@ class MusicBrainzCollectionPlugin(BeetsPlugin): super(MusicBrainzCollectionPlugin, self).__init__() config['musicbrainz']['pass'].redact = True musicbrainzngs.auth( - config['musicbrainz']['user'].get(unicode), - config['musicbrainz']['pass'].get(unicode), + config['musicbrainz']['user'].get(six.text_type), + config['musicbrainz']['pass'].get(six.text_type), ) self.config.add({'auto': False}) if self.config['auto']: diff --git a/beetsplug/metasync/__init__.py b/beetsplug/metasync/__init__.py index 3fc0be4cc..02f0b0f9b 100644 --- a/beetsplug/metasync/__init__.py +++ b/beetsplug/metasync/__init__.py @@ -24,6 +24,7 @@ from importlib import import_module from beets.util.confit import ConfigValueError from beets import ui from beets.plugins import BeetsPlugin +import six METASYNC_MODULE = 'beetsplug.metasync' @@ -35,9 +36,7 @@ SOURCES = { } -class MetaSource(object): - __metaclass__ = ABCMeta - +class MetaSource(six.with_metaclass(ABCMeta, object)): def __init__(self, config, log): self.item_types = {} self.config = config diff --git a/beetsplug/metasync/itunes.py b/beetsplug/metasync/itunes.py index a62746848..17ab1637f 100644 --- a/beetsplug/metasync/itunes.py +++ b/beetsplug/metasync/itunes.py @@ -23,8 +23,8 @@ import os import shutil import tempfile import plistlib -import urllib -from urlparse import urlparse + +from six.moves.urllib.parse import urlparse, unquote from time import mktime from beets import util @@ -57,7 +57,7 @@ def _norm_itunes_path(path): # E.g., '\\G:\\Music\\bar' needs to be stripped to 'G:\\Music\\bar' return util.bytestring_path(os.path.normpath( - urllib.unquote(urlparse(path).path)).lstrip('\\')).lower() + unquote(urlparse(path).path)).lstrip('\\')).lower() class Itunes(MetaSource): diff --git a/beetsplug/mpdstats.py b/beetsplug/mpdstats.py index fedf3276c..ca8dc9c7b 100644 --- a/beetsplug/mpdstats.py +++ b/beetsplug/mpdstats.py @@ -27,6 +27,7 @@ from beets import plugins from beets import library from beets.util import displayable_path from beets.dbcore import types +import six # If we lose the connection, how many times do we want to retry and how # much time should we wait between retries? @@ -49,7 +50,7 @@ def is_url(path): # see http://www.tarmack.eu/code/mpdunicode.py for the general idea class MPDClient(mpd.MPDClient): def _write_command(self, command, args=[]): - args = [unicode(arg).encode('utf-8') for arg in args] + args = [six.text_type(arg).encode('utf-8') for arg in args] super(MPDClient, self)._write_command(command, args) def _read_line(self): @@ -64,14 +65,14 @@ class MPDClientWrapper(object): self._log = log self.music_directory = ( - mpd_config['music_directory'].get(unicode)) + mpd_config['music_directory'].get(six.text_type)) self.client = MPDClient() def connect(self): """Connect to the MPD. """ - host = mpd_config['host'].get(unicode) + host = mpd_config['host'].get(six.text_type) port = mpd_config['port'].get(int) if host[0] in ['/', '~']: @@ -83,7 +84,7 @@ class MPDClientWrapper(object): except socket.error as e: raise ui.UserError(u'could not connect to MPD: {0}'.format(e)) - password = mpd_config['password'].get(unicode) + password = mpd_config['password'].get(six.text_type) if password: try: self.client.password(password) diff --git a/beetsplug/mpdupdate.py b/beetsplug/mpdupdate.py index f828ba5d7..36449a5ac 100644 --- a/beetsplug/mpdupdate.py +++ b/beetsplug/mpdupdate.py @@ -27,6 +27,7 @@ from beets.plugins import BeetsPlugin import os import socket from beets import config +import six # No need to introduce a dependency on an MPD library for such a @@ -86,9 +87,9 @@ class MPDUpdatePlugin(BeetsPlugin): def update(self, lib): self.update_mpd( - config['mpd']['host'].get(unicode), + config['mpd']['host'].get(six.text_type), config['mpd']['port'].get(int), - config['mpd']['password'].get(unicode), + config['mpd']['password'].get(six.text_type), ) def update_mpd(self, host='localhost', port=6600, password=None): @@ -101,7 +102,7 @@ class MPDUpdatePlugin(BeetsPlugin): s = BufferedSocket(host, port) except socket.error as e: self._log.warning(u'MPD connection failed: {0}', - unicode(e.strerror)) + six.text_type(e.strerror)) return resp = s.readline() diff --git a/beetsplug/plexupdate.py b/beetsplug/plexupdate.py index ef50fde73..7dbc8b018 100644 --- a/beetsplug/plexupdate.py +++ b/beetsplug/plexupdate.py @@ -12,9 +12,8 @@ Put something like the following in your config.yaml to configure: from __future__ import division, absolute_import, print_function import requests -from urlparse import urljoin -from urllib import urlencode import xml.etree.ElementTree as ET +from six.moves.urllib.parse import urljoin, urlencode from beets import config from beets.plugins import BeetsPlugin diff --git a/beetsplug/replaygain.py b/beetsplug/replaygain.py index a1e573361..27659ca27 100644 --- a/beetsplug/replaygain.py +++ b/beetsplug/replaygain.py @@ -18,15 +18,16 @@ from __future__ import division, absolute_import, print_function import subprocess import os import collections -import itertools import sys import warnings import re +from six.moves import zip from beets import logging from beets import ui from beets.plugins import BeetsPlugin from beets.util import syspath, command_output, displayable_path +import six # Utilities. @@ -102,7 +103,7 @@ class Bs1770gainBackend(Backend): 'method': 'replaygain', }) self.chunk_at = config['chunk_at'].as_number() - self.method = b'--' + bytes(config['method'].get(unicode)) + self.method = b'--' + bytes(config['method'].get(six.text_type)) cmd = b'bs1770gain' try: @@ -256,7 +257,7 @@ class CommandBackend(Backend): 'noclip': True, }) - self.command = config["command"].get(unicode) + self.command = config["command"].get(six.text_type) if self.command: # Explicit executable path. @@ -809,7 +810,7 @@ class ReplayGainPlugin(BeetsPlugin): }) self.overwrite = self.config['overwrite'].get(bool) - backend_name = self.config['backend'].get(unicode) + backend_name = self.config['backend'].get(six.text_type) if backend_name not in self.backends: raise ui.UserError( u"Selected ReplayGain backend {0} is not supported. " @@ -883,8 +884,7 @@ class ReplayGainPlugin(BeetsPlugin): ) self.store_album_gain(album, album_gain.album_gain) - for item, track_gain in itertools.izip(album.items(), - album_gain.track_gains): + for item, track_gain in zip(album.items(), album_gain.track_gains): self.store_track_gain(item, track_gain) if write: item.try_write() diff --git a/beetsplug/rewrite.py b/beetsplug/rewrite.py index b0104a118..b04973101 100644 --- a/beetsplug/rewrite.py +++ b/beetsplug/rewrite.py @@ -24,6 +24,7 @@ from collections import defaultdict from beets.plugins import BeetsPlugin from beets import ui from beets import library +import six def rewriter(field, rules): @@ -51,7 +52,7 @@ class RewritePlugin(BeetsPlugin): # Gather all the rewrite rules for each field. rules = defaultdict(list) for key, view in self.config.items(): - value = view.get(unicode) + value = view.get(six.text_type) try: fieldname, pattern = key.split(None, 1) except ValueError: @@ -68,7 +69,7 @@ class RewritePlugin(BeetsPlugin): rules['albumartist'].append((pattern, value)) # Replace each template field with the new rewriter function. - for fieldname, fieldrules in rules.iteritems(): + for fieldname, fieldrules in six.iteritems(rules): getter = rewriter(fieldname, fieldrules) self.template_fields[fieldname] = getter if fieldname in library.Album._fields: diff --git a/beetsplug/smartplaylist.py b/beetsplug/smartplaylist.py index 1ccb37030..11c5530b2 100644 --- a/beetsplug/smartplaylist.py +++ b/beetsplug/smartplaylist.py @@ -25,6 +25,7 @@ from beets.library import Item, Album, parse_query_string from beets.dbcore import OrQuery from beets.dbcore.query import MultipleSort, ParsingError import os +import six class SmartPlaylistPlugin(BeetsPlugin): @@ -106,7 +107,7 @@ class SmartPlaylistPlugin(BeetsPlugin): qs = playlist.get(key) if qs is None: query_and_sort = None, None - elif isinstance(qs, basestring): + elif isinstance(qs, six.string_types): query_and_sort = parse_query_string(qs, Model) elif len(qs) == 1: query_and_sort = parse_query_string(qs[0], Model) diff --git a/beetsplug/spotify.py b/beetsplug/spotify.py index 77df15790..33570b5e4 100644 --- a/beetsplug/spotify.py +++ b/beetsplug/spotify.py @@ -9,6 +9,7 @@ from beets.plugins import BeetsPlugin from beets.ui import decargs from beets import ui from requests.exceptions import HTTPError +import six class SpotifyPlugin(BeetsPlugin): @@ -170,6 +171,6 @@ class SpotifyPlugin(BeetsPlugin): else: for item in ids: - print(unicode.encode(self.open_url + item)) + print(six.text_type.encode(self.open_url + item)) else: self._log.warn(u'No Spotify tracks found from beets query') diff --git a/beetsplug/the.py b/beetsplug/the.py index 6bed4c6ed..149233c45 100644 --- a/beetsplug/the.py +++ b/beetsplug/the.py @@ -19,6 +19,7 @@ from __future__ import division, absolute_import, print_function import re from beets.plugins import BeetsPlugin +import six __author__ = 'baobab@heresiarch.info' __version__ = '1.1' @@ -81,7 +82,7 @@ class ThePlugin(BeetsPlugin): if self.config['strip']: return r else: - fmt = self.config['format'].get(unicode) + fmt = self.config['format'].get(six.text_type) return fmt.format(r, t.strip()).strip() else: return u'' diff --git a/beetsplug/thumbnails.py b/beetsplug/thumbnails.py index ce0a9490e..1ea90f01e 100644 --- a/beetsplug/thumbnails.py +++ b/beetsplug/thumbnails.py @@ -35,6 +35,7 @@ from beets.plugins import BeetsPlugin from beets.ui import Subcommand, decargs from beets import util from beets.util.artresizer import ArtResizer, get_im_version, get_pil_version +import six BASE_DIR = os.path.join(BaseDirectory.xdg_cache_home, "thumbnails") @@ -169,8 +170,9 @@ class ThumbnailsPlugin(BeetsPlugin): """Write required metadata to the thumbnail See http://standards.freedesktop.org/thumbnail-spec/latest/x142.html """ + mtime = os.stat(album.artpath).st_mtime metadata = {"Thumb::URI": self.get_uri(album.artpath), - "Thumb::MTime": unicode(os.stat(album.artpath).st_mtime)} + "Thumb::MTime": six.text_type(mtime)} try: self.write_metadata(image_path, metadata) except Exception: diff --git a/beetsplug/web/__init__.py b/beetsplug/web/__init__.py index 67d99db67..6f480fb36 100644 --- a/beetsplug/web/__init__.py +++ b/beetsplug/web/__init__.py @@ -25,6 +25,7 @@ from flask import g from werkzeug.routing import BaseConverter, PathConverter import os import json +import six # Utilities. @@ -321,7 +322,7 @@ class WebPlugin(BeetsPlugin): } CORS(app) # Start the web application. - app.run(host=self.config['host'].get(unicode), + app.run(host=self.config['host'].get(six.text_type), port=self.config['port'].get(int), debug=opts.debug, threaded=True) cmd.func = func diff --git a/beetsplug/zero.py b/beetsplug/zero.py index d20f76166..80925741b 100644 --- a/beetsplug/zero.py +++ b/beetsplug/zero.py @@ -22,6 +22,7 @@ from beets.plugins import BeetsPlugin from beets.mediafile import MediaFile from beets.importer import action from beets.util import confit +import six __author__ = 'baobab@heresiarch.info' __version__ = '0.10' @@ -113,7 +114,7 @@ class ZeroPlugin(BeetsPlugin): if patterns is True: return True for p in patterns: - if re.search(p, unicode(field), flags=re.IGNORECASE): + if re.search(p, six.text_type(field), flags=re.IGNORECASE): return True return False diff --git a/docs/faq.rst b/docs/faq.rst index 974b0dce6..ff74a5350 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -289,7 +289,7 @@ Also note that beets may take some time to quit after ^C is typed; it tries to clean up after itself briefly even when canceled. (For developers: this is because the UI thread is blocking on -``raw_input`` and cannot be interrupted by the main thread, which is +``input`` and cannot be interrupted by the main thread, which is trying to close all pipeline stages in the exception handler by setting a flag. There is no simple way to remedy this.) diff --git a/setup.py b/setup.py index 6877d794b..e736b3b09 100755 --- a/setup.py +++ b/setup.py @@ -86,6 +86,7 @@ setup( }, install_requires=[ + 'six', 'enum34>=1.0.4', 'mutagen>=1.27', 'munkres', diff --git a/test/helper.py b/test/helper.py index 9335a7d6b..8df7c8e3c 100644 --- a/test/helper.py +++ b/test/helper.py @@ -40,7 +40,7 @@ import shutil import subprocess from tempfile import mkdtemp, mkstemp from contextlib import contextmanager -from StringIO import StringIO +from six import StringIO from enum import Enum import beets @@ -56,6 +56,7 @@ from beets import util # TODO Move AutotagMock here from test import _common +import six class LogCapture(logging.Handler): @@ -65,7 +66,7 @@ class LogCapture(logging.Handler): self.messages = [] def emit(self, record): - self.messages.append(unicode(record.msg)) + self.messages.append(six.text_type(record.msg)) @contextmanager @@ -89,7 +90,8 @@ def control_stdin(input=None): """ org = sys.stdin sys.stdin = StringIO(input) - sys.stdin.encoding = 'utf8' + if six.PY2: # StringIO encoding attr isn't writable in python >= 3 + sys.stdin.encoding = 'utf8' try: yield sys.stdin finally: @@ -108,7 +110,8 @@ def capture_stdout(): """ org = sys.stdout sys.stdout = capture = StringIO() - sys.stdout.encoding = 'utf8' + if six.PY2: # StringIO encoding attr isn't writable in python >= 3 + sys.stdout.encoding = 'utf8' try: yield sys.stdout finally: @@ -121,7 +124,7 @@ def has_program(cmd, args=['--version']): """ full_cmd = [cmd] + args for i, elem in enumerate(full_cmd): - if isinstance(elem, unicode): + if isinstance(elem, six.text_type): full_cmd[i] = elem.encode(_arg_encoding()) try: with open(os.devnull, 'wb') as devnull: diff --git a/test/test_autotag.py b/test/test_autotag.py index 61a4d5d6f..5e6f76ffb 100644 --- a/test/test_autotag.py +++ b/test/test_autotag.py @@ -29,6 +29,7 @@ from beets.library import Item from beets.util import plurality from beets.autotag import AlbumInfo, TrackInfo from beets import config +import six class PluralityTest(_common.TestCase): @@ -611,7 +612,7 @@ class AssignmentTest(unittest.TestCase): match.assign_items(items, trackinfo) self.assertEqual(extra_items, []) self.assertEqual(extra_tracks, []) - for item, info in mapping.iteritems(): + for item, info in six.iteritems(mapping): self.assertEqual(items.index(item), trackinfo.index(info)) diff --git a/test/test_config_command.py b/test/test_config_command.py index 77a370ce3..d59181373 100644 --- a/test/test_config_command.py +++ b/test/test_config_command.py @@ -14,6 +14,7 @@ from beets import config from test._common import unittest from test.helper import TestHelper, capture_stdout from beets.library import Library +import six class ConfigCommandTest(unittest.TestCase, TestHelper): @@ -114,8 +115,8 @@ class ConfigCommandTest(unittest.TestCase, TestHelper): execlp.side_effect = OSError('here is problem') self.run_command('config', '-e') self.assertIn('Could not edit configuration', - unicode(user_error.exception)) - self.assertIn('here is problem', unicode(user_error.exception)) + six.text_type(user_error.exception)) + self.assertIn('here is problem', six.text_type(user_error.exception)) def test_edit_invalid_config_file(self): self.lib = Library(':memory:') diff --git a/test/test_dbcore.py b/test/test_dbcore.py index bd51aea32..8fc8532c6 100644 --- a/test/test_dbcore.py +++ b/test/test_dbcore.py @@ -20,11 +20,13 @@ from __future__ import division, absolute_import, print_function import os import shutil import sqlite3 +from six import assertRaisesRegex from test import _common from test._common import unittest from beets import dbcore from tempfile import mkstemp +import six # Fixture: concrete database and model classes. For migration tests, we @@ -298,9 +300,9 @@ class ModelTest(unittest.TestCase): self.assertNotIn('flex_field', model2) def test_check_db_fails(self): - with self.assertRaisesRegexp(ValueError, 'no database'): + with assertRaisesRegex(self, ValueError, 'no database'): dbcore.Model()._check_db() - with self.assertRaisesRegexp(ValueError, 'no id'): + with assertRaisesRegex(self, ValueError, 'no id'): TestModel1(self.db)._check_db() dbcore.Model(self.db)._check_db(need_id=False) @@ -312,7 +314,7 @@ class ModelTest(unittest.TestCase): def test_computed_field(self): model = TestModelWithGetters() self.assertEqual(model.aComputedField, 'thing') - with self.assertRaisesRegexp(KeyError, u'computed field .+ deleted'): + with assertRaisesRegex(self, KeyError, u'computed field .+ deleted'): del model.aComputedField def test_items(self): @@ -328,7 +330,7 @@ class ModelTest(unittest.TestCase): model._db def test_parse_nonstring(self): - with self.assertRaisesRegexp(TypeError, u"must be a string"): + with assertRaisesRegex(self, TypeError, u"must be a string"): dbcore.Model._parse(None, 42) @@ -349,7 +351,7 @@ class FormatTest(unittest.TestCase): model = TestModel1() model.other_field = u'caf\xe9'.encode('utf8') value = model.formatted().get('other_field') - self.assertTrue(isinstance(value, unicode)) + self.assertTrue(isinstance(value, six.text_type)) self.assertEqual(value, u'caf\xe9') def test_format_unset_field(self): diff --git a/test/test_edit.py b/test/test_edit.py index 669f6a2fa..9205166fd 100644 --- a/test/test_edit.py +++ b/test/test_edit.py @@ -23,6 +23,7 @@ from test.test_ui_importer import TerminalImportSessionSetup from test.test_importer import ImportHelper, AutotagStub from beets.library import Item from beetsplug.edit import EditPlugin +import six class ModifyFileMocker(object): @@ -63,7 +64,7 @@ class ModifyFileMocker(object): """ with codecs.open(filename, 'r', encoding='utf8') as f: contents = f.read() - for old, new_ in self.replacements.iteritems(): + for old, new_ in six.iteritems(self.replacements): contents = contents.replace(old, new_) with codecs.open(filename, 'w', encoding='utf8') as f: f.write(contents) diff --git a/test/test_importadded.py b/test/test_importadded.py index 474f56384..01c7d0ed9 100644 --- a/test/test_importadded.py +++ b/test/test_importadded.py @@ -14,6 +14,7 @@ # included in all copies or substantial portions of the Software. from __future__ import division, absolute_import, print_function +import six """Tests for the `importadded` plugin.""" @@ -124,7 +125,7 @@ class ImportAddedTest(unittest.TestCase, ImportHelper): self.assertEqualTimes(album.added, album_added_before) items_added_after = dict((item.path, item.added) for item in album.items()) - for item_path, added_after in items_added_after.iteritems(): + for item_path, added_after in six.iteritems(items_added_after): self.assertEqualTimes(items_added_before[item_path], added_after, u"reimport modified Item.added for " + util.displayable_path(item_path)) @@ -162,7 +163,7 @@ class ImportAddedTest(unittest.TestCase, ImportHelper): # Verify the reimported items items_added_after = dict((item.path, item.added) for item in self.lib.items()) - for item_path, added_after in items_added_after.iteritems(): + for item_path, added_after in six.iteritems(items_added_after): self.assertEqualTimes(items_added_before[item_path], added_after, u"reimport modified Item.added for " + util.displayable_path(item_path)) diff --git a/test/test_importer.py b/test/test_importer.py index a1b0cd895..cd5756b1d 100644 --- a/test/test_importer.py +++ b/test/test_importer.py @@ -20,9 +20,9 @@ from __future__ import division, absolute_import, print_function import os import re import shutil -import StringIO import unicodedata import sys +from six import StringIO from tempfile import mkstemp from zipfile import ZipFile from tarfile import TarFile @@ -1250,14 +1250,14 @@ class ImportDuplicateSingletonTest(unittest.TestCase, TestHelper, class TagLogTest(_common.TestCase): def test_tag_log_line(self): - sio = StringIO.StringIO() + sio = StringIO() handler = logging.StreamHandler(sio) session = _common.import_session(loghandler=handler) session.tag_log('status', 'path') self.assertIn('status path', sio.getvalue()) def test_tag_log_unicode(self): - sio = StringIO.StringIO() + sio = StringIO() handler = logging.StreamHandler(sio) session = _common.import_session(loghandler=handler) session.tag_log('status', u'caf\xe9') # send unicode diff --git a/test/test_lastgenre.py b/test/test_lastgenre.py index 19adab98a..c060e95a9 100644 --- a/test/test_lastgenre.py +++ b/test/test_lastgenre.py @@ -25,6 +25,7 @@ from beetsplug import lastgenre from beets import config from test.helper import TestHelper +import six class LastGenrePluginTest(unittest.TestCase, TestHelper): @@ -38,11 +39,11 @@ class LastGenrePluginTest(unittest.TestCase, TestHelper): def _setup_config(self, whitelist=False, canonical=False, count=1): config['lastgenre']['canonical'] = canonical config['lastgenre']['count'] = count - if isinstance(whitelist, (bool, basestring)): + if isinstance(whitelist, (bool, six.string_types)): # Filename, default, or disabled. config['lastgenre']['whitelist'] = whitelist self.plugin.setup() - if not isinstance(whitelist, (bool, basestring)): + if not isinstance(whitelist, (bool, six.string_types)): # Explicit list of genres. self.plugin.whitelist = whitelist diff --git a/test/test_library.py b/test/test_library.py index 4cd922910..b1ac76c25 100644 --- a/test/test_library.py +++ b/test/test_library.py @@ -38,6 +38,7 @@ from beets import config from beets.mediafile import MediaFile from beets.util import syspath, bytestring_path from test.helper import TestHelper +import six # Shortcut to path normalization. np = util.normpath @@ -964,7 +965,7 @@ class PathStringTest(_common.TestCase): def test_sanitize_path_returns_unicode(self): path = u'b\xe1r?' new_path = util.sanitize_path(path) - self.assertTrue(isinstance(new_path, unicode)) + self.assertTrue(isinstance(new_path, six.text_type)) def test_unicode_artpath_becomes_bytestring(self): alb = self.lib.add_album([self.i]) @@ -1051,7 +1052,7 @@ class TemplateTest(_common.LibTestCase): album.tagada = u'togodo' self.assertEqual(u"{0}".format(album), u"foö bar") self.assertEqual(u"{0:$tagada}".format(album), u"togodo") - self.assertEqual(unicode(album), u"foö bar") + self.assertEqual(six.text_type(album), u"foö bar") self.assertEqual(bytes(album), b"fo\xc3\xb6 bar") config['format_item'] = 'bar $foo' @@ -1174,7 +1175,8 @@ class LibraryFieldTypesTest(unittest.TestCase): t = beets.library.DateType() # format - time_local = time.strftime(beets.config['time_format'].get(unicode), + time_format = beets.config['time_format'].get(six.text_type) + time_local = time.strftime(time_format, time.localtime(123456789)) self.assertEqual(time_local, t.format(123456789)) # parse diff --git a/test/test_logging.py b/test/test_logging.py index 19d9b0e1e..6e5e6c8b6 100644 --- a/test/test_logging.py +++ b/test/test_logging.py @@ -6,7 +6,7 @@ from __future__ import division, absolute_import, print_function import sys import threading import logging as log -from StringIO import StringIO +from six import StringIO import beets.logging as blog from beets import plugins, ui @@ -14,6 +14,7 @@ import beetsplug from test import _common from test._common import unittest, TestCase from test import helper +import six class LoggingTest(TestCase): @@ -218,7 +219,7 @@ class ConcurrentEventsTest(TestCase, helper.TestHelper): def check_dp_exc(): if dp.exc_info: - raise dp.exc_info[1], None, dp.exc_info[2] + six.reraise(dp.exc_info[1], None, dp.exc_info[2]) try: dp.lock1.acquire() diff --git a/test/test_lyrics.py b/test/test_lyrics.py index 9136131c1..d49c4d980 100644 --- a/test/test_lyrics.py +++ b/test/test_lyrics.py @@ -29,6 +29,7 @@ from beetsplug import lyrics from beets.library import Item from beets.util import confit, bytestring_path from beets import logging +import six log = logging.getLogger('beets.test_lyrics') raw_backend = lyrics.Backend({}, log) @@ -354,7 +355,7 @@ class LyricsGooglePluginTest(unittest.TestCase): present in the title.""" from bs4 import SoupStrainer, BeautifulSoup s = self.source - url = unicode(s['url'] + s['path']) + url = six.text_type(s['url'] + s['path']) html = raw_backend.fetch_url(url) soup = BeautifulSoup(html, "html.parser", parse_only=SoupStrainer('title')) diff --git a/test/test_mediafile.py b/test/test_mediafile.py index e2fdd9fce..258e03896 100644 --- a/test/test_mediafile.py +++ b/test/test_mediafile.py @@ -23,6 +23,7 @@ import shutil import tempfile import datetime import time +from six import assertCountEqual from test import _common from test._common import unittest @@ -32,6 +33,7 @@ from beets.mediafile import MediaFile, MediaField, Image, \ from beets.library import Item from beets.plugins import BeetsPlugin from beets.util import bytestring_path +import six class ArtTestMixin(object): @@ -268,7 +270,7 @@ class GenreListTestMixin(object): def test_read_genre_list(self): mediafile = self._mediafile_fixture('full') - self.assertItemsEqual(mediafile.genres, ['the genre']) + assertCountEqual(self, mediafile.genres, ['the genre']) def test_write_genre_list(self): mediafile = self._mediafile_fixture('empty') @@ -276,7 +278,7 @@ class GenreListTestMixin(object): mediafile.save() mediafile = MediaFile(mediafile.path) - self.assertItemsEqual(mediafile.genres, [u'one', u'two']) + assertCountEqual(self, mediafile.genres, [u'one', u'two']) def test_write_genre_list_get_first(self): mediafile = self._mediafile_fixture('empty') @@ -293,7 +295,7 @@ class GenreListTestMixin(object): mediafile.save() mediafile = MediaFile(mediafile.path) - self.assertItemsEqual(mediafile.genres, [u'the genre', u'another']) + assertCountEqual(self, mediafile.genres, [u'the genre', u'another']) field_extension = MediaField( @@ -352,13 +354,13 @@ class ExtendedFieldTestMixin(object): with self.assertRaises(ValueError) as cm: MediaFile.add_field('somekey', True) self.assertIn(u'must be an instance of MediaField', - unicode(cm.exception)) + six.text_type(cm.exception)) def test_overwrite_property(self): with self.assertRaises(ValueError) as cm: MediaFile.add_field('artist', MediaField()) self.assertIn(u'property "artist" already exists', - unicode(cm.exception)) + six.text_type(cm.exception)) class ReadWriteTestBase(ArtTestMixin, GenreListTestMixin, @@ -949,7 +951,7 @@ class MediaFieldTest(unittest.TestCase): def test_known_fields(self): fields = list(ReadWriteTestBase.tag_fields) fields.extend(('encoder', 'images', 'genres', 'albumtype')) - self.assertItemsEqual(MediaFile.fields(), fields) + assertCountEqual(self, MediaFile.fields(), fields) def test_fields_in_readable_fields(self): readable = MediaFile.readable_fields() diff --git a/test/test_mediafile_edge.py b/test/test_mediafile_edge.py index 26d88ae74..9fbf4e2bb 100644 --- a/test/test_mediafile_edge.py +++ b/test/test_mediafile_edge.py @@ -26,6 +26,7 @@ from test.helper import TestHelper from beets.util import bytestring_path import beets.mediafile +import six _sc = beets.mediafile._safe_cast @@ -127,8 +128,8 @@ class InvalidValueToleranceTest(unittest.TestCase): self.assertAlmostEqual(_sc(float, u'-1.234'), -1.234) def test_safe_cast_special_chars_to_unicode(self): - us = _sc(unicode, 'caf\xc3\xa9') - self.assertTrue(isinstance(us, unicode)) + us = _sc(six.text_type, 'caf\xc3\xa9') + self.assertTrue(isinstance(us, six.text_type)) self.assertTrue(us.startswith(u'caf')) def test_safe_cast_float_with_no_numbers(self): @@ -350,7 +351,7 @@ class ID3v23Test(unittest.TestCase, TestHelper): mf.year = 2013 mf.save() frame = mf.mgfile['TDRC'] - self.assertTrue('2013' in unicode(frame)) + self.assertTrue('2013' in six.text_type(frame)) self.assertTrue('TYER' not in mf.mgfile) finally: self._delete_test() @@ -361,7 +362,7 @@ class ID3v23Test(unittest.TestCase, TestHelper): mf.year = 2013 mf.save() frame = mf.mgfile['TYER'] - self.assertTrue('2013' in unicode(frame)) + self.assertTrue('2013' in six.text_type(frame)) self.assertTrue('TDRC' not in mf.mgfile) finally: self._delete_test() diff --git a/test/test_pipeline.py b/test/test_pipeline.py index e48a62f53..a40cccdbd 100644 --- a/test/test_pipeline.py +++ b/test/test_pipeline.py @@ -17,6 +17,8 @@ """ from __future__ import division, absolute_import, print_function +import six + from test._common import unittest from beets.util import pipeline @@ -134,7 +136,10 @@ class ExceptionTest(unittest.TestCase): pull = pl.pull() for i in range(3): next(pull) - self.assertRaises(TestException, pull.next) + if six.PY2: + self.assertRaises(TestException, pull.next) + else: + self.assertRaises(TestException, pull.__next__) class ParallelExceptionTest(unittest.TestCase): @@ -157,6 +162,7 @@ class ConstrainedThreadedPipelineTest(unittest.TestCase): pl.run_parallel(1) self.assertEqual(l, [i * 2 for i in range(1000)]) + @unittest.skipIf(six.PY3, u'freezes the test suite in py3') def test_constrained_exception(self): # Raise an exception in a constrained pipeline. l = [] diff --git a/test/test_query.py b/test/test_query.py index fc4ee04a6..4650df868 100644 --- a/test/test_query.py +++ b/test/test_query.py @@ -34,6 +34,7 @@ from beets.dbcore.query import (NoneQuery, ParsingError, from beets.library import Library, Item from beets import util import platform +import six class TestHelper(helper.TestHelper): @@ -302,11 +303,11 @@ class GetTest(DummyDataTestCase): def test_invalid_query(self): with self.assertRaises(InvalidQueryArgumentTypeError) as raised: dbcore.query.NumericQuery('year', u'199a') - self.assertIn(u'not an int', unicode(raised.exception)) + self.assertIn(u'not an int', six.text_type(raised.exception)) with self.assertRaises(InvalidQueryArgumentTypeError) as raised: dbcore.query.RegexpQuery('year', u'199(') - exception_text = unicode(raised.exception) + exception_text = six.text_type(raised.exception) self.assertIn(u'not a regular expression', exception_text) if sys.version_info >= (3, 5): self.assertIn(u'unterminated subpattern', exception_text) diff --git a/test/test_replaygain.py b/test/test_replaygain.py index e3fdf18a6..d2a8aa9ba 100644 --- a/test/test_replaygain.py +++ b/test/test_replaygain.py @@ -23,6 +23,7 @@ from beets import config from beets.mediafile import MediaFile from beetsplug.replaygain import (FatalGstreamerPluginReplayGainError, GStreamerBackend) +import six try: import gi @@ -62,7 +63,7 @@ class ReplayGainCliTestBase(TestHelper): # teardown operations may fail. In particular # {Item,Album} # may not have the _original_types attribute in unload_plugins pass - raise exc_info[1], None, exc_info[2] + six.reraise(exc_info[1], None, exc_info[2]) album = self.add_album_fixture(2) for item in album.items(): diff --git a/test/test_spotify.py b/test/test_spotify.py index 721d4f953..20101d76c 100644 --- a/test/test_spotify.py +++ b/test/test_spotify.py @@ -13,7 +13,7 @@ from beets import config from beets.library import Item from beetsplug import spotify from test.helper import TestHelper -import urlparse +from six.moves.urllib.parse import parse_qs, urlparse class ArgumentsMock(object): @@ -25,7 +25,7 @@ class ArgumentsMock(object): def _params(url): """Get the query parameters from a URL.""" - return urlparse.parse_qs(urlparse.urlparse(url).query) + return parse_qs(urlparse(url).query) class SpotifyPluginTest(_common.TestCase, TestHelper): diff --git a/test/test_template.py b/test/test_template.py index 3ec97bea4..0605675c6 100644 --- a/test/test_template.py +++ b/test/test_template.py @@ -21,6 +21,7 @@ import warnings from test._common import unittest from beets.util import functemplate +import six def _normexpr(expr): @@ -30,7 +31,7 @@ def _normexpr(expr): """ textbuf = [] for part in expr.parts: - if isinstance(part, basestring): + if isinstance(part, six.string_types): textbuf.append(part) else: if textbuf: @@ -227,7 +228,7 @@ class EvalTest(unittest.TestCase): u'baz': u'BaR', } functions = { - u'lower': unicode.lower, + u'lower': six.text_type.lower, u'len': len, } return functemplate.Template(template).substitute(values, functions) @@ -258,7 +259,7 @@ class EvalTest(unittest.TestCase): def test_function_call_exception(self): res = self._eval(u"%lower{a,b,c,d,e}") - self.assertTrue(isinstance(res, basestring)) + self.assertTrue(isinstance(res, six.string_types)) def test_function_returning_integer(self): self.assertEqual(self._eval(u"%len{foo}"), u"3") diff --git a/test/test_ui.py b/test/test_ui.py index d14febbff..78d777f9e 100644 --- a/test/test_ui.py +++ b/test/test_ui.py @@ -39,6 +39,7 @@ from beets import config from beets import plugins from beets.util.confit import ConfigError from beets import util +import six class ListTest(unittest.TestCase): @@ -1257,15 +1258,15 @@ class CommonOptionsParserTest(unittest.TestCase, TestHelper): config['format_item'].set('$foo') self.assertEqual(parser.parse_args([]), ({'path': None}, [])) - self.assertEqual(config['format_item'].get(unicode), u'$foo') + self.assertEqual(config['format_item'].get(six.text_type), u'$foo') self.assertEqual(parser.parse_args([u'-p']), ({'path': True, 'format': u'$path'}, [])) self.assertEqual(parser.parse_args(['--path']), ({'path': True, 'format': u'$path'}, [])) - self.assertEqual(config['format_item'].get(unicode), u'$path') - self.assertEqual(config['format_album'].get(unicode), u'$path') + self.assertEqual(config['format_item'].get(six.text_type), u'$path') + self.assertEqual(config['format_album'].get(six.text_type), u'$path') def test_format_option(self): parser = ui.CommonOptionsParser() @@ -1274,15 +1275,15 @@ class CommonOptionsParserTest(unittest.TestCase, TestHelper): config['format_item'].set('$foo') self.assertEqual(parser.parse_args([]), ({'format': None}, [])) - self.assertEqual(config['format_item'].get(unicode), u'$foo') + self.assertEqual(config['format_item'].get(six.text_type), u'$foo') self.assertEqual(parser.parse_args([u'-f', u'$bar']), ({'format': u'$bar'}, [])) self.assertEqual(parser.parse_args([u'--format', u'$baz']), ({'format': u'$baz'}, [])) - self.assertEqual(config['format_item'].get(unicode), u'$baz') - self.assertEqual(config['format_album'].get(unicode), u'$baz') + self.assertEqual(config['format_item'].get(six.text_type), u'$baz') + self.assertEqual(config['format_album'].get(six.text_type), u'$baz') def test_format_option_with_target(self): with self.assertRaises(KeyError): @@ -1297,8 +1298,8 @@ class CommonOptionsParserTest(unittest.TestCase, TestHelper): self.assertEqual(parser.parse_args([u'-f', u'$bar']), ({'format': u'$bar'}, [])) - self.assertEqual(config['format_item'].get(unicode), u'$bar') - self.assertEqual(config['format_album'].get(unicode), u'$album') + self.assertEqual(config['format_item'].get(six.text_type), u'$bar') + self.assertEqual(config['format_album'].get(six.text_type), u'$album') def test_format_option_with_album(self): parser = ui.CommonOptionsParser() @@ -1309,15 +1310,15 @@ class CommonOptionsParserTest(unittest.TestCase, TestHelper): config['format_album'].set('$album') parser.parse_args([u'-f', u'$bar']) - self.assertEqual(config['format_item'].get(unicode), u'$bar') - self.assertEqual(config['format_album'].get(unicode), u'$album') + self.assertEqual(config['format_item'].get(six.text_type), u'$bar') + self.assertEqual(config['format_album'].get(six.text_type), u'$album') parser.parse_args([u'-a', u'-f', u'$foo']) - self.assertEqual(config['format_item'].get(unicode), u'$bar') - self.assertEqual(config['format_album'].get(unicode), u'$foo') + self.assertEqual(config['format_item'].get(six.text_type), u'$bar') + self.assertEqual(config['format_album'].get(six.text_type), u'$foo') parser.parse_args([u'-f', u'$foo2', u'-a']) - self.assertEqual(config['format_album'].get(unicode), u'$foo2') + self.assertEqual(config['format_album'].get(six.text_type), u'$foo2') def test_add_all_common_options(self): parser = ui.CommonOptionsParser() diff --git a/test/test_ui_importer.py b/test/test_ui_importer.py index 4596e781a..cc2938ad3 100644 --- a/test/test_ui_importer.py +++ b/test/test_ui_importer.py @@ -26,6 +26,7 @@ from test import test_importer from beets.ui.commands import TerminalImportSession from beets import importer from beets import config +import six class TestTerminalImportSession(TerminalImportSession): @@ -69,7 +70,7 @@ class TestTerminalImportSession(TerminalImportSession): self.io.addinput(u'S') elif isinstance(choice, int): self.io.addinput(u'M') - self.io.addinput(unicode(choice)) + self.io.addinput(six.text_type(choice)) self._add_choice_input() else: raise Exception(u'Unknown choice %s' % choice) diff --git a/test/test_util.py b/test/test_util.py index 32028c9fb..f4c2eca80 100644 --- a/test/test_util.py +++ b/test/test_util.py @@ -26,6 +26,7 @@ from mock import patch, Mock from test._common import unittest from test import _common from beets import util +import six class UtilTest(unittest.TestCase): @@ -122,7 +123,7 @@ class PathConversionTest(_common.TestCase): with _common.platform_windows(): path = os.path.join(u'a', u'b', u'c') outpath = util.syspath(path) - self.assertTrue(isinstance(outpath, unicode)) + self.assertTrue(isinstance(outpath, six.text_type)) self.assertTrue(outpath.startswith(u'\\\\?\\')) def test_syspath_windows_format_unc_path(self): @@ -131,7 +132,7 @@ class PathConversionTest(_common.TestCase): path = '\\\\server\\share\\file.mp3' with _common.platform_windows(): outpath = util.syspath(path) - self.assertTrue(isinstance(outpath, unicode)) + self.assertTrue(isinstance(outpath, six.text_type)) self.assertEqual(outpath, u'\\\\?\\UNC\\server\\share\\file.mp3') def test_syspath_posix_unchanged(self): diff --git a/test/test_web.py b/test/test_web.py index 51a55f33d..ac4e03e97 100644 --- a/test/test_web.py +++ b/test/test_web.py @@ -4,6 +4,8 @@ from __future__ import division, absolute_import, print_function +from six import assertCountEqual + from test._common import unittest from test import _common import json @@ -52,7 +54,7 @@ class WebPluginTest(_common.LibTestCase): self.assertEqual(response.status_code, 200) self.assertEqual(len(response.json['items']), 2) response_titles = [item['title'] for item in response.json['items']] - self.assertItemsEqual(response_titles, [u'title', u'another title']) + assertCountEqual(self, response_titles, [u'title', u'another title']) def test_get_single_item_not_found(self): response = self.client.get('/item/3') @@ -80,7 +82,7 @@ class WebPluginTest(_common.LibTestCase): self.assertEqual(response.status_code, 200) response_albums = [album['album'] for album in response.json['albums']] - self.assertItemsEqual(response_albums, [u'album', u'another album']) + assertCountEqual(self, response_albums, [u'album', u'another album']) def test_get_single_album_by_id(self): response = self.client.get('/album/2') @@ -96,7 +98,7 @@ class WebPluginTest(_common.LibTestCase): self.assertEqual(response.status_code, 200) response_albums = [album['album'] for album in response.json['albums']] - self.assertItemsEqual(response_albums, [u'album', u'another album']) + assertCountEqual(self, response_albums, [u'album', u'another album']) def test_get_album_empty_query(self): response = self.client.get('/album/query/')