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