Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

split apply_choice coroutine

This essential import pipeline stage is now two: one that applies metadata
changes and one that manipulates the filesystem. This will eventually allow
lastgenere to apply its changes before destinations are calculated.
  • Loading branch information...
commit 86f513d4ab2feb239735cbe348b2c045e4e64cf3 1 parent 66e75c3
@sampsyo authored
Showing with 68 additions and 72 deletions.
  1. +18 −6 beets/importer.py
  2. +50 −66 test/test_importer.py
View
24 beets/importer.py
@@ -688,14 +688,14 @@ def apply_choices(config):
# Find existing item entries that these are replacing (for
# re-imports). Old album structures are automatically cleaned up
# when the last item is removed.
- replaced_items = defaultdict(list)
+ task.replaced_items = defaultdict(list)
for item in items:
dup_items = lib.items(library.MatchQuery('path', item.path))
for dup_item in dup_items:
- replaced_items[item].append(dup_item)
+ task.replaced_items[item].append(dup_item)
log.debug('replacing item %i: %s' %
(dup_item.id, displayable_path(item.path)))
- log.debug('%i of %i items replaced' % (len(replaced_items),
+ log.debug('%i of %i items replaced' % (len(task.replaced_items),
len(items)))
# Find old items that should be replaced as part of a duplicate
@@ -725,7 +725,7 @@ def apply_choices(config):
# are in place before calls to destination().
with lib.transaction():
# Remove old items.
- for replaced in replaced_items.itervalues():
+ for replaced in task.replaced_items.itervalues():
for item in replaced:
lib.remove(item)
for item in duplicate_items:
@@ -741,7 +741,19 @@ def apply_choices(config):
for item in items:
lib.add(item)
+def manipulate_files(config):
+ """A coroutine (pipeline stage) that performs necessary file
+ manipulations *after* items have been added to the library.
+ """
+ lib = _reopen_lib(config.lib)
+ task = None
+ while True:
+ task = yield task
+ if task.should_skip():
+ continue
+
# Move/copy files.
+ items = task.all_items()
task.old_paths = [item.path for item in items] # For deletion.
for item in items:
if config.move:
@@ -756,7 +768,7 @@ def apply_choices(config):
# out-of-library files. Otherwise, copy and keep track
# of the old path.
old_path = item.path
- if replaced_items[item]:
+ if task.replaced_items[item]:
# This is a reimport. Move in-library files and copy
# out-of-library files.
if lib.directory in util.ancestry(old_path):
@@ -928,7 +940,7 @@ def run_import(**kwargs):
else:
# When not autotagging, just display progress.
stages += [show_progress(config)]
- stages += [apply_choices(config)]
+ stages += [apply_choices(config), manipulate_files(config)]
if config.art:
stages += [fetch_art(config)]
stages += [finalize(config)]
View
116 test/test_importer.py
@@ -168,24 +168,31 @@ def test_import_singleton(self):
paths = self._run_import(['sometrack'], singletons=True)
self.assertTrue(os.path.exists(paths[0]))
-# Utilities for invoking the apply_choices coroutine.
-def _call_apply(coros, items, info, toppath=None):
- task = importer.ImportTask(None, None, None)
+# Utilities for invoking the apply_choices, manipulate_files, and finalize
+# coroutines.
+def _call_stages(config, items, choice_or_info,
+ stages=[importer.apply_choices,
+ importer.manipulate_files,
+ importer.finalize],
+ album=True, toppath=None):
+ # Set up the import task.
+ task = importer.ImportTask(None, None, items)
task.is_album = True
task.toppath = toppath
- task.set_choice((info, items))
- if not isinstance(coros, list):
- coros = [coros]
- for coro in coros:
- task = coro.send(task)
- return task
-def _call_apply_choice(coro, items, choice, album=True):
- task = importer.ImportTask(None, None, items)
- task.is_album = album
if not album:
task.item = items[0]
- task.set_choice(choice)
- coro.send(task)
+ if isinstance(choice_or_info, importer.action):
+ task.set_choice(choice_or_info)
+ else:
+ task.set_choice((choice_or_info, items))
+
+ # Call the coroutines.
+ for stage in stages:
+ coro = stage(config)
+ coro.next()
+ coro.send(task)
+
+ return task
class ImportApplyTest(unittest.TestCase, _common.ExtraAsserts):
def setUp(self):
@@ -228,51 +235,41 @@ def tearDown(self):
def test_finalize_no_delete(self):
config = _common.iconfig(self.lib, delete=False)
- applyc = importer.apply_choices(config)
- applyc.next()
- finalize = importer.finalize(config)
- finalize.next()
- _call_apply([applyc, finalize], [self.i], self.info)
+ _call_stages(config, [self.i], self.info)
self.assertExists(self.srcpath)
def test_finalize_with_delete(self):
config = _common.iconfig(self.lib, delete=True)
- applyc = importer.apply_choices(config)
- applyc.next()
- finalize = importer.finalize(config)
- finalize.next()
- _call_apply([applyc, finalize], [self.i], self.info)
+ _call_stages(config, [self.i], self.info)
self.assertNotExists(self.srcpath)
def test_finalize_with_delete_prunes_directory_empty(self):
config = _common.iconfig(self.lib, delete=True)
- applyc = importer.apply_choices(config)
- applyc.next()
- finalize = importer.finalize(config)
- finalize.next()
- _call_apply([applyc, finalize], [self.i], self.info,
- self.srcdir)
+ _call_stages(config, [self.i], self.info,
+ toppath=self.srcdir)
self.assertNotExists(os.path.dirname(self.srcpath))
def test_apply_asis_uses_album_path(self):
- coro = importer.apply_choices(_common.iconfig(self.lib))
- coro.next() # Prime coroutine.
- _call_apply_choice(coro, [self.i], importer.action.ASIS)
+ config = _common.iconfig(self.lib)
+ _call_stages(config, [self.i], importer.action.ASIS)
self.assertExists(os.path.join(self.libdir, 'one.mp3'))
def test_apply_match_uses_album_path(self):
- coro = importer.apply_choices(_common.iconfig(self.lib))
- coro.next() # Prime coroutine.
- _call_apply(coro, [self.i], self.info)
+ config = _common.iconfig(self.lib)
+ _call_stages(config, [self.i], self.info)
self.assertExists(os.path.join(self.libdir, 'one.mp3'))
def test_apply_tracks_uses_singleton_path(self):
- coro = importer.apply_choices(_common.iconfig(self.lib))
- coro.next() # Prime coroutine.
+ config = _common.iconfig(self.lib)
+ apply_coro = importer.apply_choices(config)
+ apply_coro.next()
+ manip_coro = importer.manipulate_files(config)
+ manip_coro.next()
task = importer.ImportTask.item_task(self.i)
task.set_choice(self.info.tracks[0])
- coro.send(task)
+ apply_coro.send(task)
+ manip_coro.send(task)
self.assertExists(
os.path.join(self.libdir, 'three.mp3')
@@ -285,9 +282,8 @@ def test_apply_sentinel(self):
# Just test no exception for now.
def test_apply_populates_old_paths(self):
- coro = importer.apply_choices(_common.iconfig(self.lib))
- coro.next()
- task = _call_apply(coro, [self.i], self.info)
+ config = _common.iconfig(self.lib)
+ task = _call_stages(config, [self.i], self.info)
self.assertEqual(task.old_paths, [self.srcpath])
def test_reimport_inside_file_moves_and_does_not_add_to_old_paths(self):
@@ -305,9 +301,8 @@ def test_reimport_inside_file_moves_and_does_not_add_to_old_paths(self):
self.i.comp = False
# Then, re-import the same file.
- coro = importer.apply_choices(_common.iconfig(self.lib))
- coro.next()
- task = _call_apply(coro, [self.i], self.info)
+ config =_common.iconfig(self.lib)
+ task = _call_stages(config, [self.i], self.info)
# Old file should be gone.
self.assertNotExists(internal_srcpath)
@@ -327,9 +322,8 @@ def test_reimport_outside_file_copies(self):
self.lib.conn.commit()
# Then, re-import the same file.
- coro = importer.apply_choices(_common.iconfig(self.lib))
- coro.next()
- task = _call_apply(coro, [self.i], self.info)
+ config = _common.iconfig(self.lib)
+ task = _call_stages(config, [self.i], self.info)
# Old file should still exist.
self.assertExists(self.srcpath)
@@ -341,21 +335,13 @@ def test_reimport_outside_file_copies(self):
def test_apply_with_move(self):
config = _common.iconfig(self.lib, move=True)
- applyc = importer.apply_choices(config)
- applyc.next()
- finalize = importer.finalize(config)
- finalize.next()
- _call_apply([applyc], [self.i], self.info)
+ _call_stages(config, [self.i], self.info)
self.assertExists(list(self.lib.items())[0].path)
self.assertNotExists(self.srcpath)
def test_apply_with_move_prunes_empty_directory(self):
config = _common.iconfig(self.lib, move=True)
- applyc = importer.apply_choices(config)
- applyc.next()
- finalize = importer.finalize(config)
- finalize.next()
- _call_apply([applyc], [self.i], self.info, self.srcdir)
+ _call_stages(config, [self.i], self.info, toppath=self.srcdir)
self.assertNotExists(os.path.dirname(self.srcpath))
class AsIsApplyTest(unittest.TestCase):
@@ -380,11 +366,9 @@ def tearDown(self):
os.remove(self.dbpath)
def _apply_result(self):
- """Run the "apply" coroutine and get the resulting Album."""
- coro = importer.apply_choices(self.config)
- coro.next()
- _call_apply_choice(coro, self.items, importer.action.ASIS)
-
+ """Run the "apply" coroutines and get the resulting Album."""
+ _call_stages(self.config, self.items, importer.action.ASIS,
+ stages=[importer.apply_choices])
return self.lib.albums()[0]
def test_asis_homogenous_va_not_set(self):
@@ -428,9 +412,9 @@ def tearDown(self):
def _apply_asis(self, items, album=True):
"""Run the "apply" coroutine."""
- coro = importer.apply_choices(self.config)
- coro.next()
- _call_apply_choice(coro, items, importer.action.ASIS, album)
+ _call_stages(self.config, items, importer.action.ASIS, album=album,
+ stages=[importer.apply_choices,
+ importer.manipulate_files])
def test_apply_existing_album_does_not_duplicate_item(self):
# First, import an item to add it to the library.
Please sign in to comment.
Something went wrong with that request. Please try again.