diff --git a/awscli/formatter.py b/awscli/formatter.py index 6021bbc88155..a26a794de128 100644 --- a/awscli/formatter.py +++ b/awscli/formatter.py @@ -44,6 +44,12 @@ def _remove_request_id(self, response_data): def _get_default_stream(self): return compat.get_stdout_text_writer() + def _flush_stream(self, stream): + try: + stream.flush() + except IOError: + pass + class FullyBufferedFormatter(Formatter): def __call__(self, operation, response, stream=None): @@ -58,15 +64,19 @@ def __call__(self, operation, response, stream=None): response_data = response.build_full_result() else: response_data = response + self._remove_request_id(response_data) + if self._args.query is not None: + response_data = self._args.query.search(response_data) try: - self._remove_request_id(response_data) - if self._args.query is not None: - response_data = self._args.query.search(response_data) self._format_response(operation, response_data, stream) + except IOError as e: + # If the reading end of our stdout stream has closed the file + # we can just exit. + pass finally: # flush is needed to avoid the "close failed in file object # destructor" in python2.x (see http://bugs.python.org/issue11380). - stream.flush() + self._flush_stream(stream) class JSONFormatter(FullyBufferedFormatter): @@ -238,7 +248,7 @@ def __call__(self, operation, response, stream=None): finally: # flush is needed to avoid the "close failed in file object # destructor" in python2.x (see http://bugs.python.org/issue11380). - stream.flush() + self._flush_stream(stream) def _format_response(self, response, stream): if self._args.query is not None: diff --git a/tests/unit/output/test_json_output.py b/tests/unit/output/test_json_output.py index 253c2fe2fa88..b645572bbe43 100644 --- a/tests/unit/output/test_json_output.py +++ b/tests/unit/output/test_json_output.py @@ -15,6 +15,7 @@ import platform import mock from awscli.compat import six +from awscli.formatter import JSONFormatter from awscli.testutils import BaseAWSCommandParamsTest, unittest from awscli.compat import get_stdout_text_writer @@ -95,3 +96,18 @@ def test_json_prints_unicode_chars(self): # It should be encoded into the default encoding. self.assertNotIn('\\u2713', output) self.assertIn(expected, output) + + +class TestFormattersHandleClosedPipes(unittest.TestCase): + def test_fully_buffered_handles_io_error(self): + args = mock.Mock(query=None) + operation = mock.Mock(can_paginate=False) + response = '{"Foo": "Bar"}' + fake_closed_stream = mock.Mock(spec=six.StringIO) + fake_closed_stream.flush.side_effect = IOError + formatter = JSONFormatter(args) + formatter(operation, response, fake_closed_stream) + # We should not have let the IOError propogate, but + # we still should have called the flush() on the + # stream. + fake_closed_stream.flush.assert_called_with()