Rev 6215: (jelmer) improvements to the interactive text ui framework, in file:///srv/pqm.bazaar-vcs.org/archives/thelove/bzr/%2Btrunk/
Patch Queue Manager
pqm at pqm.ubuntu.com
Fri Oct 14 13:19:36 UTC 2011
At file:///srv/pqm.bazaar-vcs.org/archives/thelove/bzr/%2Btrunk/
------------------------------------------------------------
revno: 6215 [merge]
revision-id: pqm at pqm.ubuntu.com-20111014131935-bcj8kpy7tr9b7rsy
parent: pqm at pqm.ubuntu.com-20111014111425-c7nzqujggvlsd9zz
parent: benoit.pierre at gmail.com-20111010212150-784qmc0ev60wcnyo
committer: Patch Queue Manager <pqm at pqm.ubuntu.com>
branch nick: +trunk
timestamp: Fri 2011-10-14 13:19:35 +0000
message:
(jelmer) improvements to the interactive text ui framework,
and make shelf and break-lock use it (Benoit PIERRE)
modified:
bzrlib/help_topics/__init__.py help_topics.py-20060920210027-rnim90q9e0bwxvy4-1
bzrlib/shelf_ui.py shelver.py-20081005210102-33worgzwrtdw0yrm-1
bzrlib/tests/blackbox/test_uncommit.py test_uncommit.py-20051027212835-84944b63adae51be
bzrlib/tests/test_script.py test_script.py-20090901081156-y90z4w2t62fv7e7b-1
bzrlib/tests/test_shelf_ui.py test_shelf_ui.py-20081027155203-wtcuazg85wp9u4fv-1
bzrlib/tests/test_ui.py test_ui.py-20051130162854-458e667a7414af09
bzrlib/ui/__init__.py ui.py-20050824083933-8cf663c763ba53a9
bzrlib/ui/text.py text.py-20051130153916-2e438cffc8afc478
=== modified file 'bzrlib/help_topics/__init__.py'
--- a/bzrlib/help_topics/__init__.py 2011-09-29 13:54:10 +0000
+++ b/bzrlib/help_topics/__init__.py 2011-10-10 20:15:02 +0000
@@ -622,6 +622,8 @@
BZR_PDB Control whether to launch a debugger on error.
BZR_SIGQUIT_PDB Control whether SIGQUIT behaves normally or invokes a
breakin debugger.
+BZR_TEXTUI_INPUT Force console input mode for prompts to line-based (instead
+ of char-based).
=================== ===========================================================
"""
=== modified file 'bzrlib/shelf_ui.py'
--- a/bzrlib/shelf_ui.py 2011-09-28 14:49:44 +0000
+++ b/bzrlib/shelf_ui.py 2011-10-05 18:40:53 +0000
@@ -252,46 +252,10 @@
diff_file.seek(0)
return patches.parse_patch(diff_file)
- def _char_based(self):
- # FIXME: A bit hackish to use INSIDE_EMACS here, but there is another
- # work in progress moving this method (and more importantly prompt()
- # below) into the ui area and address the issue in better ways.
- # -- vila 2011-09-28
- return os.environ.get('INSIDE_EMACS', None) is None
-
- def prompt(self, message):
- """Prompt the user for a character.
-
- :param message: The message to prompt a user with.
- :return: A character.
- """
- char_based = self._char_based()
- if char_based and not sys.stdin.isatty():
- # Since there is no controlling terminal we will hang when
- # trying to prompt the user, better abort now. See
- # https://code.launchpad.net/~bialix/bzr/shelve-no-tty/+merge/14905
- # for more context.
- raise errors.BzrError(gettext("You need a controlling terminal."))
- sys.stdout.write(message)
- if char_based:
- # We peek one char at a time which requires a real term here
- char = osutils.getchar()
- else:
- # While running tests (or under emacs) the input is line buffered
- # so we must not use osutils.getchar(). Instead we switch to a mode
- # where each line is terminated by a new line
- line = sys.stdin.readline()
- if line:
- # XXX: Warn if more than one char is typed ?
- char = line[0]
- else:
- # Empty input, callers handle it as enter
- char = ''
- sys.stdout.write("\r" + ' ' * len(message) + '\r')
- sys.stdout.flush()
- return char
-
- def prompt_bool(self, question, long=False, allow_editor=False):
+ def prompt(self, message, choices, default):
+ return ui.ui_factory.choose(message, choices, default=default)
+
+ def prompt_bool(self, question, allow_editor=False):
"""Prompt the user with a yes/no question.
This may be overridden by self.auto. It may also *set* self.auto. It
@@ -301,16 +265,19 @@
"""
if self.auto:
return True
- editor_string = ''
- if long:
- if allow_editor:
- editor_string = '(E)dit manually, '
- prompt = ' [(y)es, (N)o, %s(f)inish, or (q)uit]' % editor_string
+ alternatives_chars = 'yn'
+ alternatives = '&yes\n&No'
+ if allow_editor:
+ alternatives_chars += 'e'
+ alternatives += '\n&edit manually'
+ alternatives_chars += 'fq'
+ alternatives += '\n&finish\n&quit'
+ choice = self.prompt(question, alternatives, 1)
+ if choice is None:
+ # EOF.
+ char = 'n'
else:
- if allow_editor:
- editor_string = 'e'
- prompt = ' [yN%sfq?]' % editor_string
- char = self.prompt(question + prompt)
+ char = alternatives_chars[choice]
if char == 'y':
return True
elif char == 'e' and allow_editor:
@@ -318,8 +285,6 @@
elif char == 'f':
self.auto = True
return True
- elif char == '?':
- return self.prompt_bool(question, long=True)
if char == 'q':
raise errors.UserAbort()
else:
=== modified file 'bzrlib/tests/blackbox/test_uncommit.py'
--- a/bzrlib/tests/blackbox/test_uncommit.py 2011-09-26 00:00:44 +0000
+++ b/bzrlib/tests/blackbox/test_uncommit.py 2011-10-05 22:14:33 +0000
@@ -72,7 +72,7 @@
$ bzr uncommit
...
The above revision(s) will be removed.
- 2>Uncommit these revisions? [y/n]:
+ 2>Uncommit these revisions? ([y]es, [n]o): no
<n
Canceled
""")
=== modified file 'bzrlib/tests/test_script.py'
--- a/bzrlib/tests/test_script.py 2011-09-28 15:06:42 +0000
+++ b/bzrlib/tests/test_script.py 2011-10-05 22:15:03 +0000
@@ -580,12 +580,12 @@
self.addCleanup(commands.builtin_command_registry.remove, 'test-confirm')
self.run_script("""
$ bzr test-confirm
- 2>Really do it? [y/n]:
- <yes
+ 2>Really do it? ([y]es, [n]o): yes
+ <y
Do it!
$ bzr test-confirm
- 2>Really do it? [y/n]:
- <no
+ 2>Really do it? ([y]es, [n]o): no
+ <n
ok, no
""")
@@ -610,16 +610,14 @@
def test_shelve(self):
self.run_script("""
$ bzr shelve -m 'shelve bar'
- # Shelve? [yNfq?]
- <y
- # Shelve 1 change(s)? [yNfq?]
+ 2>Shelve? ([y]es, [N]o, [f]inish, [q]uit): yes
<y
2>Selected changes:
2> M file
+ 2>Shelve 1 change(s)? ([y]es, [N]o, [f]inish, [q]uit): yes
+ <y
2>Changes shelved with id "1".
""",
- # shelve uses \r that can't be represented in the
- # script ?
null_output_matches_anything=True)
self.run_script("""
$ bzr shelve --list
@@ -630,12 +628,9 @@
# We intentionally provide no input here to test EOF
self.run_script("""
$ bzr shelve -m 'shelve bar'
- # Shelve? [yNfq?]
- # Shelve 1 change(s)? [yNfq?]
+ 2>Shelve? ([y]es, [N]o, [f]inish, [q]uit):
2>No changes to shelve.
""",
- # shelve uses \r that can't be represented in the
- # script ?
null_output_matches_anything=True)
self.run_script("""
$ bzr st
=== modified file 'bzrlib/tests/test_shelf_ui.py'
--- a/bzrlib/tests/test_shelf_ui.py 2011-07-08 23:01:39 +0000
+++ b/bzrlib/tests/test_shelf_ui.py 2011-10-10 20:55:52 +0000
@@ -44,16 +44,18 @@
self.expected = []
self.diff_writer = StringIO()
- def expect(self, prompt, response):
- self.expected.append((prompt, response))
+ def expect(self, message, response):
+ self.expected.append((message, response))
- def prompt(self, message):
+ def prompt(self, message, choices, default):
try:
- prompt, response = self.expected.pop(0)
+ expected_message, response = self.expected.pop(0)
except IndexError:
raise AssertionError('Unexpected prompt: %s' % message)
- if prompt != message:
+ if message != expected_message:
raise AssertionError('Wrong prompt: %s' % message)
+ if choices != '&yes\n&No\n&finish\n&quit':
+ raise AssertionError('Wrong choices: %s' % choices)
return response
@@ -86,7 +88,7 @@
shelver = ExpectShelver(tree, tree.basis_tree())
self.addCleanup(shelver.finalize)
e = self.assertRaises(AssertionError, shelver.run)
- self.assertEqual('Unexpected prompt: Shelve? [yNfq?]', str(e))
+ self.assertEqual('Unexpected prompt: Shelve?', str(e))
def test_wrong_prompt_failure(self):
tree = self.create_shelvable_tree()
@@ -94,9 +96,9 @@
self.addCleanup(tree.unlock)
shelver = ExpectShelver(tree, tree.basis_tree())
self.addCleanup(shelver.finalize)
- shelver.expect('foo', 'y')
+ shelver.expect('foo', 0)
e = self.assertRaises(AssertionError, shelver.run)
- self.assertEqual('Wrong prompt: Shelve? [yNfq?]', str(e))
+ self.assertEqual('Wrong prompt: Shelve?', str(e))
def test_shelve_not_diff(self):
tree = self.create_shelvable_tree()
@@ -104,8 +106,8 @@
self.addCleanup(tree.unlock)
shelver = ExpectShelver(tree, tree.basis_tree())
self.addCleanup(shelver.finalize)
- shelver.expect('Shelve? [yNfq?]', 'n')
- shelver.expect('Shelve? [yNfq?]', 'n')
+ shelver.expect('Shelve?', 1)
+ shelver.expect('Shelve?', 1)
# No final shelving prompt because no changes were selected
shelver.run()
self.assertFileEqual(LINES_ZY, 'tree/foo')
@@ -116,9 +118,9 @@
self.addCleanup(tree.unlock)
shelver = ExpectShelver(tree, tree.basis_tree())
self.addCleanup(shelver.finalize)
- shelver.expect('Shelve? [yNfq?]', 'y')
- shelver.expect('Shelve? [yNfq?]', 'y')
- shelver.expect('Shelve 2 change(s)? [yNfq?]', 'n')
+ shelver.expect('Shelve?', 0)
+ shelver.expect('Shelve?', 0)
+ shelver.expect('Shelve 2 change(s)?', 1)
shelver.run()
self.assertFileEqual(LINES_ZY, 'tree/foo')
@@ -128,9 +130,9 @@
self.addCleanup(tree.unlock)
shelver = ExpectShelver(tree, tree.basis_tree())
self.addCleanup(shelver.finalize)
- shelver.expect('Shelve? [yNfq?]', 'y')
- shelver.expect('Shelve? [yNfq?]', 'y')
- shelver.expect('Shelve 2 change(s)? [yNfq?]', 'y')
+ shelver.expect('Shelve?', 0)
+ shelver.expect('Shelve?', 0)
+ shelver.expect('Shelve 2 change(s)?', 0)
shelver.run()
self.assertFileEqual(LINES_AJ, 'tree/foo')
@@ -140,9 +142,9 @@
self.addCleanup(tree.unlock)
shelver = ExpectShelver(tree, tree.basis_tree())
self.addCleanup(shelver.finalize)
- shelver.expect('Shelve? [yNfq?]', 'y')
- shelver.expect('Shelve? [yNfq?]', 'n')
- shelver.expect('Shelve 1 change(s)? [yNfq?]', 'y')
+ shelver.expect('Shelve?', 0)
+ shelver.expect('Shelve?', 1)
+ shelver.expect('Shelve 1 change(s)?', 0)
shelver.run()
self.assertFileEqual(LINES_AY, 'tree/foo')
@@ -153,8 +155,8 @@
self.addCleanup(tree.unlock)
shelver = ExpectShelver(tree, tree.basis_tree())
self.addCleanup(shelver.finalize)
- shelver.expect('Shelve binary changes? [yNfq?]', 'y')
- shelver.expect('Shelve 1 change(s)? [yNfq?]', 'y')
+ shelver.expect('Shelve binary changes?', 0)
+ shelver.expect('Shelve 1 change(s)?', 0)
shelver.run()
self.assertFileEqual(LINES_AJ, 'tree/foo')
@@ -165,10 +167,10 @@
self.addCleanup(tree.unlock)
shelver = ExpectShelver(tree, tree.basis_tree())
self.addCleanup(shelver.finalize)
- shelver.expect('Shelve renaming "foo" => "bar"? [yNfq?]', 'y')
- shelver.expect('Shelve? [yNfq?]', 'y')
- shelver.expect('Shelve? [yNfq?]', 'y')
- shelver.expect('Shelve 3 change(s)? [yNfq?]', 'y')
+ shelver.expect('Shelve renaming "foo" => "bar"?', 0)
+ shelver.expect('Shelve?', 0)
+ shelver.expect('Shelve?', 0)
+ shelver.expect('Shelve 3 change(s)?', 0)
shelver.run()
self.assertFileEqual(LINES_AJ, 'tree/foo')
@@ -179,8 +181,8 @@
self.addCleanup(tree.unlock)
shelver = ExpectShelver(tree, tree.basis_tree())
self.addCleanup(shelver.finalize)
- shelver.expect('Shelve removing file "foo"? [yNfq?]', 'y')
- shelver.expect('Shelve 1 change(s)? [yNfq?]', 'y')
+ shelver.expect('Shelve removing file "foo"?', 0)
+ shelver.expect('Shelve 1 change(s)?', 0)
shelver.run()
self.assertFileEqual(LINES_AJ, 'tree/foo')
@@ -193,8 +195,8 @@
self.addCleanup(tree.unlock)
shelver = ExpectShelver(tree, tree.basis_tree())
self.addCleanup(shelver.finalize)
- shelver.expect('Shelve adding file "foo"? [yNfq?]', 'y')
- shelver.expect('Shelve 1 change(s)? [yNfq?]', 'y')
+ shelver.expect('Shelve adding file "foo"?', 0)
+ shelver.expect('Shelve 1 change(s)?', 0)
shelver.run()
self.assertPathDoesNotExist('tree/foo')
@@ -206,9 +208,9 @@
self.addCleanup(tree.unlock)
shelver = ExpectShelver(tree, tree.basis_tree())
self.addCleanup(shelver.finalize)
- shelver.expect('Shelve changing "foo" from file to directory? [yNfq?]',
- 'y')
- shelver.expect('Shelve 1 change(s)? [yNfq?]', 'y')
+ shelver.expect('Shelve changing "foo" from file to directory?',
+ 0)
+ shelver.expect('Shelve 1 change(s)?', 0)
def test_shelve_modify_target(self):
self.requireFeature(features.SymlinkFeature)
@@ -223,8 +225,8 @@
shelver = ExpectShelver(tree, tree.basis_tree())
self.addCleanup(shelver.finalize)
shelver.expect('Shelve changing target of "baz" from "bar" to '
- '"vax"? [yNfq?]', 'y')
- shelver.expect('Shelve 1 change(s)? [yNfq?]', 'y')
+ '"vax"?', 0)
+ shelver.expect('Shelve 1 change(s)?', 0)
shelver.run()
self.assertEqual('bar', os.readlink('tree/baz'))
@@ -234,8 +236,8 @@
self.addCleanup(tree.unlock)
shelver = ExpectShelver(tree, tree.basis_tree())
self.addCleanup(shelver.finalize)
- shelver.expect('Shelve? [yNfq?]', 'f')
- shelver.expect('Shelve 2 change(s)? [yNfq?]', 'y')
+ shelver.expect('Shelve?', 2)
+ shelver.expect('Shelve 2 change(s)?', 0)
shelver.run()
self.assertFileEqual(LINES_AJ, 'tree/foo')
@@ -245,7 +247,7 @@
self.addCleanup(tree.unlock)
shelver = ExpectShelver(tree, tree.basis_tree())
self.addCleanup(shelver.finalize)
- shelver.expect('Shelve? [yNfq?]', 'q')
+ shelver.expect('Shelve?', 3)
self.assertRaises(errors.UserAbort, shelver.run)
self.assertFileEqual(LINES_ZY, 'tree/foo')
@@ -267,19 +269,8 @@
self.addCleanup(tree.unlock)
shelver = ExpectShelver(tree, tree.basis_tree(), file_list=['bar'])
self.addCleanup(shelver.finalize)
- shelver.expect('Shelve adding file "bar"? [yNfq?]', 'y')
- shelver.expect('Shelve 1 change(s)? [yNfq?]', 'y')
- shelver.run()
-
- def test_shelve_help(self):
- tree = self.create_shelvable_tree()
- tree.lock_tree_write()
- self.addCleanup(tree.unlock)
- shelver = ExpectShelver(tree, tree.basis_tree())
- self.addCleanup(shelver.finalize)
- shelver.expect('Shelve? [yNfq?]', '?')
- shelver.expect('Shelve? [(y)es, (N)o, (f)inish, or (q)uit]', 'f')
- shelver.expect('Shelve 2 change(s)? [yNfq?]', 'y')
+ shelver.expect('Shelve adding file "bar"?', 0)
+ shelver.expect('Shelve 1 change(s)?', 0)
shelver.run()
def test_shelve_destroy(self):
@@ -350,8 +341,8 @@
shelver = ExpectShelver(tree, tree.basis_tree(),
reporter=shelf_ui.ApplyReporter())
self.addCleanup(shelver.finalize)
- shelver.expect('Apply change? [yNfq?]', 'n')
- shelver.expect('Apply change? [yNfq?]', 'n')
+ shelver.expect('Apply change?', 1)
+ shelver.expect('Apply change?', 1)
# No final shelving prompt because no changes were selected
shelver.run()
self.assertFileEqual(LINES_ZY, 'tree/foo')
@@ -363,9 +354,9 @@
shelver = ExpectShelver(tree, tree.basis_tree(),
reporter=shelf_ui.ApplyReporter())
self.addCleanup(shelver.finalize)
- shelver.expect('Apply change? [yNfq?]', 'y')
- shelver.expect('Apply change? [yNfq?]', 'y')
- shelver.expect('Apply 2 change(s)? [yNfq?]', 'n')
+ shelver.expect('Apply change?', 0)
+ shelver.expect('Apply change?', 0)
+ shelver.expect('Apply 2 change(s)?', 1)
shelver.run()
self.assertFileEqual(LINES_ZY, 'tree/foo')
@@ -376,9 +367,9 @@
shelver = ExpectShelver(tree, tree.basis_tree(),
reporter=shelf_ui.ApplyReporter())
self.addCleanup(shelver.finalize)
- shelver.expect('Apply change? [yNfq?]', 'y')
- shelver.expect('Apply change? [yNfq?]', 'y')
- shelver.expect('Apply 2 change(s)? [yNfq?]', 'y')
+ shelver.expect('Apply change?', 0)
+ shelver.expect('Apply change?', 0)
+ shelver.expect('Apply 2 change(s)?', 0)
shelver.run()
self.assertFileEqual(LINES_AJ, 'tree/foo')
@@ -390,8 +381,8 @@
shelver = ExpectShelver(tree, tree.basis_tree(),
reporter=shelf_ui.ApplyReporter())
self.addCleanup(shelver.finalize)
- shelver.expect('Apply binary changes? [yNfq?]', 'y')
- shelver.expect('Apply 1 change(s)? [yNfq?]', 'y')
+ shelver.expect('Apply binary changes?', 0)
+ shelver.expect('Apply 1 change(s)?', 0)
shelver.run()
self.assertFileEqual(LINES_AJ, 'tree/foo')
@@ -403,10 +394,10 @@
shelver = ExpectShelver(tree, tree.basis_tree(),
reporter=shelf_ui.ApplyReporter())
self.addCleanup(shelver.finalize)
- shelver.expect('Rename "bar" => "foo"? [yNfq?]', 'y')
- shelver.expect('Apply change? [yNfq?]', 'y')
- shelver.expect('Apply change? [yNfq?]', 'y')
- shelver.expect('Apply 3 change(s)? [yNfq?]', 'y')
+ shelver.expect('Rename "bar" => "foo"?', 0)
+ shelver.expect('Apply change?', 0)
+ shelver.expect('Apply change?', 0)
+ shelver.expect('Apply 3 change(s)?', 0)
shelver.run()
self.assertFileEqual(LINES_AJ, 'tree/foo')
@@ -418,8 +409,8 @@
shelver = ExpectShelver(tree, tree.basis_tree(),
reporter=shelf_ui.ApplyReporter())
self.addCleanup(shelver.finalize)
- shelver.expect('Add file "foo"? [yNfq?]', 'y')
- shelver.expect('Apply 1 change(s)? [yNfq?]', 'y')
+ shelver.expect('Add file "foo"?', 0)
+ shelver.expect('Apply 1 change(s)?', 0)
shelver.run()
self.assertFileEqual(LINES_AJ, 'tree/foo')
@@ -433,8 +424,8 @@
shelver = ExpectShelver(tree, tree.basis_tree(),
reporter=shelf_ui.ApplyReporter())
self.addCleanup(shelver.finalize)
- shelver.expect('Delete file "foo"? [yNfq?]', 'y')
- shelver.expect('Apply 1 change(s)? [yNfq?]', 'y')
+ shelver.expect('Delete file "foo"?', 0)
+ shelver.expect('Apply 1 change(s)?', 0)
shelver.run()
self.assertPathDoesNotExist('tree/foo')
@@ -447,8 +438,8 @@
shelver = ExpectShelver(tree, tree.basis_tree(),
reporter=shelf_ui.ApplyReporter())
self.addCleanup(shelver.finalize)
- shelver.expect('Change "foo" from directory to a file? [yNfq?]', 'y')
- shelver.expect('Apply 1 change(s)? [yNfq?]', 'y')
+ shelver.expect('Change "foo" from directory to a file?', 0)
+ shelver.expect('Apply 1 change(s)?', 0)
def test_shelve_modify_target(self):
self.requireFeature(features.SymlinkFeature)
@@ -463,9 +454,9 @@
shelver = ExpectShelver(tree, tree.basis_tree(),
reporter=shelf_ui.ApplyReporter())
self.addCleanup(shelver.finalize)
- shelver.expect('Change target of "baz" from "vax" to "bar"? [yNfq?]',
- 'y')
- shelver.expect('Apply 1 change(s)? [yNfq?]', 'y')
+ shelver.expect('Change target of "baz" from "vax" to "bar"?',
+ 0)
+ shelver.expect('Apply 1 change(s)?', 0)
shelver.run()
self.assertEqual('bar', os.readlink('tree/baz'))
=== modified file 'bzrlib/tests/test_ui.py'
--- a/bzrlib/tests/test_ui.py 2011-05-16 13:39:39 +0000
+++ b/bzrlib/tests/test_ui.py 2011-10-08 19:01:59 +0000
@@ -111,6 +111,8 @@
def test_text_ui_get_boolean(self):
stdin = tests.StringIOWrapper("y\n" # True
"n\n" # False
+ " \n y \n" # True
+ " no \n" # False
"yes with garbage\nY\n" # True
"not an answer\nno\n" # False
"I'm sure!\nyes\n" # True
@@ -125,9 +127,77 @@
self.assertEqual(False, factory.get_boolean(u""))
self.assertEqual(True, factory.get_boolean(u""))
self.assertEqual(False, factory.get_boolean(u""))
- self.assertEqual("foo\n", factory.stdin.read())
- # stdin should be empty
- self.assertEqual('', factory.stdin.readline())
+ self.assertEqual(True, factory.get_boolean(u""))
+ self.assertEqual(False, factory.get_boolean(u""))
+ self.assertEqual("foo\n", factory.stdin.read())
+ # stdin should be empty
+ self.assertEqual('', factory.stdin.readline())
+ # return false on EOF
+ self.assertEqual(False, factory.get_boolean(u""))
+
+ def test_text_ui_choose_bad_parameters(self):
+ stdin = tests.StringIOWrapper()
+ stdout = tests.StringIOWrapper()
+ stderr = tests.StringIOWrapper()
+ factory = _mod_ui_text.TextUIFactory(stdin, stdout, stderr)
+ # invalid default index
+ self.assertRaises(ValueError, factory.choose, u"", u"&Yes\n&No", 3)
+ # duplicated choice
+ self.assertRaises(ValueError, factory.choose, u"", u"&choice\n&ChOiCe")
+ # duplicated shortcut
+ self.assertRaises(ValueError, factory.choose, u"", u"&choice1\nchoi&ce2")
+
+ def test_text_ui_choose_prompt(self):
+ stdin = tests.StringIOWrapper()
+ stdout = tests.StringIOWrapper()
+ stderr = tests.StringIOWrapper()
+ factory = _mod_ui_text.TextUIFactory(stdin, stdout, stderr)
+ # choices with explicit shortcuts
+ factory.choose(u"prompt", u"&yes\n&No\nmore &info")
+ self.assertEqual("prompt ([y]es, [N]o, more [i]nfo): \n", factory.stderr.getvalue())
+ # automatic shortcuts
+ factory.stderr.truncate(0)
+ factory.choose(u"prompt", u"yes\nNo\nmore info")
+ self.assertEqual("prompt ([y]es, [N]o, [m]ore info): \n", factory.stderr.getvalue())
+
+ def test_text_ui_choose_return_values(self):
+ choose = lambda: factory.choose(u"", u"&Yes\n&No\nMaybe\nmore &info", 3)
+ stdin = tests.StringIOWrapper("y\n" # 0
+ "n\n" # 1
+ " \n" # default: 3
+ " no \n" # 1
+ "b\na\nd \n" # bad shortcuts, all ignored
+ "yes with garbage\nY\n" # 0
+ "not an answer\nno\n" # 1
+ "info\nmore info\n" # 3
+ "Maybe\n" # 2
+ "foo\n")
+ stdout = tests.StringIOWrapper()
+ stderr = tests.StringIOWrapper()
+ factory = _mod_ui_text.TextUIFactory(stdin, stdout, stderr)
+ self.assertEqual(0, choose())
+ self.assertEqual(1, choose())
+ self.assertEqual(3, choose())
+ self.assertEqual(1, choose())
+ self.assertEqual(0, choose())
+ self.assertEqual(1, choose())
+ self.assertEqual(3, choose())
+ self.assertEqual(2, choose())
+ self.assertEqual("foo\n", factory.stdin.read())
+ # stdin should be empty
+ self.assertEqual('', factory.stdin.readline())
+ # return None on EOF
+ self.assertEqual(None, choose())
+
+ def test_text_ui_choose_no_default(self):
+ stdin = tests.StringIOWrapper(" \n" # no default, invalid!
+ " yes \n" # 0
+ "foo\n")
+ stdout = tests.StringIOWrapper()
+ stderr = tests.StringIOWrapper()
+ factory = _mod_ui_text.TextUIFactory(stdin, stdout, stderr)
+ self.assertEqual(0, factory.choose(u"", u"&Yes\n&No"))
+ self.assertEqual("foo\n", factory.stdin.read())
def test_text_ui_get_integer(self):
stdin = tests.StringIOWrapper(
@@ -170,8 +240,8 @@
output = out.getvalue()
self.assertContainsRe(output,
"| foo *\r\r *\r*")
- self.assertContainsRe(output,
- r"what do you want\? \[y/n\]: what do you want\? \[y/n\]: ")
+ self.assertContainsString(output,
+ r"what do you want? ([y]es, [n]o): what do you want? ([y]es, [n]o): ")
# stdin should have been totally consumed
self.assertEqual('', factory.stdin.readline())
=== modified file 'bzrlib/ui/__init__.py'
--- a/bzrlib/ui/__init__.py 2011-09-19 08:28:03 +0000
+++ b/bzrlib/ui/__init__.py 2011-10-05 18:40:53 +0000
@@ -324,6 +324,26 @@
warnings.warn(fail) # so tests will fail etc
return fail
+ def choose(self, msg, choices, default=None):
+ """Prompt the user for a list of alternatives.
+
+ :param msg: message to be shown as part of the prompt.
+
+ :param choices: list of choices, with the individual choices separated
+ by '\n', e.g.: choose("Save changes?", "&Yes\n&No\n&Cancel"). The
+ letter after the '&' is the shortcut key for that choice. Thus you
+ can type 'c' to select "Cancel". Shorcuts are case insensitive.
+ The shortcut does not need to be the first letter. If a shorcut key
+ is not provided, the first letter for the choice will be used.
+
+ :param default: default choice (index), returned for example when enter
+ is pressed for the console version.
+
+ :return: the index fo the user choice (so '0', '1' or '2' for
+ respectively yes/no/cancel in the previous example).
+ """
+ raise NotImplementedError(self.choose)
+
def get_boolean(self, prompt):
"""Get a boolean question answered from the user.
@@ -331,7 +351,8 @@
line without terminating \\n.
:return: True or False for y/yes or n/no.
"""
- raise NotImplementedError(self.get_boolean)
+ choice = self.choose(prompt + '?', '&yes\n&no', default=None)
+ return 0 == choice
def get_integer(self, prompt):
"""Get an integer from the user.
=== modified file 'bzrlib/ui/text.py'
--- a/bzrlib/ui/text.py 2011-05-16 13:39:39 +0000
+++ b/bzrlib/ui/text.py 2011-10-10 21:21:50 +0000
@@ -43,6 +43,116 @@
)
+class _ChooseUI(object):
+
+ """ Helper class for choose implementation.
+ """
+
+ def __init__(self, ui, msg, choices, default):
+ self.ui = ui
+ self._setup_mode()
+ self._build_alternatives(msg, choices, default)
+
+ def _setup_mode(self):
+ """Setup input mode (line-based, char-based) and echo-back.
+
+ Line-based input is used if the BZR_TEXTUI_INPUT environment
+ variable is set to 'line-based', or if there is no controlling
+ terminal.
+ """
+ if os.environ.get('BZR_TEXTUI_INPUT') != 'line-based' and \
+ self.ui.stdin == sys.stdin and self.ui.stdin.isatty():
+ self.line_based = False
+ self.echo_back = True
+ else:
+ self.line_based = True
+ self.echo_back = not self.ui.stdin.isatty()
+
+ def _build_alternatives(self, msg, choices, default):
+ """Parse choices string.
+
+ Setup final prompt and the lists of choices and associated
+ shortcuts.
+ """
+ index = 0
+ help_list = []
+ self.alternatives = {}
+ choices = choices.split('\n')
+ if default is not None and default not in range(0, len(choices)):
+ raise ValueError("invalid default index")
+ for c in choices:
+ name = c.replace('&', '').lower()
+ choice = (name, index)
+ if name in self.alternatives:
+ raise ValueError("duplicated choice: %s" % name)
+ self.alternatives[name] = choice
+ shortcut = c.find('&')
+ if -1 != shortcut and (shortcut + 1) < len(c):
+ help = c[:shortcut]
+ help += '[' + c[shortcut + 1] + ']'
+ help += c[(shortcut + 2):]
+ shortcut = c[shortcut + 1]
+ else:
+ c = c.replace('&', '')
+ shortcut = c[0]
+ help = '[%s]%s' % (shortcut, c[1:])
+ shortcut = shortcut.lower()
+ if shortcut in self.alternatives:
+ raise ValueError("duplicated shortcut: %s" % shortcut)
+ self.alternatives[shortcut] = choice
+ # Add redirections for default.
+ if index == default:
+ self.alternatives[''] = choice
+ self.alternatives['\r'] = choice
+ help_list.append(help)
+ index += 1
+
+ self.prompt = u'%s (%s): ' % (msg, ', '.join(help_list))
+
+ def _getline(self):
+ line = self.ui.stdin.readline()
+ if '' == line:
+ raise EOFError
+ return line.strip()
+
+ def _getchar(self):
+ char = osutils.getchar()
+ if char == chr(3): # INTR
+ raise KeyboardInterrupt
+ if char == chr(4): # EOF (^d, C-d)
+ raise EOFError
+ return char
+
+ def interact(self):
+ """Keep asking the user until a valid choice is made.
+ """
+ if self.line_based:
+ getchoice = self._getline
+ else:
+ getchoice = self._getchar
+ iter = 0
+ while True:
+ iter += 1
+ if 1 == iter or self.line_based:
+ self.ui.prompt(self.prompt)
+ try:
+ choice = getchoice()
+ except EOFError:
+ self.ui.stderr.write('\n')
+ return None
+ except KeyboardInterrupt:
+ self.ui.stderr.write('\n')
+ raise KeyboardInterrupt
+ choice = choice.lower()
+ if choice not in self.alternatives:
+ # Not a valid choice, keep on asking.
+ continue
+ name, index = self.alternatives[choice]
+ if self.echo_back:
+ self.ui.stderr.write(name + '\n')
+ return index
+
+
class TextUIFactory(UIFactory):
"""A UI factory for Text user interefaces."""
@@ -61,6 +171,27 @@
# paints progress, network activity, etc
self._progress_view = self.make_progress_view()
+ def choose(self, msg, choices, default=None):
+ """Prompt the user for a list of alternatives.
+
+ Support both line-based and char-based editing.
+
+ In line-based mode, both the shortcut and full choice name are valid
+ answers, e.g. for choose('prompt', '&yes\n&no'): 'y', ' Y ', ' yes',
+ 'YES ' are all valid input lines for choosing 'yes'.
+
+ An empty line, when in line-based mode, or pressing enter in char-based
+ mode will select the default choice (if any).
+
+ Choice is echoed back if:
+ - input is char-based; which means a controlling terminal is available,
+ and osutils.getchar is used
+ - input is line-based, and no controlling terminal is available
+ """
+
+ choose_ui = _ChooseUI(self, msg, choices, default)
+ return choose_ui.interact()
+
def be_quiet(self, state):
if state and not self._quiet:
self.clear_term()
@@ -78,18 +209,6 @@
# to clear it. We might need to separately check for the case of
self._progress_view.clear()
- def get_boolean(self, prompt):
- while True:
- self.prompt(prompt + "? [y/n]: ")
- line = self.stdin.readline().lower()
- if line in ('y\n', 'yes\n'):
- return True
- elif line in ('n\n', 'no\n'):
- return False
- elif line in ('', None):
- # end-of-file; possibly should raise an error here instead
- return None
-
def get_integer(self, prompt):
while True:
self.prompt(prompt)
@@ -205,6 +324,7 @@
prompt = prompt % kwargs
prompt = prompt.encode(osutils.get_terminal_encoding(), 'replace')
self.clear_term()
+ self.stdout.flush()
self.stderr.write(prompt)
def report_transport_activity(self, transport, byte_count, direction):
More information about the bazaar-commits
mailing list