From be484f2af0ed471b61aa53a2522e6016240e1bf3 Mon Sep 17 00:00:00 2001 From: Tom Jaspers Date: Wed, 20 May 2015 12:53:43 +0200 Subject: [PATCH] Implement `--pretend` option for the move command The method `show_path_changes` takes a list of tuples (source, destination) that will be printed on either single / double line, as proposed in #1405. --- beets/ui/__init__.py | 38 ++++++++++++++++++++++++++++++++++++++ beets/ui/commands.py | 25 ++++++++++++++++++------- docs/changelog.rst | 2 ++ docs/reference/cli.rst | 6 +++++- test/test_ui.py | 15 +++++++++++++-- 5 files changed, 76 insertions(+), 10 deletions(-) diff --git a/beets/ui/__init__.py b/beets/ui/__init__.py index 291e8dbe3..851c7b32c 100644 --- a/beets/ui/__init__.py +++ b/beets/ui/__init__.py @@ -615,6 +615,44 @@ def show_model_changes(new, old=None, fields=None, always=False): return bool(changes) +def show_path_changes(path_changes): + """ Given a list of tuples (source, destination) that indicate the path + changes, the changes are shown. Output is guaranteed to be unicode. + + Every tuple is shown on a single line if the terminal width permits it, + else it is split over two lines. E.g., + + Source -> Destination + + vs. + + Source + -> Destination + """ + sources, destinations = zip(*path_changes) + + # Ensure unicode output + sources = map(util.displayable_path, sources) + destinations = map(util.displayable_path, destinations) + + # Calculate widths for terminal split + col_width = (term_width() - len(' -> ')) // 2 + max_width = len(max(sources + destinations, key=len)) + + if max_width > col_width: + # Print every change over two lines + for source, dest in zip(sources, destinations): + log.info(u'{0} \n -> {1}', source, dest) + else: + # Print every change on a single line, and add a header + title_pad = max_width - len('Source ') + len(' -> ') + + log.info(u'Source {0} Destination', ' ' * title_pad) + for source, dest in zip(sources, destinations): + pad = max_width - len(source) + log.info(u'{0} {1} -> {2}', source, ' ' * pad, dest) + + class CommonOptionsParser(optparse.OptionParser, object): """Offers a simple way to add common formatting options. diff --git a/beets/ui/commands.py b/beets/ui/commands.py index 263df8fdc..00006ce51 100644 --- a/beets/ui/commands.py +++ b/beets/ui/commands.py @@ -24,7 +24,7 @@ import re import beets from beets import ui -from beets.ui import print_, input_, decargs +from beets.ui import print_, input_, decargs, show_path_changes from beets import autotag from beets.autotag import Recommendation from beets.autotag import hooks @@ -1344,7 +1344,7 @@ default_commands.append(modify_cmd) # move: Move/copy files to the library or a new base directory. -def move_items(lib, dest, query, copy, album): +def move_items(lib, dest, query, copy, album, pretend): """Moves or copies items to a new base directory, given by dest. If dest is None, then the library's base directory is used, making the command "consolidate" files. @@ -1355,11 +1355,19 @@ def move_items(lib, dest, query, copy, album): action = 'Copying' if copy else 'Moving' entity = 'album' if album else 'item' log.info(u'{0} {1} {2}s.', action, len(objs), entity) - for obj in objs: - log.debug(u'moving: {0}', util.displayable_path(obj.path)) + if pretend: + if album: + show_path_changes([(item.path, item.destination(basedir=dest)) + for obj in objs for item in obj.items()]) + else: + show_path_changes([(obj.path, obj.destination(basedir=dest)) + for obj in objs]) + else: + for obj in objs: + log.debug(u'moving: {0}', util.displayable_path(obj.path)) - obj.move(copy, basedir=dest) - obj.store() + obj.move(copy, basedir=dest) + obj.store() def move_func(lib, opts, args): @@ -1369,7 +1377,7 @@ def move_func(lib, opts, args): if not os.path.isdir(dest): raise ui.UserError('no such directory: %s' % dest) - move_items(lib, dest, decargs(args), opts.copy, opts.album) + move_items(lib, dest, decargs(args), opts.copy, opts.album, opts.pretend) move_cmd = ui.Subcommand( @@ -1383,6 +1391,9 @@ move_cmd.parser.add_option( '-c', '--copy', default=False, action='store_true', help='copy instead of moving' ) +move_cmd.parser.add_option( + '-p', '--pretend', default=False, action='store_true', + help='show how files would be moved, but don\'t touch anything') move_cmd.parser.add_album_option() move_cmd.func = move_func default_commands.append(move_cmd) diff --git a/docs/changelog.rst b/docs/changelog.rst index 487c27ad9..738696933 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -13,6 +13,8 @@ New features: This plugin is still in an experimental phase. :bug:`1450` * The :doc:`/plugins/fetchart` plugin will now complain for the `enforce_ratio` and `min_width` options if no local imaging backend is available. :bug:`1460` +* The `move` command has a new `-p/--pretend` option, making the command show + how the items will be moved, without modifying the files on disk. Fixes: diff --git a/docs/reference/cli.rst b/docs/reference/cli.rst index d3fc9fd68..ab229e362 100644 --- a/docs/reference/cli.rst +++ b/docs/reference/cli.rst @@ -245,7 +245,7 @@ move ```` :: - beet move [-ca] [-d DIR] QUERY + beet move [-cap] [-d DIR] QUERY Move or copy items in your library. @@ -255,6 +255,10 @@ destination directory with ``-d`` manually, you can move items matching a query anywhere in your filesystem. The ``-c`` option copies files instead of moving them. As with other commands, the ``-a`` option matches albums instead of items. +To perform a "dry run", just use the ``-p`` (for "pretend") flag. This will +show you all how the files would be moved but won't actually change anything +on disk. + .. _update-cmd: update diff --git a/test/test_ui.py b/test/test_ui.py index 14cb4081f..0685742ec 100644 --- a/test/test_ui.py +++ b/test/test_ui.py @@ -373,8 +373,9 @@ class MoveTest(_common.TestCase): # Alternate destination directory. self.otherdir = os.path.join(self.temp_dir, 'testotherdir') - def _move(self, query=(), dest=None, copy=False, album=False): - commands.move_items(self.lib, dest, query, copy, album) + def _move(self, query=(), dest=None, copy=False, album=False, + pretend=False): + commands.move_items(self.lib, dest, query, copy, album, pretend) def test_move_item(self): self._move() @@ -418,6 +419,16 @@ class MoveTest(_common.TestCase): self.assertExists(self.i.path) self.assertNotExists(self.itempath) + def test_pretend_move_item(self): + self._move(dest=self.otherdir, pretend=True) + self.i.load() + self.assertIn('srcfile', self.i.path) + + def test_pretend_move_album(self): + self._move(album=True, pretend=True) + self.i.load() + self.assertIn('srcfile', self.i.path) + class UpdateTest(_common.TestCase): def setUp(self):