From 3f61ff26ab8a72f51a67a55dbfedc0dc8c1346a9 Mon Sep 17 00:00:00 2001 From: JordonPhillips Date: Fri, 29 Apr 2016 12:50:42 -0700 Subject: [PATCH] Flush stdout after input prompt. Some terminal emulators (such as mintty) will not write output to the terminal window until stdout is explicitly flushed. When running `aws configure` a user would see nothing but a new line. After pressing 'enter' four times, they would then see the output flushed all in a single line. This makes it very difficult to configure the command. The solution is to call `flush` after every prompt. Since `raw_input` does not have an option to do this, we have to prompt and flush manually. Since we're already accessing stdout directly, it's easier to write to it directly since print adds some formatting that we don't want (namely, a newline at the end of the print). Fixes #1925 --- awscli/customizations/configure/configure.py | 12 +++++- .../configure/test_configure.py | 40 +++++++++++++++---- 2 files changed, 44 insertions(+), 8 deletions(-) diff --git a/awscli/customizations/configure/configure.py b/awscli/customizations/configure/configure.py index de97fd3fd40df..5f9c96e8dcd01 100644 --- a/awscli/customizations/configure/configure.py +++ b/awscli/customizations/configure/configure.py @@ -11,6 +11,7 @@ # ANY KIND, either express or implied. See the License for the specific # language governing permissions and limitations under the License. import os +import sys import logging from botocore.exceptions import ProfileNotFound @@ -39,7 +40,16 @@ class InteractivePrompter(object): def get_value(self, current_value, config_name, prompt_text=''): if config_name in ('aws_access_key_id', 'aws_secret_access_key'): current_value = mask_value(current_value) - response = raw_input("%s [%s]: " % (prompt_text, current_value)) + + # Many terminals will opt to print text as soon as possible, meaning + # we can use the 'prompt' parameter of raw_input without worry. However, + # There are some terminals that will only write to the screen when + # an explicit flush is called, such as MINGW. To support these systems, + # We need to explicitly flush each prompt we print. + sys.stdout.write("%s [%s]: " % (prompt_text, current_value)) + sys.stdout.flush() + response = raw_input() + if not response: # If the user hits enter, we return a value of None # instead of an empty string. That way we can determine diff --git a/tests/unit/customizations/configure/test_configure.py b/tests/unit/customizations/configure/test_configure.py index 5da2d4becd7ad..bb722f385f390 100644 --- a/tests/unit/customizations/configure/test_configure.py +++ b/tests/unit/customizations/configure/test_configure.py @@ -15,6 +15,7 @@ from awscli.customizations.configure import configure, ConfigValue, NOT_SET from awscli.testutils import unittest +from awscli.compat import six from . import FakeSession @@ -132,12 +133,16 @@ def test_session_says_profile_does_not_exist(self): class TestInteractivePrompter(unittest.TestCase): def setUp(self): - self.patch = mock.patch( + self.input_patch = mock.patch( 'awscli.customizations.configure.configure.raw_input') - self.mock_raw_input = self.patch.start() + self.mock_raw_input = self.input_patch.start() + self.stdout = six.StringIO() + self.stdout_patch = mock.patch('sys.stdout', self.stdout) + self.stdout_patch.start() def tearDown(self): - self.patch.stop() + self.input_patch.stop() + self.stdout_patch.stop() def test_access_key_is_masked(self): self.mock_raw_input.return_value = 'foo' @@ -148,7 +153,7 @@ def test_access_key_is_masked(self): # First we should return the value from raw_input. self.assertEqual(response, 'foo') # We should also not display the entire access key. - prompt_text = self.mock_raw_input.call_args[0][0] + prompt_text = self.stdout.getvalue() self.assertNotIn('myaccesskey', prompt_text) self.assertRegexpMatches(prompt_text, r'\[\*\*\*\*.*\]') @@ -160,7 +165,7 @@ def test_access_key_not_masked_when_none(self): prompt_text='Access key') # First we should return the value from raw_input. self.assertEqual(response, 'foo') - prompt_text = self.mock_raw_input.call_args[0][0] + prompt_text = self.stdout.getvalue() self.assertIn('[None]', prompt_text) def test_secret_key_is_masked(self): @@ -170,7 +175,7 @@ def test_secret_key_is_masked(self): config_name='aws_secret_access_key', prompt_text='Secret Key') # We should also not display the entire secret key. - prompt_text = self.mock_raw_input.call_args[0][0] + prompt_text = self.stdout.getvalue() self.assertNotIn('mysupersecretkey', prompt_text) self.assertRegexpMatches(prompt_text, r'\[\*\*\*\*.*\]') @@ -180,7 +185,7 @@ def test_non_secret_keys_are_not_masked(self): current_value='mycurrentvalue', config_name='not_a_secret_key', prompt_text='Enter value') # We should also not display the entire secret key. - prompt_text = self.mock_raw_input.call_args[0][0] + prompt_text = self.stdout.getvalue() self.assertIn('mycurrentvalue', prompt_text) self.assertRegexpMatches(prompt_text, r'\[mycurrentvalue\]') @@ -196,6 +201,27 @@ def test_user_hits_enter_returns_none(self): # was no input. self.assertIsNone(response) + def test_prompter_flushes_after_each_prompt(self): + # Clear out the default patch + self.stdout_patch.stop() + + # Create a mock stdout to record flush calls and replace stdout_patch + self.stdout = mock.Mock() + self.stdout_patch = mock.patch('sys.stdout', self.stdout) + self.stdout_patch.start() + + # Make sure flush called at least once + prompter = configure.InteractivePrompter() + prompter.get_value(current_value='foo', config_name='bar', + prompt_text='baz') + self.assertTrue(self.stdout.flush.called) + + # Make sure flush is called after *every* prompt + self.stdout.reset_mock() + prompter.get_value(current_value='foo2', config_name='bar2', + prompt_text='baz2') + self.assertTrue(self.stdout.flush.called) + class TestConfigValueMasking(unittest.TestCase):