mirror of
https://github.com/beetbox/beets.git
synced 2026-02-24 08:12:54 +01:00
Merge pull request #1758 from diego-plan9/prompthook
Add event for adding importer options + mbsubmit plugin
This commit is contained in:
commit
9bddeceb9a
4 changed files with 370 additions and 13 deletions
|
|
@ -22,6 +22,8 @@ from __future__ import (division, absolute_import, print_function,
|
|||
|
||||
import os
|
||||
import re
|
||||
from collections import namedtuple, Counter
|
||||
from itertools import chain
|
||||
|
||||
import beets
|
||||
from beets import ui
|
||||
|
|
@ -39,6 +41,7 @@ from beets import logging
|
|||
from beets.util.confit import _package_path
|
||||
|
||||
VARIOUS_ARTISTS = u'Various Artists'
|
||||
PromptChoice = namedtuple('ExtraChoice', ['short', 'long', 'callback'])
|
||||
|
||||
# Global logger.
|
||||
log = logging.getLogger('beets')
|
||||
|
|
@ -471,7 +474,8 @@ def _summary_judment(rec):
|
|||
|
||||
|
||||
def choose_candidate(candidates, singleton, rec, cur_artist=None,
|
||||
cur_album=None, item=None, itemcount=None):
|
||||
cur_album=None, item=None, itemcount=None,
|
||||
extra_choices=[]):
|
||||
"""Given a sorted list of candidates, ask the user for a selection
|
||||
of which candidate to use. Applies to both full albums and
|
||||
singletons (tracks). Candidates are either AlbumMatch or TrackMatch
|
||||
|
|
@ -479,8 +483,16 @@ def choose_candidate(candidates, singleton, rec, cur_artist=None,
|
|||
`cur_album`, and `itemcount` must be provided. For singletons,
|
||||
`item` must be provided.
|
||||
|
||||
Returns the result of the choice, which may SKIP, ASIS, TRACKS, or
|
||||
MANUAL or a candidate (an AlbumMatch/TrackMatch object).
|
||||
`extra_choices` is a list of `PromptChoice`s, containg the choices
|
||||
appended by the plugins after receiving the `before_choose_candidate`
|
||||
event. If not empty, the choices are appended to the prompt presented
|
||||
to the user.
|
||||
|
||||
Returns one of the following:
|
||||
* the result of the choice, which may be SKIP, ASIS, TRACKS, or MANUAL
|
||||
* a candidate (an AlbumMatch/TrackMatch object)
|
||||
* the short letter of a `PromptChoice` (if the user selected one of
|
||||
the `extra_choices`).
|
||||
"""
|
||||
# Sanity check.
|
||||
if singleton:
|
||||
|
|
@ -489,6 +501,10 @@ def choose_candidate(candidates, singleton, rec, cur_artist=None,
|
|||
assert cur_artist is not None
|
||||
assert cur_album is not None
|
||||
|
||||
# Build helper variables for extra choices.
|
||||
extra_opts = tuple(c.long for c in extra_choices)
|
||||
extra_actions = tuple(c.short for c in extra_choices)
|
||||
|
||||
# Zero candidates.
|
||||
if not candidates:
|
||||
if singleton:
|
||||
|
|
@ -502,7 +518,7 @@ def choose_candidate(candidates, singleton, rec, cur_artist=None,
|
|||
'http://beets.readthedocs.org/en/latest/faq.html#nomatch')
|
||||
opts = ('Use as-is', 'as Tracks', 'Group albums', 'Skip',
|
||||
'Enter search', 'enter Id', 'aBort')
|
||||
sel = ui.input_options(opts)
|
||||
sel = ui.input_options(opts + extra_opts)
|
||||
if sel == 'u':
|
||||
return importer.action.ASIS
|
||||
elif sel == 't':
|
||||
|
|
@ -518,6 +534,8 @@ def choose_candidate(candidates, singleton, rec, cur_artist=None,
|
|||
return importer.action.MANUAL_ID
|
||||
elif sel == 'g':
|
||||
return importer.action.ALBUMS
|
||||
elif sel in extra_actions:
|
||||
return sel
|
||||
else:
|
||||
assert False
|
||||
|
||||
|
|
@ -571,7 +589,8 @@ def choose_candidate(candidates, singleton, rec, cur_artist=None,
|
|||
else:
|
||||
opts = ('Skip', 'Use as-is', 'as Tracks', 'Group albums',
|
||||
'Enter search', 'enter Id', 'aBort')
|
||||
sel = ui.input_options(opts, numrange=(1, len(candidates)))
|
||||
sel = ui.input_options(opts + extra_opts,
|
||||
numrange=(1, len(candidates)))
|
||||
if sel == 's':
|
||||
return importer.action.SKIP
|
||||
elif sel == 'u':
|
||||
|
|
@ -589,6 +608,8 @@ def choose_candidate(candidates, singleton, rec, cur_artist=None,
|
|||
return importer.action.MANUAL_ID
|
||||
elif sel == 'g':
|
||||
return importer.action.ALBUMS
|
||||
elif sel in extra_actions:
|
||||
return sel
|
||||
else: # Numerical selection.
|
||||
match = candidates[sel - 1]
|
||||
if sel != 1:
|
||||
|
|
@ -623,7 +644,8 @@ def choose_candidate(candidates, singleton, rec, cur_artist=None,
|
|||
})
|
||||
if default is None:
|
||||
require = True
|
||||
sel = ui.input_options(opts, require=require, default=default)
|
||||
sel = ui.input_options(opts + extra_opts, require=require,
|
||||
default=default)
|
||||
if sel == 'a':
|
||||
return match
|
||||
elif sel == 'g':
|
||||
|
|
@ -641,6 +663,8 @@ def choose_candidate(candidates, singleton, rec, cur_artist=None,
|
|||
raise importer.ImportAbort()
|
||||
elif sel == 'i':
|
||||
return importer.action.MANUAL_ID
|
||||
elif sel in extra_actions:
|
||||
return sel
|
||||
|
||||
|
||||
def manual_search(singleton):
|
||||
|
|
@ -684,10 +708,14 @@ class TerminalImportSession(importer.ImportSession):
|
|||
# Loop until we have a choice.
|
||||
candidates, rec = task.candidates, task.rec
|
||||
while True:
|
||||
# Gather extra choices from plugins.
|
||||
extra_choices = self._get_plugin_choices(task)
|
||||
extra_ops = {c.short: c.callback for c in extra_choices}
|
||||
|
||||
# Ask for a choice from the user.
|
||||
choice = choose_candidate(
|
||||
candidates, False, rec, task.cur_artist, task.cur_album,
|
||||
itemcount=len(task.items)
|
||||
itemcount=len(task.items), extra_choices=extra_choices
|
||||
)
|
||||
|
||||
# Choose which tags to use.
|
||||
|
|
@ -708,6 +736,12 @@ class TerminalImportSession(importer.ImportSession):
|
|||
_, _, candidates, rec = autotag.tag_album(
|
||||
task.items, search_id=search_id
|
||||
)
|
||||
elif choice in 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):
|
||||
# MANUAL and MANUAL_ID have no effect, even if returned.
|
||||
return post_choice
|
||||
else:
|
||||
# We have a candidate! Finish tagging. Here, choice is an
|
||||
# AlbumMatch object.
|
||||
|
|
@ -732,8 +766,12 @@ class TerminalImportSession(importer.ImportSession):
|
|||
return action
|
||||
|
||||
while True:
|
||||
extra_choices = self._get_plugin_choices(task)
|
||||
extra_ops = {c.short: c.callback for c in extra_choices}
|
||||
|
||||
# Ask for a choice.
|
||||
choice = choose_candidate(candidates, True, rec, item=task.item)
|
||||
choice = choose_candidate(candidates, True, rec, item=task.item,
|
||||
extra_choices=extra_choices)
|
||||
|
||||
if choice in (importer.action.SKIP, importer.action.ASIS):
|
||||
return choice
|
||||
|
|
@ -750,6 +788,12 @@ class TerminalImportSession(importer.ImportSession):
|
|||
if search_id:
|
||||
candidates, rec = autotag.tag_item(task.item,
|
||||
search_id=search_id)
|
||||
elif choice in 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):
|
||||
# MANUAL and MANUAL_ID have no effect, even if returned.
|
||||
return post_choice
|
||||
else:
|
||||
# Chose a candidate.
|
||||
assert isinstance(choice, autotag.TrackMatch)
|
||||
|
|
@ -801,6 +845,48 @@ class TerminalImportSession(importer.ImportSession):
|
|||
"was interrupted. Resume (Y/n)?"
|
||||
.format(displayable_path(path)))
|
||||
|
||||
def _get_plugin_choices(self, task):
|
||||
"""Get the extra choices appended to the plugins to the ui prompt.
|
||||
|
||||
The `before_choose_candidate` event is sent to the plugins, with
|
||||
session and task as its parameters. Plugins are responsible for
|
||||
checking the right conditions and returning a list of `PromptChoice`s,
|
||||
which is flattened and checked for conflicts.
|
||||
|
||||
Raises `ValueError` if two of the choices have the same short letter.
|
||||
|
||||
Returns a list of `PromptChoice`s.
|
||||
"""
|
||||
# Send the before_choose_candidate event and flatten list.
|
||||
extra_choices = list(chain(*plugins.send('before_choose_candidate',
|
||||
session=self, task=task)))
|
||||
# Take into account default options, for duplicate checking.
|
||||
all_choices = [PromptChoice('a', 'Apply', None),
|
||||
PromptChoice('s', 'Skip', None),
|
||||
PromptChoice('u', 'Use as-is', None),
|
||||
PromptChoice('t', 'as Tracks', None),
|
||||
PromptChoice('g', 'Group albums', None),
|
||||
PromptChoice('e', 'Enter search', None),
|
||||
PromptChoice('i', 'enter Id', None),
|
||||
PromptChoice('b', 'aBort', None)] +\
|
||||
extra_choices
|
||||
|
||||
short_letters = [c.short for c in all_choices]
|
||||
if len(short_letters) != len(set(short_letters)):
|
||||
# Duplicate short letter has been found.
|
||||
duplicates = [i for i, count in Counter(short_letters).items()
|
||||
if count > 1]
|
||||
for short in duplicates:
|
||||
# Keep the first of the choices, removing the rest.
|
||||
dup_choices = [c for c in all_choices if c.short == short]
|
||||
for c in dup_choices[1:]:
|
||||
log.warn(u"Prompt choice '{0}' removed due to conflict "
|
||||
u"with '{1}' (short letter: '{2}')",
|
||||
c.long, dup_choices[0].long, c.short)
|
||||
extra_choices.remove(c)
|
||||
return extra_choices
|
||||
|
||||
|
||||
# The import command.
|
||||
|
||||
|
||||
|
|
|
|||
55
beetsplug/mbsubmit.py
Normal file
55
beetsplug/mbsubmit.py
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# This file is part of beets.
|
||||
# Copyright 2015, Adrian Sampson and Diego Moreda.
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining
|
||||
# a copy of this software and associated documentation files (the
|
||||
# "Software"), to deal in the Software without restriction, including
|
||||
# without limitation the rights to use, copy, modify, merge, publish,
|
||||
# distribute, sublicense, and/or sell copies of the Software, and to
|
||||
# permit persons to whom the Software is furnished to do so, subject to
|
||||
# the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be
|
||||
# included in all copies or substantial portions of the Software.
|
||||
|
||||
"""Aid in submitting information to MusicBrainz.
|
||||
|
||||
This plugin allows the user to print track information in a format that is
|
||||
parseable by the MusicBrainz track parser. Programmatic submitting is not
|
||||
implemented by MusicBrainz yet.
|
||||
"""
|
||||
|
||||
from __future__ import (division, absolute_import, print_function,
|
||||
unicode_literals)
|
||||
|
||||
|
||||
from beets.autotag import Recommendation
|
||||
from beets.importer import action
|
||||
from beets.plugins import BeetsPlugin
|
||||
from beets.ui.commands import PromptChoice
|
||||
from beetsplug.info import print_data
|
||||
|
||||
|
||||
class MBSubmitPlugin(BeetsPlugin):
|
||||
def __init__(self):
|
||||
super(MBSubmitPlugin, self).__init__()
|
||||
|
||||
self.register_listener('before_choose_candidate',
|
||||
self.before_choose_candidate_event)
|
||||
|
||||
def before_choose_candidate_event(self, session, task):
|
||||
if not task.candidates or task.rec == Recommendation.none:
|
||||
return [PromptChoice('p', 'Print tracks', self.print_tracks),
|
||||
PromptChoice('k', 'print tracks and sKip',
|
||||
self.print_tracks_and_skip)]
|
||||
|
||||
# Callbacks for choices.
|
||||
def print_tracks(self, session, task):
|
||||
for i in task.items:
|
||||
print_data(None, i, '$track. $artist - $title ($length)')
|
||||
|
||||
def print_tracks_and_skip(self, session, task):
|
||||
for i in task.items:
|
||||
print_data(None, i, '$track. $artist - $title ($length)')
|
||||
return action.SKIP
|
||||
|
|
@ -227,12 +227,17 @@ The events currently available are:
|
|||
of a ``TrackInfo``.
|
||||
Parameter: ``info``.
|
||||
|
||||
* `before_choose_candidate`: called before the user is prompted for a decision
|
||||
during a ``beet import`` interactive session. Plugins can use this event for
|
||||
:ref:`appending choices to the prompt <append_prompt_choices>` by returning a
|
||||
list of ``PromptChoices``. Parameters: ``task`` and ``session``.
|
||||
|
||||
The included ``mpdupdate`` plugin provides an example use case for event listeners.
|
||||
|
||||
Extend the Autotagger
|
||||
^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
Plugins in can also enhance the functionality of the autotagger. For a
|
||||
Plugins can also enhance the functionality of the autotagger. For a
|
||||
comprehensive example, try looking at the ``chroma`` plugin, which is included
|
||||
with beets.
|
||||
|
||||
|
|
@ -528,3 +533,64 @@ command and an import stage, but the command needs to print more messages than
|
|||
the import stage. (For example, you'll want to log "found lyrics for this song"
|
||||
when you're run explicitly as a command, but you don't want to noisily
|
||||
interrupt the importer interface when running automatically.)
|
||||
|
||||
.. _append_prompt_choices:
|
||||
|
||||
Append Prompt Choices
|
||||
^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
Plugins can also append choices to the prompt presented to the user during
|
||||
an import session.
|
||||
|
||||
To do so, add a listener for the ``before_choose_candidate`` event, and return
|
||||
a list of ``PromptChoices`` that represent the additional choices that your
|
||||
plugin shall expose to the user::
|
||||
|
||||
from beets.plugins import BeetsPlugin
|
||||
from beets.ui.commands import PromptChoice
|
||||
|
||||
class ExamplePlugin(BeetsPlugin):
|
||||
def __init__(self):
|
||||
super(ExamplePlugin, self).__init__()
|
||||
self.register_listener('before_choose_candidate',
|
||||
self.before_choose_candidate_event)
|
||||
|
||||
def before_choose_candidate_event(self, session, task):
|
||||
return [PromptChoice('p', 'Print foo', self.foo),
|
||||
PromptChoice('d', 'Do bar', self.bar)]
|
||||
|
||||
def foo(self, session, task):
|
||||
print('User has chosen "Print foo"!')
|
||||
|
||||
def bar(self, session, task):
|
||||
print('User has chosen "Do bar"!')
|
||||
|
||||
The previous example modifies the standard prompt::
|
||||
|
||||
# selection (default 1), Skip, Use as-is, as Tracks, Group albums,
|
||||
Enter search, enter Id, aBort?
|
||||
|
||||
by appending two additional options (``Print foo`` and ``Do bar``)::
|
||||
|
||||
# selection (default 1), Skip, Use as-is, as Tracks, Group albums,
|
||||
Enter search, enter Id, aBort, Print foo, Do bar?
|
||||
|
||||
If the user selects a choice, the ``callback`` attribute of the corresponding
|
||||
``PromptChoice`` will be called. It is the responsibility of the plugin to
|
||||
check for the status of the import session and decide the choices to be
|
||||
appended: for example, if a particular choice should only be presented if the
|
||||
album has no candidates, the relevant checks against ``task.candidates`` should
|
||||
be performed inside the plugin's ``before_choose_candidate_event`` accordingly.
|
||||
|
||||
Please make sure that the short letter for each of the choices provided by the
|
||||
plugin is not already in use: the importer will raise an exception if two or
|
||||
more choices try to use the same short letter. As a reference, the following
|
||||
characters are used by the choices on the core importer prompt, and hence
|
||||
should not be used: ``a``, ``s``, ``u``, ``t``, ``g``, ``e``, ``i``, ``b``.
|
||||
|
||||
Additionally, the callback function can optionally specify the next action to
|
||||
be performed by returning one of the values from ``importer.action``, which
|
||||
will be passed to the main loop upon the callback has been processed. Note that
|
||||
``action.MANUAL`` and ``action.MANUAL_ID`` will have no effect even if
|
||||
returned by the callback, due to the current architecture of the import
|
||||
process.
|
||||
|
|
|
|||
|
|
@ -17,17 +17,18 @@ from __future__ import (division, absolute_import, print_function,
|
|||
unicode_literals)
|
||||
|
||||
import os
|
||||
from mock import patch, Mock
|
||||
from mock import patch, Mock, ANY
|
||||
import shutil
|
||||
import itertools
|
||||
|
||||
from beets.importer import SingletonImportTask, SentinelImportTask, \
|
||||
ArchiveImportTask
|
||||
from beets import plugins, config
|
||||
ArchiveImportTask, action
|
||||
from beets import plugins, config, ui
|
||||
from beets.library import Item
|
||||
from beets.dbcore import types
|
||||
from beets.mediafile import MediaFile
|
||||
from test.test_importer import ImportHelper
|
||||
from test.test_importer import ImportHelper, AutotagStub
|
||||
from test.test_ui_importer import TerminalImportSessionSetup
|
||||
from test._common import unittest, RSRC
|
||||
from test import helper
|
||||
|
||||
|
|
@ -401,6 +402,155 @@ class ListenersTest(unittest.TestCase, TestHelper):
|
|||
plugins.send('event9', foo=5)
|
||||
|
||||
|
||||
class PromptChoicesTest(TerminalImportSessionSetup, unittest.TestCase,
|
||||
ImportHelper, TestHelper):
|
||||
def setUp(self):
|
||||
self.setup_plugin_loader()
|
||||
self.setup_beets()
|
||||
self._create_import_dir(3)
|
||||
self._setup_import_session()
|
||||
self.matcher = AutotagStub().install()
|
||||
# keep track of ui.input_option() calls
|
||||
self.input_options_patcher = patch('beets.ui.input_options',
|
||||
side_effect=ui.input_options)
|
||||
self.mock_input_options = self.input_options_patcher.start()
|
||||
|
||||
def tearDown(self):
|
||||
self.input_options_patcher.stop()
|
||||
self.teardown_plugin_loader()
|
||||
self.teardown_beets()
|
||||
|
||||
def test_plugin_choices_in_ui_input_options_album(self):
|
||||
"""Test the presence of plugin choices on the prompt (album)."""
|
||||
class DummyPlugin(plugins.BeetsPlugin):
|
||||
def __init__(self):
|
||||
super(DummyPlugin, self).__init__()
|
||||
self.register_listener('before_choose_candidate',
|
||||
self.return_choices)
|
||||
|
||||
def return_choices(self, session, task):
|
||||
return [ui.commands.PromptChoice('f', 'Foo', None),
|
||||
ui.commands.PromptChoice('r', 'baR', None)]
|
||||
|
||||
self.register_plugin(DummyPlugin)
|
||||
# Default options + extra choices by the plugin ('Foo', 'Bar')
|
||||
opts = (u'Apply', u'More candidates', u'Skip', u'Use as-is',
|
||||
u'as Tracks', u'Group albums', u'Enter search',
|
||||
u'enter Id', u'aBort') + ('Foo', 'baR')
|
||||
|
||||
self.importer.add_choice(action.SKIP)
|
||||
self.importer.run()
|
||||
self.mock_input_options.assert_called_once_with(opts, default='a',
|
||||
require=ANY)
|
||||
|
||||
def test_plugin_choices_in_ui_input_options_singleton(self):
|
||||
"""Test the presence of plugin choices on the prompt (singleton)."""
|
||||
class DummyPlugin(plugins.BeetsPlugin):
|
||||
def __init__(self):
|
||||
super(DummyPlugin, self).__init__()
|
||||
self.register_listener('before_choose_candidate',
|
||||
self.return_choices)
|
||||
|
||||
def return_choices(self, session, task):
|
||||
return [ui.commands.PromptChoice('f', 'Foo', None),
|
||||
ui.commands.PromptChoice('r', 'baR', None)]
|
||||
|
||||
self.register_plugin(DummyPlugin)
|
||||
# Default options + extra choices by the plugin ('Foo', 'Bar')
|
||||
opts = (u'Apply', u'More candidates', u'Skip', u'Use as-is',
|
||||
u'Enter search',
|
||||
u'enter Id', u'aBort') + ('Foo', 'baR')
|
||||
|
||||
config['import']['singletons'] = True
|
||||
self.importer.add_choice(action.SKIP)
|
||||
self.importer.run()
|
||||
self.mock_input_options.assert_called_with(opts, default='a',
|
||||
require=ANY)
|
||||
|
||||
def test_choices_conflicts(self):
|
||||
"""Test the short letter conflict solving."""
|
||||
class DummyPlugin(plugins.BeetsPlugin):
|
||||
def __init__(self):
|
||||
super(DummyPlugin, self).__init__()
|
||||
self.register_listener('before_choose_candidate',
|
||||
self.return_choices)
|
||||
|
||||
def return_choices(self, session, task):
|
||||
return [ui.commands.PromptChoice('a', 'A foo', None), # dupe
|
||||
ui.commands.PromptChoice('z', 'baZ', None), # ok
|
||||
ui.commands.PromptChoice('z', 'Zupe', None), # dupe
|
||||
ui.commands.PromptChoice('z', 'Zoo', None)] # dupe
|
||||
|
||||
self.register_plugin(DummyPlugin)
|
||||
# Default options + not dupe extra choices by the plugin ('baZ')
|
||||
opts = (u'Apply', u'More candidates', u'Skip', u'Use as-is',
|
||||
u'as Tracks', u'Group albums', u'Enter search',
|
||||
u'enter Id', u'aBort') + ('baZ',)
|
||||
self.importer.add_choice(action.SKIP)
|
||||
self.importer.run()
|
||||
self.mock_input_options.assert_called_once_with(opts, default='a',
|
||||
require=ANY)
|
||||
|
||||
def test_plugin_callback(self):
|
||||
"""Test that plugin callbacks are being called upon user choice."""
|
||||
class DummyPlugin(plugins.BeetsPlugin):
|
||||
def __init__(self):
|
||||
super(DummyPlugin, self).__init__()
|
||||
self.register_listener('before_choose_candidate',
|
||||
self.return_choices)
|
||||
|
||||
def return_choices(self, session, task):
|
||||
return [ui.commands.PromptChoice('f', 'Foo', self.foo)]
|
||||
|
||||
def foo(self, session, task):
|
||||
pass
|
||||
|
||||
self.register_plugin(DummyPlugin)
|
||||
# Default options + extra choices by the plugin ('Foo', 'Bar')
|
||||
opts = (u'Apply', u'More candidates', u'Skip', u'Use as-is',
|
||||
u'as Tracks', u'Group albums', u'Enter search',
|
||||
u'enter Id', u'aBort') + ('Foo',)
|
||||
|
||||
# DummyPlugin.foo() should be called once
|
||||
with patch.object(DummyPlugin, 'foo', autospec=True) as mock_foo:
|
||||
with helper.control_stdin('\n'.join(['f', 's'])):
|
||||
self.importer.run()
|
||||
self.assertEqual(mock_foo.call_count, 1)
|
||||
|
||||
# input_options should be called twice, as foo() returns None
|
||||
self.assertEqual(self.mock_input_options.call_count, 2)
|
||||
self.mock_input_options.assert_called_with(opts, default='a',
|
||||
require=ANY)
|
||||
|
||||
def test_plugin_callback_return(self):
|
||||
"""Test that plugin callbacks that return a value exit the loop."""
|
||||
class DummyPlugin(plugins.BeetsPlugin):
|
||||
def __init__(self):
|
||||
super(DummyPlugin, self).__init__()
|
||||
self.register_listener('before_choose_candidate',
|
||||
self.return_choices)
|
||||
|
||||
def return_choices(self, session, task):
|
||||
return [ui.commands.PromptChoice('f', 'Foo', self.foo)]
|
||||
|
||||
def foo(self, session, task):
|
||||
return action.SKIP
|
||||
|
||||
self.register_plugin(DummyPlugin)
|
||||
# Default options + extra choices by the plugin ('Foo', 'Bar')
|
||||
opts = (u'Apply', u'More candidates', u'Skip', u'Use as-is',
|
||||
u'as Tracks', u'Group albums', u'Enter search',
|
||||
u'enter Id', u'aBort') + ('Foo',)
|
||||
|
||||
# DummyPlugin.foo() should be called once
|
||||
with helper.control_stdin('f\n'):
|
||||
self.importer.run()
|
||||
|
||||
# input_options should be called once, as foo() returns SKIP
|
||||
self.mock_input_options.assert_called_once_with(opts, default='a',
|
||||
require=ANY)
|
||||
|
||||
|
||||
def suite():
|
||||
return unittest.TestLoader().loadTestsFromName(__name__)
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue