Release v0.19.5

This commit is contained in:
Stein Magnus Jodal 2014-12-24 00:39:06 +01:00
commit e0dd9bcb11
22 changed files with 195 additions and 60 deletions

View File

@ -16,3 +16,4 @@ Janez Troha <janez.troha@gmail.com> <dz0ny@ubuntu.si>
Luke Giuliani <luke@giuliani.com.au> Luke Giuliani <luke@giuliani.com.au>
Colin Montgomerie <kiteflyingmonkey@gmail.com> Colin Montgomerie <kiteflyingmonkey@gmail.com>
Ignasi Fosch <natx@y10k.ws> <ifosch@serenity-2.local> Ignasi Fosch <natx@y10k.ws> <ifosch@serenity-2.local>
Christopher Schirner <christopher@hackerspace-bamberg.de> <schinken@hackerspace-bamberg.de>

View File

@ -42,3 +42,7 @@
- Sam Willcocks <sam@wlcx.cc> - Sam Willcocks <sam@wlcx.cc>
- Ignasi Fosch <natx@y10k.ws> - Ignasi Fosch <natx@y10k.ws>
- Arjun Naik <arjun@arjunnaik.in> - Arjun Naik <arjun@arjunnaik.in>
- Christopher Schirner <christopher@hackerspace-bamberg.de>
- Dmitry Sandalov <dmitry@sandalov.org>
- Deni Bertovic <deni@kset.org>
- Thomas Amland <thomas.amland@gmail.com>

View File

@ -5,6 +5,49 @@ Changelog
This changelog is used to track all major changes to Mopidy. This changelog is used to track all major changes to Mopidy.
v0.19.5 (2014-12-23)
====================
Today is Mopidy's five year anniversary. We're celebrating with a bugfix
release and are looking forward to the next five years!
- Config: Support UTF-8 in extension's default config. If an extension with
non-ASCII characters in its default config was installed, and Mopidy didn't
already have a config file, Mopidy would crashed when trying to create the
initial config file based on the default config of all available extensions.
(Fixes: :discuss:`428`)
- Extensions: Fix crash when unpacking data from
:exc:`pkg_resources.VersionConflict` created with a single argument. (Fixes:
:issue:`911`)
- Models: Hide empty collections from :func:`repr()` representations.
- Models: Field values are no longer stored on the model instance when the
value matches the default value for the field. This makes two models equal
when they have a field which in one case is implicitly set to the default
value and in the other case explicitly set to the default value, but with
otherwise equal fields. (Fixes: :issue:`837`)
- Models: Changed the default value of :attr:`mopidy.models.Album.num_tracks`,
:attr:`mopidy.models.Track.track_no`, and
:attr:`mopidy.models.Track.last_modified` from ``0`` to :class:`None`.
- Core: When skipping to the next track in consume mode, remove the skipped
track from the tracklist. This is consistent with the original MPD server's
behavior. (Fixes: :issue:`902`)
- Local: Fix scanning of modified files. (PR: :issue:`904`)
- MPD: Re-enable browsing of empty directories. (PR: :issue:`906`)
- MPD: Remove track comments from responses. They are not included by the
original MPD server, and this works around :issue:`881`. (PR: :issue:`882`)
- HTTP: Errors while starting HTTP apps are logged instead of crashing the HTTP
server. (Fixes: :issue:`875`)
v0.19.4 (2014-09-01) v0.19.4 (2014-09-01)
==================== ====================

View File

@ -156,6 +156,7 @@ extlinks = {
'commit': ('https://github.com/mopidy/mopidy/commit/%s', 'commit '), 'commit': ('https://github.com/mopidy/mopidy/commit/%s', 'commit '),
'mpris': ( 'mpris': (
'https://github.com/mopidy/mopidy-mpris/issues/%s', 'mopidy-mpris#'), 'https://github.com/mopidy/mopidy-mpris/issues/%s', 'mopidy-mpris#'),
'discuss': ('https://discuss.mopidy.com/t/%s', 'discuss.mopidy.com/t/'),
} }

View File

