diff --git a/CHANGELOG.rst b/CHANGELOG.rst index a158311a24c5..dad42f72b3f9 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -2,6 +2,23 @@ CHANGELOG ========= +1.3.21 +====== + +* feature:``aws opsworks``: Update the ``aws opsworks`` command + to the latest version +* bugfix:Shorthand JSON: Fix bug where shorthand lists with + a single item (e.g. ``--arg Param=[item]``) were not parsed + correctly. + (`issue 830 `__) +* bugfix:Text output: Fix bug when rendering only + scalars that are numbers in text output + (`issue 829 `__) +* bugfix:``aws cloudsearchdomain``: Fix bug where + ``--endpoint-url`` is required even for ``help`` subcommands + (`issue 828 `__) + + 1.3.20 ====== diff --git a/awscli/__init__.py b/awscli/__init__.py index bcc14bf96417..6aea8984ed37 100644 --- a/awscli/__init__.py +++ b/awscli/__init__.py @@ -17,7 +17,7 @@ """ import os -__version__ = '1.3.20' +__version__ = '1.3.21' # # Get our data path to be added to botocore's search path diff --git a/awscli/customizations/cloudsearchdomain.py b/awscli/customizations/cloudsearchdomain.py index 533c6fe8a512..c21ce9d151c0 100644 --- a/awscli/customizations/cloudsearchdomain.py +++ b/awscli/customizations/cloudsearchdomain.py @@ -19,11 +19,11 @@ """ def register_cloudsearchdomain(cli): - cli.register('top-level-args-parsed', validate_endpoint_url) + cli.register('operation-args-parsed.cloudsearchdomain', + validate_endpoint_url) -def validate_endpoint_url(parsed_args, **kwargs): - if parsed_args.command == 'cloudsearchdomain' and \ - parsed_args.endpoint_url is None: +def validate_endpoint_url(parsed_globals, **kwargs): + if parsed_globals.endpoint_url is None: raise ValueError( "--endpoint-url is required for cloudsearchdomain commands") diff --git a/awscli/help.py b/awscli/help.py index 25218a427b3f..5a695ed0b74c 100644 --- a/awscli/help.py +++ b/awscli/help.py @@ -114,7 +114,7 @@ def _exists_on_path(self, name): # Since we're only dealing with POSIX systems, we can # ignore things like PATHEXT. return any([os.path.exists(os.path.join(p, name)) - for p in os.environ.get('PATH', []).split(os.pathsep)]) + for p in os.environ.get('PATH', '').split(os.pathsep)]) def _popen(self, *args, **kwargs): return Popen(*args, **kwargs) diff --git a/awscli/text.py b/awscli/text.py index cb370d10ac3a..d74d4cb0dd1d 100644 --- a/awscli/text.py +++ b/awscli/text.py @@ -25,7 +25,7 @@ def _format_text(item, stream, identifier=None, scalar_keys=None): else: # If it's not a list or a dict, we just write the scalar # value out directly. - stream.write(item) + stream.write(six.text_type(item)) stream.write('\n') diff --git a/awscli/utils.py b/awscli/utils.py index 414474f1bc29..bd37243fcbf5 100644 --- a/awscli/utils.py +++ b/awscli/utils.py @@ -45,7 +45,11 @@ def _split_with_quotes(value): if list_start >= 0 and value.find(']') != -1 and \ (quote_char is None or part.find(quote_char) > list_start): # This is a list, eat all the items until the end - new_chunk = _eat_items(value, iter_parts, part, ']') + if ']' in part: + # Short circuit for only one item + new_chunk = part + else: + new_chunk = _eat_items(value, iter_parts, part, ']') list_items = _split_with_quotes(new_chunk[list_start + 2:-1]) new_chunk = new_chunk[:list_start + 1] + ','.join(list_items) new_parts.append(new_chunk) diff --git a/doc/source/conf.py b/doc/source/conf.py index 648996a20b2d..426a233c4ed6 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -52,7 +52,7 @@ # The short X.Y version. version = '1.3.' # The full version, including alpha/beta/rc tags. -release = '1.3.20' +release = '1.3.21' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/setup.py b/setup.py index ec1e88c7cd1d..f3ba063dc590 100644 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ import awscli -requires = ['botocore>=0.54.0,<0.55.0', +requires = ['botocore>=0.55.0,<0.56.0', 'bcdoc>=0.12.0,<0.13.0', 'six>=1.1.0', 'colorama==0.2.5', diff --git a/tests/integration/test_cli.py b/tests/integration/test_cli.py index d96a524386f3..edbac3fa9fc2 100644 --- a/tests/integration/test_cli.py +++ b/tests/integration/test_cli.py @@ -76,6 +76,23 @@ def test_operation_help_with_required_arg(self): self.assertEqual(p.rc, 1, p.stderr) self.assertIn('get-object', p.stdout) + def test_service_help_with_required_option(self): + # In cloudsearchdomain, the --endpoint-url is required. + # We want to make sure if you're just getting help tex + # that we don't trigger that validation. + p = aws('cloudsearchdomain help') + self.assertEqual(p.rc, 1, p.stderr) + self.assertIn('cloudsearchdomain', p.stdout) + # And nothing on stderr about missing options. + self.assertEqual(p.stderr, '') + + def test_operation_help_with_required_option(self): + p = aws('cloudsearchdomain search help') + self.assertEqual(p.rc, 1, p.stderr) + self.assertIn('search', p.stdout) + # And nothing on stderr about missing options. + self.assertEqual(p.stderr, '') + def test_help_with_warning_blocks(self): p = aws('elastictranscoder create-pipeline help') self.assertEqual(p.rc, 1, p.stderr) diff --git a/tests/unit/customizations/test_cloudsearchdomain.py b/tests/unit/customizations/test_cloudsearchdomain.py index 8ba25137c980..211ccd404e2a 100644 --- a/tests/unit/customizations/test_cloudsearchdomain.py +++ b/tests/unit/customizations/test_cloudsearchdomain.py @@ -12,6 +12,7 @@ # language governing permissions and limitations under the License. from awscli.testutils import unittest from awscli.testutils import BaseAWSCommandParamsTest +from awscli.help import HelpRenderer from awscli.customizations.cloudsearchdomain import validate_endpoint_url import mock @@ -48,14 +49,24 @@ def test_endpoint_is_required(self): stderr = self.run_cmd(cmd, expected_rc=255)[1] self.assertIn('--endpoint-url is required', stderr) + def test_endpoint_not_required_for_help(self): + cmd = self.prefix + 'help' + with mock.patch('awscli.help.get_renderer') as get_renderer: + mock_render = mock.Mock(spec=HelpRenderer) + get_renderer.return_value = mock_render + stdout, stderr, rc = self.run_cmd(cmd, expected_rc=None) + # If we get this far we've succeeded, but we can do + # a quick sanity check and make sure the service name is + # in the stdout help text. + self.assertIn(stdout, 'cloudsearchdomain') + class TestCloudsearchDomainHandler(unittest.TestCase): def test_validate_endpoint_url_is_none(self): - parsed_args = mock.Mock() - parsed_args.endpoint_url = None - parsed_args.command = 'cloudsearchdomain' + parsed_globals = mock.Mock() + parsed_globals.endpoint_url = None with self.assertRaises(ValueError): - validate_endpoint_url(parsed_args) + validate_endpoint_url(parsed_globals) if __name__ == "__main__": diff --git a/tests/unit/test_argprocess.py b/tests/unit/test_argprocess.py index 3356006cc659..207ccd8e3f50 100644 --- a/tests/unit/test_argprocess.py +++ b/tests/unit/test_argprocess.py @@ -220,12 +220,18 @@ def test_list_structure_list_scalar_3(self): {"Name": "foo", "Args": ["a", "k1=v1", "b"]}, {"Name": "bar", - "Args": ["baz"]} + "Args": ["baz"]}, + {"Name": "single_kv", + "Args": ["key=value"]}, + {"Name": "single_v", + "Args": ["value"]} ] simplified = self.simplify(p, [ "Name=foo,Args=[a,k1=v1,b]", - "Name=bar,Args=baz" + "Name=bar,Args=baz", + "Name=single_kv,Args=[key=value]", + "Name=single_v,Args=[value]" ]) self.assertEqual(simplified, expected) diff --git a/tests/unit/test_text.py b/tests/unit/test_text.py index 4c28ef4c33f9..341053efe03d 100644 --- a/tests/unit/test_text.py +++ b/tests/unit/test_text.py @@ -61,6 +61,15 @@ def test_multiple_list_of_dicts(self): 'ZOO\t0\t1\t2\n' ) + def test_single_scalar_number(self): + self.assert_text_renders_to(10, '10\n') + + def test_list_of_single_number(self): + self.assert_text_renders_to([10], '10\n') + + def test_list_of_multiple_numbers(self): + self.assert_text_renders_to([10, 10, 10], '10\t10\t10\n') + def test_different_keys_in_sublists(self): self.assert_text_renders_to( # missing "b" adds "d"