diff --git a/beets/config_default.yaml b/beets/config_default.yaml index b98ffbfc6..e0b942d82 100644 --- a/beets/config_default.yaml +++ b/beets/config_default.yaml @@ -61,8 +61,8 @@ ui: action_default: turquoise action: blue -list_format_item: $artist - $album - $title -list_format_album: $albumartist - $album +format_item: $artist - $album - $title +format_album: $albumartist - $album time_format: '%Y-%m-%d %H:%M:%S' sort_album: albumartist+ album+ diff --git a/beets/library.py b/beets/library.py index 0dd3acaf1..139cdfec0 100644 --- a/beets/library.py +++ b/beets/library.py @@ -444,7 +444,7 @@ class Item(LibModel): _sorts = {'artist': SmartArtistSort} - _format_config_key = 'list_format_item' + _format_config_key = 'format_item' @classmethod def _getters(cls): @@ -877,7 +877,7 @@ class Album(LibModel): """List of keys that are set on an album's items. """ - _format_config_key = 'list_format_album' + _format_config_key = 'format_album' @classmethod def _getters(cls): diff --git a/beets/ui/__init__.py b/beets/ui/__init__.py index e041db95e..335129af2 100644 --- a/beets/ui/__init__.py +++ b/beets/ui/__init__.py @@ -592,6 +592,119 @@ def show_model_changes(new, old=None, fields=None, always=False): return bool(changes) +class CommonOptionsParser(optparse.OptionParser, object): + """Offers a simple way to add common formatting options. + + Options available include: + - matching albums instead of tracks: add_album_option() + - showing paths instead of items/albums: add_path_option() + - changing the format of displayed items/albums: add_format_option() + + The last one can have several behaviors: + - against a special target + - with a certain format + - autodetected target with the album option + + Each method is fully documented in the related method. + """ + def __init__(self, *args, **kwargs): + super(CommonOptionsParser, self).__init__(*args, **kwargs) + self._album_flags = False + # this serves both as an indicator that we offer the feature AND allows + # us to check whether it has been specified on the CLI - bypassing the + # fact that arguments may be in any order + + def add_album_option(self, flags=('-a', '--album')): + """Add a -a/--album option to match albums instead of tracks. + + If used then the format option can auto-detect whether we're setting + the format for items or albums. + Sets the album property on the options extracted from the CLI. + """ + album = optparse.Option(*flags, action='store_true', + help='match albums instead of tracks') + self.add_option(album) + self._album_flags = set(flags) + + def _set_format(self, option, opt_str, value, parser, target=None, + fmt=None, store_true=False): + """Internal callback that sets the correct format while parsing CLI + arguments. + """ + if store_true: + setattr(parser.values, option.dest, True) + + value = fmt or value and unicode(value) or '' + parser.values.format = value + if target: + config[target._format_config_key].set(value) + else: + if self._album_flags: + if parser.values.album: + target = library.Album + else: + # the option is either missing either not parsed yet + if self._album_flags & set(parser.rargs): + target = library.Album + else: + target = library.Item + config[target._format_config_key].set(value) + else: + config[library.Item._format_config_key].set(value) + config[library.Album._format_config_key].set(value) + + def add_path_option(self, flags=('-p', '--path')): + """Add a -p/--path option to display the path instead of the default + format. + + By default this affects both items and albums. If add_album_option() + is used then the target will be autodetected. + + Sets the format property to u'$path' on the options extracted from the + CLI. + """ + path = optparse.Option(*flags, nargs=0, action='callback', + callback=self._set_format, + callback_kwargs={'fmt': '$path', + 'store_true': True}, + help='print paths for matched items or albums') + self.add_option(path) + + def add_format_option(self, flags=('-f', '--format'), target=None): + """Add -f/--format option to print some LibModel instances with a + custom format. + + `target` is optional and can be one of ``library.Item``, 'item', + ``library.Album`` and 'album'. + + Several behaviors are available: + - if `target` is given then the format is only applied to that + LibModel + - if the album option is used then the target will be autodetected + - otherwise the format is applied to both items and albums. + + Sets the format property on the options extracted from the CLI. + """ + kwargs = {} + if target: + if isinstance(target, basestring): + target = {'item': library.Item, + 'album': library.Album}[target] + kwargs['target'] = target + + opt = optparse.Option(*flags, action='callback', + callback=self._set_format, + callback_kwargs=kwargs, + help='print with custom format') + self.add_option(opt) + + def add_all_common_options(self): + """Add album, path and format options. + """ + self.add_album_option() + self.add_path_option() + self.add_format_option() + # Subcommand parsing infrastructure. # # This is a fairly generic subcommand parser for optparse. It is @@ -600,6 +713,7 @@ def show_model_changes(new, old=None, fields=None, always=False): # There you will also find a better description of the code and a more # succinct example program. + class Subcommand(object): """A subcommand of a root command-line application that may be invoked by a SubcommandOptionParser. @@ -609,10 +723,10 @@ class Subcommand(object): the subcommand; aliases are alternate names. parser is an OptionParser responsible for parsing the subcommand's options. help is a short description of the command. If no parser is - given, it defaults to a new, empty OptionParser. + given, it defaults to a new, empty CommonOptionsParser. """ self.name = name - self.parser = parser or optparse.OptionParser() + self.parser = parser or CommonOptionsParser() self.aliases = aliases self.help = help self.hide = hide @@ -635,7 +749,7 @@ class Subcommand(object): root_parser.get_prog_name().decode('utf8'), self.name) -class SubcommandsOptionParser(optparse.OptionParser): +class SubcommandsOptionParser(CommonOptionsParser): """A variant of OptionParser that parses subcommands and their arguments. """ @@ -653,7 +767,7 @@ class SubcommandsOptionParser(optparse.OptionParser): kwargs['add_help_option'] = False # Super constructor. - optparse.OptionParser.__init__(self, *args, **kwargs) + super(SubcommandsOptionParser, self).__init__(*args, **kwargs) # Our root parser needs to stop on the first unrecognized argument. self.disable_interspersed_args() @@ -670,7 +784,7 @@ class SubcommandsOptionParser(optparse.OptionParser): # Add the list of subcommands to the help message. def format_help(self, formatter=None): # Get the original help message, to which we will append. - out = optparse.OptionParser.format_help(self, formatter) + out = super(SubcommandsOptionParser, self).format_help(formatter) if formatter is None: formatter = self.formatter @@ -871,6 +985,17 @@ def _configure(options): u'See documentation for more info.') config['ui']['color'].set(config['color'].get(bool)) + # Compatibility from list_format_{item,album} to format_{item,album} + for elem in ('item', 'album'): + old_key = 'list_format_{0}'.format(elem) + if config[old_key].exists(): + new_key = 'format_{0}'.format(elem) + log.warning('Warning: configuration uses "{0}" which is deprecated' + ' in favor of "{1}" now that it affects all commands. ' + 'See changelog & documentation.'.format(old_key, + new_key)) + config[new_key].set(config[old_key]) + config_path = config.user_config_path() if os.path.isfile(config_path): log.debug(u'user configuration: {0}', @@ -913,6 +1038,8 @@ def _raw_main(args, lib=None): handling. """ parser = SubcommandsOptionParser() + parser.add_format_option(flags=('--format-item',), target=library.Item) + parser.add_format_option(flags=('--format-album',), target=library.Album) parser.add_option('-l', '--library', dest='library', help='library database file to use') parser.add_option('-d', '--directory', dest='directory', diff --git a/beets/ui/commands.py b/beets/ui/commands.py index bea5dc91c..5d29093c9 100644 --- a/beets/ui/commands.py +++ b/beets/ui/commands.py @@ -957,7 +957,7 @@ default_commands.append(import_cmd) # list: Query and show library contents. -def list_items(lib, query, album, fmt): +def list_items(lib, query, album, fmt=''): """Print out items in lib matching query. If album, then search for albums instead of single items. """ @@ -970,23 +970,11 @@ def list_items(lib, query, album, fmt): def list_func(lib, opts, args): - fmt = '$path' if opts.path else opts.format - list_items(lib, decargs(args), opts.album, fmt) + list_items(lib, decargs(args), opts.album) list_cmd = ui.Subcommand('list', help='query the library', aliases=('ls',)) -list_cmd.parser.add_option( - '-a', '--album', action='store_true', - help='show matching albums instead of tracks' -) -list_cmd.parser.add_option( - '-p', '--path', action='store_true', - help='print paths for matched items or albums' -) -list_cmd.parser.add_option( - '-f', '--format', action='store', - help='print with custom format', default='' -) +list_cmd.parser.add_all_common_options() list_cmd.func = list_func default_commands.append(list_cmd) @@ -1087,10 +1075,8 @@ def update_func(lib, opts, args): update_cmd = ui.Subcommand( 'update', help='update the library', aliases=('upd', 'up',) ) -update_cmd.parser.add_option( - '-a', '--album', action='store_true', - help='match albums instead of tracks' -) +update_cmd.parser.add_album_option() +update_cmd.parser.add_format_option() update_cmd.parser.add_option( '-M', '--nomove', action='store_false', default=True, dest='move', help="don't move files in library" @@ -1099,10 +1085,6 @@ update_cmd.parser.add_option( '-p', '--pretend', action='store_true', help="show all changes but do nothing" ) -update_cmd.parser.add_option( - '-f', '--format', action='store', - help='print with custom format', default='' -) update_cmd.func = update_func default_commands.append(update_cmd) @@ -1151,10 +1133,7 @@ remove_cmd.parser.add_option( "-d", "--delete", action="store_true", help="also remove files from disk" ) -remove_cmd.parser.add_option( - '-a', '--album', action='store_true', - help='match albums instead of tracks' -) +remove_cmd.parser.add_album_option() remove_cmd.func = remove_func default_commands.append(remove_cmd) @@ -1348,18 +1327,12 @@ modify_cmd.parser.add_option( '-W', '--nowrite', action='store_false', dest='write', help="don't write metadata (opposite of -w)" ) -modify_cmd.parser.add_option( - '-a', '--album', action='store_true', - help='modify whole albums instead of tracks' -) +modify_cmd.parser.add_album_option() +modify_cmd.parser.add_format_option(target='item') modify_cmd.parser.add_option( '-y', '--yes', action='store_true', help='skip confirmation' ) -modify_cmd.parser.add_option( - '-f', '--format', action='store', - help='print with custom format', default='' -) modify_cmd.func = modify_func default_commands.append(modify_cmd) @@ -1405,10 +1378,7 @@ move_cmd.parser.add_option( '-c', '--copy', default=False, action='store_true', help='copy instead of moving' ) -move_cmd.parser.add_option( - '-a', '--album', default=False, action='store_true', - help='match whole albums instead of tracks' -) +move_cmd.parser.add_album_option() move_cmd.func = move_func default_commands.append(move_cmd) diff --git a/beetsplug/convert.py b/beetsplug/convert.py index 352c3c94d..0cce93f7a 100644 --- a/beetsplug/convert.py +++ b/beetsplug/convert.py @@ -139,8 +139,6 @@ class ConvertPlugin(BeetsPlugin): cmd = ui.Subcommand('convert', help='convert to external location') cmd.parser.add_option('-p', '--pretend', action='store_true', help='show actions but do nothing') - cmd.parser.add_option('-a', '--album', action='store_true', - help='choose albums instead of tracks') cmd.parser.add_option('-t', '--threads', action='store', type='int', help='change the number of threads, \ defaults to maximum available processors') @@ -148,11 +146,12 @@ class ConvertPlugin(BeetsPlugin): dest='keep_new', help='keep only the converted \ and move the old files') cmd.parser.add_option('-d', '--dest', action='store', - help='set the destination directory') + help='set the target format of the tracks') cmd.parser.add_option('-f', '--format', action='store', dest='format', help='set the destination directory') cmd.parser.add_option('-y', '--yes', action='store_true', dest='yes', help='do not ask for confirmation') + cmd.parser.add_album_option() cmd.func = self.convert_func return [cmd] @@ -359,7 +358,7 @@ class ConvertPlugin(BeetsPlugin): self.config['pretend'].get(bool) if not pretend: - ui.commands.list_items(lib, ui.decargs(args), opts.album, '') + ui.commands.list_items(lib, ui.decargs(args), opts.album) if not (opts.yes or ui.input_yn("Convert? (Y/n)")): return diff --git a/beetsplug/duplicates.py b/beetsplug/duplicates.py index fb697922b..1739d3530 100644 --- a/beetsplug/duplicates.py +++ b/beetsplug/duplicates.py @@ -125,17 +125,6 @@ class DuplicatesPlugin(BeetsPlugin): self._command = Subcommand('duplicates', help=__doc__, aliases=['dup']) - - self._command.parser.add_option('-f', '--format', dest='format', - action='store', type='string', - help='print with custom format', - metavar='FMT', default='') - - self._command.parser.add_option('-a', '--album', dest='album', - action='store_true', - help='show duplicate albums instead of' - ' tracks') - self._command.parser.add_option('-c', '--count', dest='count', action='store_true', help='show duplicate counts') @@ -168,15 +157,11 @@ class DuplicatesPlugin(BeetsPlugin): action='store', metavar='DEST', help='copy items to dest') - self._command.parser.add_option('-p', '--path', dest='path', - action='store_true', - help='print paths for matched items or' - ' albums') - self._command.parser.add_option('-t', '--tag', dest='tag', action='store', help='tag matched items with \'k=v\'' ' attribute') + self._command.parser.add_all_common_options() def commands(self): diff --git a/beetsplug/echonest.py b/beetsplug/echonest.py index c8d45f3b0..753bfb5c3 100644 --- a/beetsplug/echonest.py +++ b/beetsplug/echonest.py @@ -486,10 +486,7 @@ class EchonestMetadataPlugin(plugins.BeetsPlugin): '-t', '--threshold', dest='threshold', action='store', type='float', default=0.15, help='Set difference threshold' ) - sim_cmd.parser.add_option( - '-f', '--format', action='store', default='${difference}: ${path}', - help='print with custom format' - ) + sim_cmd.parser.add_format_option() def sim_func(lib, opts, args): self.config.set_args(opts) diff --git a/beetsplug/mbsync.py b/beetsplug/mbsync.py index 365d83193..974f7e894 100644 --- a/beetsplug/mbsync.py +++ b/beetsplug/mbsync.py @@ -52,8 +52,7 @@ class MBSyncPlugin(BeetsPlugin): cmd.parser.add_option('-W', '--nowrite', action='store_false', default=config['import']['write'], dest='write', help="don't write updated metadata to files") - cmd.parser.add_option('-f', '--format', action='store', default='', - help='print with custom format') + cmd.parser.add_format_option() cmd.func = self.func return [cmd] @@ -64,17 +63,16 @@ class MBSyncPlugin(BeetsPlugin): pretend = opts.pretend write = opts.write query = ui.decargs(args) - fmt = opts.format - self.singletons(lib, query, move, pretend, write, fmt) - self.albums(lib, query, move, pretend, write, fmt) + self.singletons(lib, query, move, pretend, write) + self.albums(lib, query, move, pretend, write) - def singletons(self, lib, query, move, pretend, write, fmt): + def singletons(self, lib, query, move, pretend, write): """Retrieve and apply info from the autotagger for items matched by query. """ for item in lib.items(query + ['singleton:true']): - item_formatted = format(item, fmt) + item_formatted = format(item) if not item.mb_trackid: self._log.info(u'Skipping singleton with no mb_trackid: {0}', item_formatted) @@ -93,13 +91,13 @@ class MBSyncPlugin(BeetsPlugin): autotag.apply_item_metadata(item, track_info) apply_item_changes(lib, item, move, pretend, write) - def albums(self, lib, query, move, pretend, write, fmt): + def albums(self, lib, query, move, pretend, write): """Retrieve and apply info from the autotagger for albums matched by query and their items. """ # Process matching albums. for a in lib.albums(query): - album_formatted = format(a, fmt) + album_formatted = format(a) if not a.mb_albumid: self._log.info(u'Skipping album with no mb_albumid: {0}', album_formatted) diff --git a/beetsplug/missing.py b/beetsplug/missing.py index 517a20758..7f976f75c 100644 --- a/beetsplug/missing.py +++ b/beetsplug/missing.py @@ -94,19 +94,13 @@ class MissingPlugin(BeetsPlugin): self._command = Subcommand('missing', help=__doc__, aliases=['miss']) - - self._command.parser.add_option('-f', '--format', dest='format', - action='store', type='string', - help='print with custom FORMAT', - metavar='FORMAT', default='') - self._command.parser.add_option('-c', '--count', dest='count', action='store_true', help='count missing tracks per album') - self._command.parser.add_option('-t', '--total', dest='total', action='store_true', help='count total of missing tracks') + self._command.add_format_option() def commands(self): def _miss(lib, opts, args): diff --git a/beetsplug/play.py b/beetsplug/play.py index 8a912505c..65d5b91ec 100644 --- a/beetsplug/play.py +++ b/beetsplug/play.py @@ -42,11 +42,7 @@ class PlayPlugin(BeetsPlugin): 'play', help='send music to a player as a playlist' ) - play_command.parser.add_option( - '-a', '--album', - action='store_true', default=False, - help='query and load albums rather than tracks' - ) + play_command.parser.add_album_option() play_command.func = self.play_music return [play_command] diff --git a/beetsplug/random.py b/beetsplug/random.py index f6a664a4c..43fb7957c 100644 --- a/beetsplug/random.py +++ b/beetsplug/random.py @@ -26,7 +26,6 @@ from itertools import groupby def random_item(lib, opts, args): query = decargs(args) - fmt = '$path' if opts.path else opts.format if opts.album: objs = list(lib.albums(query)) @@ -63,20 +62,15 @@ def random_item(lib, opts, args): objs = random.sample(objs, number) for item in objs: - print_(format(item, fmt)) + print_(format(item)) random_cmd = Subcommand('random', help='chose a random track or album') -random_cmd.parser.add_option('-a', '--album', action='store_true', - help='choose an album instead of track') -random_cmd.parser.add_option('-p', '--path', action='store_true', - help='print the path of the matched item') -random_cmd.parser.add_option('-f', '--format', action='store', - help='print with custom format', default='') random_cmd.parser.add_option('-n', '--number', action='store', type="int", help='number of objects to choose', default=1) random_cmd.parser.add_option('-e', '--equal-chance', action='store_true', help='each artist has the same chance') +random_cmd.parser.add_all_common_options() random_cmd.func = random_item diff --git a/beetsplug/replaygain.py b/beetsplug/replaygain.py index 41937d455..11f8c794c 100644 --- a/beetsplug/replaygain.py +++ b/beetsplug/replaygain.py @@ -867,7 +867,6 @@ class ReplayGainPlugin(BeetsPlugin): self.handle_track(item, write) cmd = ui.Subcommand('replaygain', help='analyze for ReplayGain') - cmd.parser.add_option('-a', '--album', action='store_true', - help='analyze albums instead of tracks') + cmd.parser.add_album_option() cmd.func = func return [cmd] diff --git a/docs/changelog.rst b/docs/changelog.rst index ab9918ae3..693c4b1d8 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -6,6 +6,9 @@ Changelog Features: +* Beets now accept top-level options ``--format-item`` and ``--format-album`` + before any subcommand to control how items and albums are displayed. + :bug:`1271`: * :doc:`/plugins/replaygain`: There is a new backend for the `bs1770gain`_ tool. Thanks to :user:`jmwatte`. :bug:`1343` * There are now multiple levels of verbosity. On the command line, you can @@ -69,10 +72,13 @@ Core changes: ``albumtotal`` computed attribute that provides the total number of tracks on the album. (The :ref:`per_disc_numbering` option has no influence on this field.) -* The :ref:`list_format_album` and :ref:`list_format_item` configuration keys +* The `list_format_album` and `list_format_item` configuration keys now affect (almost) every place where objects are printed and logged. (Previously, they only controlled the :ref:`list-cmd` command and a few other scattered pieces.) :bug:`1269` +* `list_format_album` and `list_format_album` have respectively been + renamed :ref:`format_album` and :ref:`format_item`. The old names still work + but each triggers a warning message. :bug:`1271` Fixes: @@ -120,6 +126,9 @@ Fixes: For developers: +* the ``OptionParser`` is now a ``CommonOptionsParser`` that offers facilities + for adding usual options (``--album``, ``--path`` and ``--format``). See + :ref:`add_subcommands`. :bug:`1271` * The logging system in beets has been overhauled. Plugins now each have their own logger, which helps by automatically adjusting the verbosity level in import mode and by prefixing the plugin's name. Logging levels are diff --git a/docs/dev/plugins.rst b/docs/dev/plugins.rst index 79a3f7354..0c1f7017f 100644 --- a/docs/dev/plugins.rst +++ b/docs/dev/plugins.rst @@ -87,8 +87,11 @@ The function should use any of the utility functions defined in ``beets.ui``. Try running ``pydoc beets.ui`` to see what's available. You can add command-line options to your new command using the ``parser`` member -of the ``Subcommand`` class, which is an ``OptionParser`` instance. Just use it -like you would a normal ``OptionParser`` in an independent script. +of the ``Subcommand`` class, which is a ``CommonOptionParser`` instance. Just +use it like you would a normal ``OptionParser`` in an independent script. Note +that it offers several methods to add common options: ``--album``, ``--path`` +and ``--format``. This feature is versatile and extensively documented, try +``pydoc beets.ui.CommonOptionParser`` for more information. .. _plugin_events: diff --git a/docs/plugins/duplicates.rst b/docs/plugins/duplicates.rst index 18c2a8052..ecbda69cf 100644 --- a/docs/plugins/duplicates.rst +++ b/docs/plugins/duplicates.rst @@ -61,7 +61,7 @@ file. The available options mirror the command-line options: or album. This uses the same template syntax as beets' :doc:`path formats`. The usage is inspired by, and therefore similar to, the :ref:`list ` command. - Default: :ref:`list_format_item` + Default: :ref:`format_item` - **full**: List every track or album that has duplicates, not just the duplicates themselves. Default: ``no``. diff --git a/docs/plugins/mbsync.rst b/docs/plugins/mbsync.rst index cf89974e4..a7633a500 100644 --- a/docs/plugins/mbsync.rst +++ b/docs/plugins/mbsync.rst @@ -34,5 +34,5 @@ The command has a few command-line options: plugin will write new metadata to files' tags. To disable this, use the ``-W`` (``--nowrite``) option. * To customize the output of unrecognized items, use the ``-f`` - (``--format``) option. The default output is ``list_format_item`` or - ``list_format_album`` for items and albums, respectively. + (``--format``) option. The default output is ``format_item`` or + ``format_album`` for items and albums, respectively. diff --git a/docs/plugins/missing.rst b/docs/plugins/missing.rst index ffca94052..aab04e71b 100644 --- a/docs/plugins/missing.rst +++ b/docs/plugins/missing.rst @@ -36,7 +36,7 @@ configuration file. The available options are: track. This uses the same template syntax as beets' :doc:`path formats `. The usage is inspired by, and therefore similar to, the :ref:`list ` command. - Default: :ref:`list_format_item`. + Default: :ref:`format_item`. - **total**: Print a single count of missing tracks in all albums. Default: ``no``. diff --git a/docs/reference/config.rst b/docs/reference/config.rst index a64e73749..fc686a8b8 100644 --- a/docs/reference/config.rst +++ b/docs/reference/config.rst @@ -162,25 +162,32 @@ Either ``yes`` or ``no``, indicating whether the autotagger should use multiple threads. This makes things faster but may behave strangely. Defaults to ``yes``. -.. _list_format_item: -list_format_item -~~~~~~~~~~~~~~~~ +.. _list_format_item: +.. _format_item: + +format_item +~~~~~~~~~~~ Format to use when listing *individual items* with the :ref:`list-cmd` command and other commands that need to print out items. Defaults to ``$artist - $album - $title``. The ``-f`` command-line option overrides this setting. -.. _list_format_album: +It used to be named `list_format_item`. -list_format_album -~~~~~~~~~~~~~~~~~ +.. _list_format_album: +.. _format_album: + +format_album +~~~~~~~~~~~~ Format to use when listing *albums* with :ref:`list-cmd` and other commands. Defaults to ``$albumartist - $album``. The ``-f`` command-line option overrides this setting. +It used to be named `list_format_album`. + .. _sort_item: sort_item diff --git a/test/test_library.py b/test/test_library.py index 130c204f3..c3807637e 100644 --- a/test/test_library.py +++ b/test/test_library.py @@ -988,7 +988,7 @@ class TemplateTest(_common.LibTestCase): self.assertEqual(self.i.evaluate_template('$foo'), 'baz') def test_album_and_item_format(self): - config['list_format_album'] = u'foö $foo' + config['format_album'] = u'foö $foo' album = beets.library.Album() album.foo = 'bar' album.tagada = 'togodo' @@ -997,7 +997,7 @@ class TemplateTest(_common.LibTestCase): self.assertEqual(unicode(album), u"foö bar") self.assertEqual(str(album), b"fo\xc3\xb6 bar") - config['list_format_item'] = 'bar $foo' + config['format_item'] = 'bar $foo' item = beets.library.Item() item.foo = 'bar' item.tagada = 'togodo' diff --git a/test/test_mbsync.py b/test/test_mbsync.py index fc37fe8c3..ff6e01cf3 100644 --- a/test/test_mbsync.py +++ b/test/test_mbsync.py @@ -73,8 +73,8 @@ class MbsyncCliTest(unittest.TestCase, TestHelper): self.assertEqual(album.album, 'album info') def test_message_when_skipping(self): - config['list_format_item'] = '$artist - $album - $title' - config['list_format_album'] = '$albumartist - $album' + config['format_item'] = '$artist - $album - $title' + config['format_album'] = '$albumartist - $album' # Test album with no mb_albumid. # The default format for an album include $albumartist so @@ -99,6 +99,10 @@ class MbsyncCliTest(unittest.TestCase, TestHelper): e = "mbsync: Skipping album with no mb_albumid: 'album info'" self.assertEqual(e, logs[0]) + # restore the config + config['format_item'] = '$artist - $album - $title' + config['format_album'] = '$albumartist - $album' + # Test singleton with no mb_trackid. # The default singleton format includes $artist and $album # so we need to stub them here diff --git a/test/test_ui.py b/test/test_ui.py index d2a2ea80e..0f8bb6306 100644 --- a/test/test_ui.py +++ b/test/test_ui.py @@ -1033,6 +1033,145 @@ class CompletionTest(_common.TestCase): self.fail('test/test_completion.sh did not execute properly') +class CommonOptionsParserCliTest(unittest.TestCase, TestHelper): + """Test CommonOptionsParser and formatting LibModel formatting on 'list' + command. + """ + def setUp(self): + self.setup_beets() + self.lib = library.Library(':memory:') + self.item = _common.item() + self.item.path = 'xxx/yyy' + self.lib.add(self.item) + self.lib.add_album([self.item]) + + def tearDown(self): + self.teardown_beets() + + def test_base(self): + l = self.run_with_output('ls') + self.assertEqual(l, 'the artist - the album - the title\n') + + l = self.run_with_output('ls', '-a') + self.assertEqual(l, 'the album artist - the album\n') + + def test_path_option(self): + l = self.run_with_output('ls', '-p') + self.assertEqual(l, 'xxx/yyy\n') + + l = self.run_with_output('ls', '-a', '-p') + self.assertEqual(l, 'xxx\n') + + def test_format_option(self): + l = self.run_with_output('ls', '-f', '$artist') + self.assertEqual(l, 'the artist\n') + + l = self.run_with_output('ls', '-a', '-f', '$albumartist') + self.assertEqual(l, 'the album artist\n') + + def test_root_format_option(self): + l = self.run_with_output('--format-item', '$artist', + '--format-album', 'foo', 'ls') + self.assertEqual(l, 'the artist\n') + + l = self.run_with_output('--format-item', 'foo', + '--format-album', '$albumartist', 'ls', '-a') + self.assertEqual(l, 'the album artist\n') + + +class CommonOptionsParserTest(unittest.TestCase, TestHelper): + def setUp(self): + self.setup_beets() + + def tearDown(self): + self.teardown_beets() + + def test_album_option(self): + parser = ui.CommonOptionsParser() + self.assertFalse(parser._album_flags) + parser.add_album_option() + self.assertTrue(bool(parser._album_flags)) + + self.assertEqual(parser.parse_args([]), ({'album': None}, [])) + self.assertEqual(parser.parse_args(['-a']), ({'album': True}, [])) + self.assertEqual(parser.parse_args(['--album']), ({'album': True}, [])) + + def test_path_option(self): + parser = ui.CommonOptionsParser() + parser.add_path_option() + self.assertFalse(parser._album_flags) + + config['format_item'].set('$foo') + self.assertEqual(parser.parse_args([]), ({'path': None}, [])) + self.assertEqual(config['format_item'].get(unicode), u'$foo') + + self.assertEqual(parser.parse_args(['-p']), + ({'path': True, 'format': '$path'}, [])) + self.assertEqual(parser.parse_args(['--path']), + ({'path': True, 'format': '$path'}, [])) + + self.assertEqual(config['format_item'].get(unicode), '$path') + self.assertEqual(config['format_album'].get(unicode), '$path') + + def test_format_option(self): + parser = ui.CommonOptionsParser() + parser.add_format_option() + self.assertFalse(parser._album_flags) + + config['format_item'].set('$foo') + self.assertEqual(parser.parse_args([]), ({'format': None}, [])) + self.assertEqual(config['format_item'].get(unicode), u'$foo') + + self.assertEqual(parser.parse_args(['-f', '$bar']), + ({'format': '$bar'}, [])) + self.assertEqual(parser.parse_args(['--format', '$baz']), + ({'format': '$baz'}, [])) + + self.assertEqual(config['format_item'].get(unicode), '$baz') + self.assertEqual(config['format_album'].get(unicode), '$baz') + + def test_format_option_with_target(self): + with self.assertRaises(KeyError): + ui.CommonOptionsParser().add_format_option(target='thingy') + + parser = ui.CommonOptionsParser() + parser.add_format_option(target='item') + + config['format_item'].set('$item') + config['format_album'].set('$album') + + self.assertEqual(parser.parse_args(['-f', '$bar']), + ({'format': '$bar'}, [])) + + self.assertEqual(config['format_item'].get(unicode), '$bar') + self.assertEqual(config['format_album'].get(unicode), '$album') + + def test_format_option_with_album(self): + parser = ui.CommonOptionsParser() + parser.add_album_option() + parser.add_format_option() + + config['format_item'].set('$item') + config['format_album'].set('$album') + + parser.parse_args(['-f', '$bar']) + self.assertEqual(config['format_item'].get(unicode), '$bar') + self.assertEqual(config['format_album'].get(unicode), '$album') + + parser.parse_args(['-a', '-f', '$foo']) + self.assertEqual(config['format_item'].get(unicode), '$bar') + self.assertEqual(config['format_album'].get(unicode), '$foo') + + parser.parse_args(['-f', '$foo2', '-a']) + self.assertEqual(config['format_album'].get(unicode), '$foo2') + + def test_add_all_common_options(self): + parser = ui.CommonOptionsParser() + parser.add_all_common_options() + self.assertEqual(parser.parse_args([]), + ({'album': None, 'path': None, 'format': None}, [])) + + def suite(): return unittest.TestLoader().loadTestsFromName(__name__)