@ -21,4 +21,4 @@ if (isinstance(pykka.__version__, basestring)
warnings.filterwarnings('ignore', 'could not open display') warnings.filterwarnings('ignore', 'could not open display')
__version__ = '0.19.4' __version__ = '0.19.5'

View File

@ -97,8 +97,12 @@ def format_initial(extensions):
versions = ['Mopidy %s' % versioning.get_version()] versions = ['Mopidy %s' % versioning.get_version()]
for extension in sorted(extensions, key=lambda ext: ext.dist_name): for extension in sorted(extensions, key=lambda ext: ext.dist_name):
versions.append('%s %s' % (extension.dist_name, extension.version)) versions.append('%s %s' % (extension.dist_name, extension.version))
description = _INITIAL_HELP.strip() % {'versions': '\n# '.join(versions)}
return description + '\n\n' + _format(config, {}, schemas, False, True) header = _INITIAL_HELP.strip() % {'versions': '\n# '.join(versions)}
formatted_config = _format(
config=config, comments={}, schemas=schemas,
display=False, disable=True).decode('utf-8')
return header + '\n\n' + formatted_config
def _load(files, defaults, overrides): def _load(files, defaults, overrides):

View File

@ -179,12 +179,16 @@ class PlaybackController(object):
The current playback state will be kept. If it was playing, playing The current playback state will be kept. If it was playing, playing
will continue. If it was paused, it will still be paused, etc. will continue. If it was paused, it will still be paused, etc.
""" """
tl_track = self.core.tracklist.next_track(self.current_tl_track) original_tl_track = self.current_tl_track
if tl_track: next_tl_track = self.core.tracklist.next_track(original_tl_track)
self.change_track(tl_track)
if next_tl_track:
self.change_track(next_tl_track)
else: else:
self.stop(clear_current_track=True) self.stop(clear_current_track=True)
self.core.tracklist.mark_played(original_tl_track)
def pause(self): def pause(self):
"""Pause playback.""" """Pause playback."""
backend = self._get_backend() backend = self._get_backend()

View File

@ -448,10 +448,10 @@ class TracklistController(object):
def mark_played(self, tl_track): def mark_played(self, tl_track):
"""Private method used by :class:`mopidy.core.PlaybackController`.""" """Private method used by :class:`mopidy.core.PlaybackController`."""
if not self.consume: if self.consume and tl_track is not None:
return False self.remove(tlid=[tl_track.tlid])
self.remove(tlid=[tl_track.tlid]) return True
return True return False
def _trigger_tracklist_changed(self): def _trigger_tracklist_changed(self):
if self.random: if self.random:

View File

@ -188,10 +188,13 @@ def validate_extension(extension):
extension.ext_name, ex) extension.ext_name, ex)
return False return False
except pkg_resources.VersionConflict as ex: except pkg_resources.VersionConflict as ex:
found, required = ex.args if len(ex.args) == 2:
logger.info( found, required = ex.args
'Disabled extension %s: %s required, but found %s at %s', logger.info(
extension.ext_name, required, found, found.location) 'Disabled extension %s: %s required, but found %s at %s',
extension.ext_name, required, found, found.location)
else:
logger.info('Disabled extension %s: %s', extension.ext_name, ex)
return False return False
try: try:

View File

@ -129,11 +129,16 @@ class HttpServer(threading.Thread):
def _get_app_request_handlers(self): def _get_app_request_handlers(self):
result = [] result = []
for app in self.apps: for app in self.apps:
try:
request_handlers = app['factory'](self.config, self.core)
except Exception:
logger.exception('Loading %s failed.', app['name'])
continue
result.append(( result.append((
r'/%s' % app['name'], r'/%s' % app['name'],
handlers.AddSlashHandler handlers.AddSlashHandler
)) ))
request_handlers = app['factory'](self.config, self.core)
for handler in request_handlers: for handler in request_handlers:
handler = list(handler) handler = list(handler)
handler[0] = '/%s%s' % (app['name'], handler[0]) handler[0] = '/%s%s' % (app['name'], handler[0])

View File

@ -70,7 +70,6 @@ class ScanCommand(commands.Command):
library = _get_library(args, config) library = _get_library(args, config)
uris_in_library = set()
uris_to_update = set() uris_to_update = set()
uris_to_remove = set() uris_to_remove = set()
@ -87,7 +86,7 @@ class ScanCommand(commands.Command):
logger.debug('Missing file %s', track.uri) logger.debug('Missing file %s', track.uri)
uris_to_remove.add(track.uri) uris_to_remove.add(track.uri)
elif mtime > track.last_modified: elif mtime > track.last_modified:
uris_in_library.add(track.uri) uris_to_update.add(track.uri)
logger.info('Removing %d missing tracks.', len(uris_to_remove)) logger.info('Removing %d missing tracks.', len(uris_to_remove))
for uri in uris_to_remove: for uri in uris_to_remove:

