Repository: python-cmd2/cmd2
Pull request #754 (2019-08-07 22:00:45)

Disabled tab completion during a select call
Disabled tab completion during a select call.
Added graceful handling of Ctrl-D during select. It now reprompts the user.


Patch:
diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py
index d82e58c9..a7cd8fac 100755
--- a/cmd2/cmd2.py
+++ b/cmd2/cmd2.py
@@ -684,7 +684,7 @@ class Cmd(cmd.Cmd):
             final_msg = ansi.style_error(final_msg)
 
         if not self.debug:
-            warning = "\nTo enable full traceback, run the following command:  'set debug true'"
+            warning = "\nTo enable full traceback, run the following command: 'set debug true'"
             final_msg += ansi.style_warning(warning)
 
         # Set apply_style to False since style has already been applied
@@ -2890,6 +2890,28 @@ class Cmd(cmd.Cmd):
              | a list of tuples -> interpreted as (value, text), so
                                    that the return value can differ from
                                    the text advertised to the user """
+
+        completion_disabled = False
+        orig_completer = None
+
+        def disable_completion():
+            """Turn off completion during the select input line"""
+            nonlocal orig_completer
+            nonlocal completion_disabled
+
+            if rl_type != RlType.NONE and not completion_disabled:
+                orig_completer = readline.get_completer()
+                readline.set_completer(lambda *args, **kwargs: None)
+                completion_disabled = True
+
+        def enable_completion():
+            """Restore tab completion when select is done reading input"""
+            nonlocal completion_disabled
+
+            if rl_type != RlType.NONE and completion_disabled:
+                readline.set_completer(orig_completer)
+                completion_disabled = False
+
         local_opts = opts
         if isinstance(opts, str):
             local_opts = list(zip(opts.split(), opts.split()))
@@ -2904,15 +2926,28 @@ class Cmd(cmd.Cmd):
                     fulloptions.append((opt[0], opt[0]))
         for (idx, (_, text)) in enumerate(fulloptions):
             self.poutput('  %2d. %s' % (idx + 1, text))
+
         while True:
             safe_prompt = rl_make_safe_prompt(prompt)
-            response = input(safe_prompt)
+
+            try:
+                with self.sigint_protection:
+                    disable_completion()
+                response = input(safe_prompt)
+            except EOFError:
+                response = ''
+                self.poutput('\n', end='')
+            finally:
+                with self.sigint_protection:
+                    enable_completion()
+
+            if not response:
+                continue
 
             if rl_type != RlType.NONE:
                 hlen = readline.get_current_history_length()
-                if hlen >= 1 and response != '':
+                if hlen >= 1:
                     readline.remove_history_item(hlen - 1)
-
             try:
                 choice = int(response)
                 if choice < 1:
@@ -2922,6 +2957,7 @@ class Cmd(cmd.Cmd):
             except (ValueError, IndexError):
                 self.poutput("{!r} isn't a valid choice. Pick a number between 1 and {}:".format(
                     response, len(fulloptions)))
+
         return result
 
     def _get_read_only_settings(self) -> str:


Test patch:
diff --git a/tests/test_cmd2.py b/tests/test_cmd2.py
index 1ba10f6b..ae02f16b 100755
--- a/tests/test_cmd2.py
+++ b/tests/test_cmd2.py
@@ -639,7 +639,7 @@ def _expected_no_editor_error():
 
     expected_text = normalize("""
 EXCEPTION of type '{}' occurred with message: 'Please use 'set editor' to specify your text editing program of choice.'
-To enable full traceback, run the following command:  'set debug true'
+To enable full traceback, run the following command: 'set debug true'
 """.format(expected_exception))
 
     return expected_text
@@ -1098,6 +1098,7 @@ def test_select_invalid_option_too_big(select_app):
     arg = 'Sauce? '
     calls = [mock.call(arg), mock.call(arg)]
     m.assert_has_calls(calls)
+    assert m.call_count == 2
 
     # And verify the expected output to stdout
     assert out == expected
@@ -1122,6 +1123,7 @@ def test_select_invalid_option_too_small(select_app):
     arg = 'Sauce? '
     calls = [mock.call(arg), mock.call(arg)]
     m.assert_has_calls(calls)
+    assert m.call_count == 2
 
     # And verify the expected output to stdout
     assert out == expected
@@ -1181,6 +1183,19 @@ Charm us with the {}...
     # And verify the expected output to stdout
     assert out == expected
 
+def test_select_eof(select_app):
+    # Ctrl-D during select causes an EOFError that just reprompts the user
+    m = mock.MagicMock(name='input', side_effect=[EOFError, 2])
+    builtins.input = m
+
+    food = 'fish'
+    out, err = run_cmd(select_app, "eat {}".format(food))
+
+    # Make sure our mock was called exactly twice with the expected arguments
+    arg = 'Sauce? '
+    calls = [mock.call(arg), mock.call(arg)]
+    m.assert_has_calls(calls)
+    assert m.call_count == 2
 
 class HelpNoDocstringApp(cmd2.Cmd):
     greet_parser = argparse.ArgumentParser()