View File

@ -18,6 +18,8 @@ class ImmutableObject(object):
raise TypeError( raise TypeError(
'__init__() got an unexpected keyword argument "%s"' % '__init__() got an unexpected keyword argument "%s"' %
key) key)
if value == getattr(self, key):
continue # Don't explicitly set default values
self.__dict__[key] = value self.__dict__[key] = value
def __setattr__(self, name, value): def __setattr__(self, name, value):
@ -29,6 +31,8 @@ class ImmutableObject(object):
kwarg_pairs = [] kwarg_pairs = []
for (key, value) in sorted(self.__dict__.items()): for (key, value) in sorted(self.__dict__.items()):
if isinstance(value, (frozenset, tuple)): if isinstance(value, (frozenset, tuple)):
if not value:
continue
value = list(value) value = list(value)
kwarg_pairs.append('%s=%s' % (key, repr(value))) kwarg_pairs.append('%s=%s' % (key, repr(value)))
return '%(classname)s(%(kwargs)s)' % { return '%(classname)s(%(kwargs)s)' % {
@ -70,13 +74,11 @@ class ImmutableObject(object):
for key in self.__dict__.keys(): for key in self.__dict__.keys():
public_key = key.lstrip('_') public_key = key.lstrip('_')
value = values.pop(public_key, self.__dict__[key]) value = values.pop(public_key, self.__dict__[key])
if value is not None: data[public_key] = value
data[public_key] = value
for key in values.keys(): for key in values.keys():
if hasattr(self, key): if hasattr(self, key):
value = values.pop(key) value = values.pop(key)
if value is not None: data[key] = value
data[key] = value
if values: if values:
raise TypeError( raise TypeError(
'copy() got an unexpected keyword argument "%s"' % key) 'copy() got an unexpected keyword argument "%s"' % key)
@ -239,7 +241,7 @@ class Album(ImmutableObject):
:param artists: album artists :param artists: album artists
:type artists: list of :class:`Artist` :type artists: list of :class:`Artist`
:param num_tracks: number of tracks in album :param num_tracks: number of tracks in album
:type num_tracks: integer :type num_tracks: integer or :class:`None` if unknown
:param num_discs: number of discs in album :param num_discs: number of discs in album
:type num_discs: integer or :class:`None` if unknown :type num_discs: integer or :class:`None` if unknown
:param date: album release date (YYYY or YYYY-MM-DD) :param date: album release date (YYYY or YYYY-MM-DD)
@ -260,7 +262,7 @@ class Album(ImmutableObject):
artists = frozenset() artists = frozenset()
#: The number of tracks in the album. Read-only. #: The number of tracks in the album. Read-only.
num_tracks = 0 num_tracks = None
#: The number of discs in the album. Read-only. #: The number of discs in the album. Read-only.
num_discs = None num_discs = None
@ -300,7 +302,7 @@ class Track(ImmutableObject):
:param genre: track genre :param genre: track genre
:type genre: string :type genre: string
:param track_no: track number in album :param track_no: track number in album
:type track_no: integer :type track_no: integer or :class:`None` if unknown
:param disc_no: disc number in album :param disc_no: disc number in album
:type disc_no: integer or :class:`None` if unknown :type disc_no: integer or :class:`None` if unknown
:param date: track release date (YYYY or YYYY-MM-DD) :param date: track release date (YYYY or YYYY-MM-DD)
@ -314,7 +316,7 @@ class Track(ImmutableObject):
:param musicbrainz_id: MusicBrainz ID :param musicbrainz_id: MusicBrainz ID
:type musicbrainz_id: string :type musicbrainz_id: string
:param last_modified: Represents last modification time :param last_modified: Represents last modification time
:type last_modified: integer :type last_modified: integer or :class:`None` if unknown
""" """
#: The track URI. Read-only. #: The track URI. Read-only.
@ -339,7 +341,7 @@ class Track(ImmutableObject):
genre = None genre = None
#: The track number in the album. Read-only. #: The track number in the album. Read-only.
track_no = 0 track_no = None
#: The disc number in the album. Read-only. #: The disc number in the album. Read-only.
disc_no = None disc_no = None
@ -362,7 +364,7 @@ class Track(ImmutableObject):
#: Integer representing when the track was last modified, exact meaning #: Integer representing when the track was last modified, exact meaning
#: depends on source of track. For local files this is the mtime, for other #: depends on source of track. For local files this is the mtime, for other
#: backends it could be a timestamp or simply a version counter. #: backends it could be a timestamp or simply a version counter.
last_modified = 0 last_modified = None
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
get = lambda key: frozenset(kwargs.pop(key, None) or []) get = lambda key: frozenset(kwargs.pop(key, None) or [])

View File

@ -421,8 +421,6 @@ def lsinfo(context, uri=None):
if uri in (None, '', '/'): if uri in (None, '', '/'):
result.extend(protocol.stored_playlists.listplaylists(context)) result.extend(protocol.stored_playlists.listplaylists(context))
if not result:
raise exceptions.MpdNoExistError('Not found')
return result return result

View File

@ -44,11 +44,11 @@ def track_to_mpd_format(track, position=None):
if track.date: if track.date:
result.append(('Date', track.date)) result.append(('Date', track.date))
if track.album is not None and track.album.num_tracks != 0: if track.album is not None and track.album.num_tracks is not None:
result.append(('Track', '%d/%d' % ( result.append(('Track', '%d/%d' % (
track.track_no, track.album.num_tracks))) track.track_no or 0, track.album.num_tracks)))
else: else:
result.append(('Track', track.track_no)) result.append(('Track', track.track_no or 0))
if position is not None and tlid is not None: if position is not None and tlid is not None:
result.append(('Pos', position)) result.append(('Pos', position))
result.append(('Id', tlid)) result.append(('Id', tlid))
@ -81,9 +81,6 @@ def track_to_mpd_format(track, position=None):
if track.disc_no: if track.disc_no:
result.append(('Disc', track.disc_no)) result.append(('Disc', track.disc_no))
if track.comment:
result.append(('Comment', track.comment))
if track.musicbrainz_id is not None: if track.musicbrainz_id is not None:
result.append(('MUSICBRAINZ_TRACKID', track.musicbrainz_id)) result.append(('MUSICBRAINZ_TRACKID', track.musicbrainz_id))
return result return result

View File

@ -44,12 +44,14 @@ def get_or_create_file(file_path, mkdir=True, content=None):
if not isinstance(file_path, bytes): if not isinstance(file_path, bytes):
raise ValueError('Path is not a bytestring.') raise ValueError('Path is not a bytestring.')
file_path = expand_path(file_path) file_path = expand_path(file_path)
if isinstance(content, unicode):
content = content.encode('utf-8')
if mkdir: if mkdir:
get_or_create_dir(os.path.dirname(file_path)) get_or_create_dir(os.path.dirname(file_path))
if not os.path.isfile(file_path): if not os.path.isfile(file_path):
logger.info('Creating file %s', file_path) logger.info('Creating file %s', file_path)
with open(file_path, 'w') as fh: with open(file_path, 'wb') as fh:
if content: if content is not None:
fh.write(content) fh.write(content)
return file_path return file_path

View File

@ -239,6 +239,23 @@ class CorePlaybackTest(unittest.TestCase):
# TODO Test next() more # TODO Test next() more
def test_next_keeps_finished_track_in_tracklist(self):
tl_track = self.tl_tracks[0]
self.core.playback.play(tl_track)
self.core.playback.next()
self.assertIn(tl_track, self.core.tracklist.tl_tracks)
def test_next_in_consume_mode_removes_finished_track(self):
tl_track = self.tl_tracks[0]
self.core.playback.play(tl_track)
self.core.tracklist.consume = True
self.core.playback.next()
self.assertNotIn(tl_track, self.core.tracklist.tl_tracks)
@mock.patch( @mock.patch(
'mopidy.core.playback.listener.CoreListener', spec=core.CoreListener) 'mopidy.core.playback.listener.CoreListener', spec=core.CoreListener)
def test_next_emits_events(self, listener_mock): def test_next_emits_events(self, listener_mock):
@ -265,6 +282,23 @@ class CorePlaybackTest(unittest.TestCase):
# TODO Test previous() more # TODO Test previous() more
def test_previous_keeps_finished_track_in_tracklist(self):
tl_track = self.tl_tracks[1]
self.core.playback.play(tl_track)
self.core.playback.previous()
self.assertIn(tl_track, self.core.tracklist.tl_tracks)
def test_previous_keeps_finished_track_even_in_consume_mode(self):
tl_track = self.tl_tracks[1]
self.core.playback.play(tl_track)
self.core.tracklist.consume = True
self.core.playback.previous()
self.assertIn(tl_track, self.core.tracklist.tl_tracks)
@mock.patch( @mock.patch(
'mopidy.core.playback.listener.CoreListener', spec=core.CoreListener) 'mopidy.core.playback.listener.CoreListener', spec=core.CoreListener)
def test_previous_emits_events(self, listener_mock): def test_previous_emits_events(self, listener_mock):
@ -291,6 +325,23 @@ class CorePlaybackTest(unittest.TestCase):
# TODO Test on_end_of_track() more # TODO Test on_end_of_track() more
def test_on_end_of_track_keeps_finished_track_in_tracklist(self):
tl_track = self.tl_tracks[0]
self.core.playback.play(tl_track)
self.core.playback.on_end_of_track()
self.assertIn(tl_track, self.core.tracklist.tl_tracks)
def test_on_end_of_track_in_consume_mode_removes_finished_track(self):
tl_track = self.tl_tracks[0]
self.core.playback.play(tl_track)
self.core.tracklist.consume = True
self.core.playback.on_end_of_track()
self.assertNotIn(tl_track, self.core.tracklist.tl_tracks)
@mock.patch( @mock.patch(
'mopidy.core.playback.listener.CoreListener', spec=core.CoreListener) 'mopidy.core.playback.listener.CoreListener', spec=core.CoreListener)
def test_on_end_of_track_emits_events(self, listener_mock): def test_on_end_of_track_emits_events(self, listener_mock):

View File

@ -347,7 +347,7 @@ class LocalPlaybackProviderTest(unittest.TestCase):
self.tracklist.consume = True self.tracklist.consume = True
self.playback.play() self.playback.play()
self.playback.next() self.playback.next()
self.assertIn(self.tracks[0], self.tracklist.tracks) self.assertNotIn(self.tracks[0], self.tracklist.tracks)
@populate_tracklist @populate_tracklist
def test_next_with_single_and_repeat(self): def test_next_with_single_and_repeat(self):

View File

@ -347,6 +347,13 @@ class MusicDatabaseHandlerTest(protocol.BaseTestCase):
self.assertInResponse('directory: dummy/foo') self.assertInResponse('directory: dummy/foo')
self.assertInResponse('OK') self.assertInResponse('OK')
def test_lsinfo_for_empty_dir_returns_nothing(self):
self.backend.library.dummy_browse_result = {
'dummy:/': []}
self.sendRequest('lsinfo "/dummy"')
self.assertInResponse('OK')
def test_lsinfo_for_dir_does_not_recurse(self): def test_lsinfo_for_dir_does_not_recurse(self):
self.backend.library.dummy_library = [ self.backend.library.dummy_library = [
Track(uri='dummy:/a', name='a'), Track(uri='dummy:/a', name='a'),

View File

@ -73,10 +73,10 @@ class TrackMpdFormatTest(unittest.TestCase):
self.assertIn(('Track', '7/13'), result) self.assertIn(('Track', '7/13'), result)
self.assertIn(('Date', datetime.date(1977, 1, 1)), result) self.assertIn(('Date', datetime.date(1977, 1, 1)), result)
self.assertIn(('Disc', '1'), result) self.assertIn(('Disc', '1'), result)
self.assertIn(('Comment', 'a comment'), result)
self.assertIn(('Pos', 9), result) self.assertIn(('Pos', 9), result)
self.assertIn(('Id', 122), result) self.assertIn(('Id', 122), result)
self.assertEqual(len(result), 15) self.assertNotIn(('Comment', 'a comment'), result)
self.assertEqual(len(result), 14)
def test_track_to_mpd_format_musicbrainz_trackid(self): def test_track_to_mpd_format_musicbrainz_trackid(self):
track = self.track.copy(musicbrainz_id='foo') track = self.track.copy(musicbrainz_id='foo')

View File

@ -171,8 +171,8 @@ class ArtistTest(unittest.TestCase):
def test_serialize_falsy_values(self): def test_serialize_falsy_values(self):
self.assertDictEqual( self.assertDictEqual(
{'__model__': 'Artist', 'uri': '', 'name': None}, {'__model__': 'Artist', 'uri': '', 'name': ''},
Artist(uri='', name=None).serialize()) Artist(uri='', name='').serialize())
def test_to_json_and_back(self): def test_to_json_and_back(self):
artist1 = Artist(uri='uri', name='name') artist1 = Artist(uri='uri', name='name')
@ -318,13 +318,12 @@ class AlbumTest(unittest.TestCase):
def test_repr_without_artists(self): def test_repr_without_artists(self):
self.assertEquals( self.assertEquals(
"Album(artists=[], images=[], name=u'name', uri=u'uri')", "Album(name=u'name', uri=u'uri')",
repr(Album(uri='uri', name='name'))) repr(Album(uri='uri', name='name')))
def test_repr_with_artists(self): def test_repr_with_artists(self):
self.assertEquals( self.assertEquals(
"Album(artists=[Artist(name=u'foo')], images=[], name=u'name', " "Album(artists=[Artist(name=u'foo')], name=u'name', uri=u'uri')",
"uri=u'uri')",
repr(Album(uri='uri', name='name', artists=[Artist(name='foo')]))) repr(Album(uri='uri', name='name', artists=[Artist(name='foo')])))
def test_serialize_without_artists(self): def test_serialize_without_artists(self):
@ -551,14 +550,12 @@ class TrackTest(unittest.TestCase):
def test_repr_without_artists(self): def test_repr_without_artists(self):
self.assertEquals( self.assertEquals(
"Track(artists=[], composers=[], name=u'name', " "Track(name=u'name', uri=u'uri')",
"performers=[], uri=u'uri')",
repr(Track(uri='uri', name='name'))) repr(Track(uri='uri', name='name')))
def test_repr_with_artists(self): def test_repr_with_artists(self):
self.assertEquals( self.assertEquals(
"Track(artists=[Artist(name=u'foo')], composers=[], name=u'name', " "Track(artists=[Artist(name=u'foo')], name=u'name', uri=u'uri')",
"performers=[], uri=u'uri')",
repr(Track(uri='uri', name='name', artists=[Artist(name='foo')]))) repr(Track(uri='uri', name='name', artists=[Artist(name='foo')])))
def test_serialize_without_artists(self): def test_serialize_without_artists(self):
@ -738,6 +735,18 @@ class TrackTest(unittest.TestCase):
self.assertNotEqual(track1, track2) self.assertNotEqual(track1, track2)
self.assertNotEqual(hash(track1), hash(track2)) self.assertNotEqual(hash(track1), hash(track2))
def test_ignores_values_with_default_value_none(self):
track1 = Track(name='name1')
track2 = Track(name='name1', album=None)
self.assertEqual(track1, track2)
self.assertEqual(hash(track1), hash(track2))
def test_copy_can_reset_to_default_value(self):
track1 = Track(name='name1')
track2 = Track(name='name1', album=Album()).copy(album=None)
self.assertEqual(track1, track2)
self.assertEqual(hash(track1), hash(track2))
class TlTrackTest(unittest.TestCase): class TlTrackTest(unittest.TestCase):
def test_tlid(self): def test_tlid(self):
@ -773,8 +782,7 @@ class TlTrackTest(unittest.TestCase):
def test_repr(self): def test_repr(self):
self.assertEquals( self.assertEquals(
"TlTrack(tlid=123, track=Track(artists=[], composers=[], " "TlTrack(tlid=123, track=Track(uri=u'uri'))",
"performers=[], uri=u'uri'))",
repr(TlTrack(tlid=123, track=Track(uri='uri')))) repr(TlTrack(tlid=123, track=Track(uri='uri'))))
def test_serialize(self): def test_serialize(self):
@ -903,13 +911,12 @@ class PlaylistTest(unittest.TestCase):
def test_repr_without_tracks(self): def test_repr_without_tracks(self):
self.assertEquals( self.assertEquals(
"Playlist(name=u'name', tracks=[], uri=u'uri')", "Playlist(name=u'name', uri=u'uri')",
repr(Playlist(uri='uri', name='name'))) repr(Playlist(uri='uri', name='name')))
def test_repr_with_tracks(self): def test_repr_with_tracks(self):
self.assertEquals( self.assertEquals(
"Playlist(name=u'name', tracks=[Track(artists=[], composers=[], " "Playlist(name=u'name', tracks=[Track(name=u'foo')], uri=u'uri')",
"name=u'foo', performers=[])], uri=u'uri')",
repr(Playlist(uri='uri', name='name', tracks=[Track(name='foo')]))) repr(Playlist(uri='uri', name='name', tracks=[Track(name='foo')])))
def test_serialize_without_tracks(self): def test_serialize_without_tracks(self):
@ -1036,7 +1043,7 @@ class SearchResultTest(unittest.TestCase):
def test_repr_without_results(self): def test_repr_without_results(self):
self.assertEquals( self.assertEquals(
"SearchResult(albums=[], artists=[], tracks=[], uri=u'uri')", "SearchResult(uri=u'uri')",
repr(SearchResult(uri='uri'))) repr(SearchResult(uri='uri')))
def test_serialize_without_results(self): def test_serialize_without_results(self):

View File

@ -50,5 +50,6 @@ class VersionTest(unittest.TestCase):
self.assertLess(SV('0.19.0'), SV('0.19.1')) self.assertLess(SV('0.19.0'), SV('0.19.1'))
self.assertLess(SV('0.19.1'), SV('0.19.2')) self.assertLess(SV('0.19.1'), SV('0.19.2'))
self.assertLess(SV('0.19.2'), SV('0.19.3')) self.assertLess(SV('0.19.2'), SV('0.19.3'))
self.assertLess(SV('0.19.3'), SV(__version__)) self.assertLess(SV('0.19.3'), SV('0.19.4'))
self.assertLess(SV(__version__), SV('0.19.5')) self.assertLess(SV('0.19.4'), SV(__version__))
self.assertLess(SV(__version__), SV('0.19.6'))

View File

@ -105,12 +105,12 @@ class GetOrCreateFileTest(unittest.TestCase):
with self.assertRaises(IOError): with self.assertRaises(IOError):
path.get_or_create_file(conflicting_dir) path.get_or_create_file(conflicting_dir)
def test_create_dir_with_unicode(self): def test_create_dir_with_unicode_filename_throws_value_error(self):
with self.assertRaises(ValueError): with self.assertRaises(ValueError):
file_path = unicode(os.path.join(self.parent, b'test')) file_path = unicode(os.path.join(self.parent, b'test'))
path.get_or_create_file(file_path) path.get_or_create_file(file_path)
def test_create_file_with_none(self): def test_create_file_with_none_filename_throws_value_error(self):
with self.assertRaises(ValueError): with self.assertRaises(ValueError):
path.get_or_create_file(None) path.get_or_create_file(None)
@ -119,12 +119,18 @@ class GetOrCreateFileTest(unittest.TestCase):
with self.assertRaises(IOError): with self.assertRaises(IOError):
path.get_or_create_file(file_path, mkdir=False) path.get_or_create_file(file_path, mkdir=False)
def test_create_dir_with_default_content(self): def test_create_dir_with_bytes_content(self):
file_path = os.path.join(self.parent, b'test') file_path = os.path.join(self.parent, b'test')
created = path.get_or_create_file(file_path, content=b'foobar') created = path.get_or_create_file(file_path, content=b'foobar')
with open(created) as fh: with open(created) as fh:
self.assertEqual(fh.read(), b'foobar') self.assertEqual(fh.read(), b'foobar')
def test_create_dir_with_unicode_content(self):
file_path = os.path.join(self.parent, b'test')
created = path.get_or_create_file(file_path, content='foobaræøå')
with open(created) as fh:
self.assertEqual(fh.read(), b'foobaræøå')
class PathToFileURITest(unittest.TestCase): class PathToFileURITest(unittest.TestCase):
def test_simple_path(self): def test_simple_path(self):