From 48d9b4e9d30759c9cd8c840fe42a3ca45c32c394 Mon Sep 17 00:00:00 2001 From: kyleknap Date: Mon, 20 Oct 2014 19:17:34 -0700 Subject: [PATCH 01/12] Add support for use of input JSON skeletons Added ``--generate-cli-skeleton`` to generate the JSON skeleton to fill out and ``--cli-input-json`` to use that skeleton as input to a CLI command. --- awscli/argprocess.py | 2 - awscli/clidriver.py | 23 ++++++- awscli/customizations/arguments.py | 38 +++++++++++ awscli/customizations/cliinputjson.py | 66 +++++++++++++++++++ awscli/customizations/generatecliskeleton.py | 68 ++++++++++++++++++++ awscli/handlers.py | 5 ++ 6 files changed, 198 insertions(+), 4 deletions(-) create mode 100644 awscli/customizations/arguments.py create mode 100644 awscli/customizations/cliinputjson.py create mode 100644 awscli/customizations/generatecliskeleton.py diff --git a/awscli/argprocess.py b/awscli/argprocess.py index cfa2d1c7afc8..9f8191003cd3 100644 --- a/awscli/argprocess.py +++ b/awscli/argprocess.py @@ -70,7 +70,6 @@ def unpack_argument(session, service_name, operation_name, cli_argument, value): """ param_name = getattr(cli_argument, 'name', 'anonymous') - value_override = session.emit_first_non_none_response( 'load-cli-arg.%s.%s.%s' % (service_name, operation_name, @@ -80,7 +79,6 @@ def unpack_argument(session, service_name, operation_name, cli_argument, value): if value_override is not None: value = value_override - return value diff --git a/awscli/clidriver.py b/awscli/clidriver.py index 0dde5bde6b5c..d54e9597f9de 100644 --- a/awscli/clidriver.py +++ b/awscli/clidriver.py @@ -430,6 +430,13 @@ def __init__(self, name, parent_name, operation_object, operation_caller, self._operation_object = operation_object self._operation_caller = operation_caller self._service_object = service_object + self._run_operation = True + + def disable_call_operation(self): + self._run_operation = False + + def enable_call_operation(self): + self._run_operation = True @property def arg_table(self): @@ -440,6 +447,9 @@ def arg_table(self): def __call__(self, args, parsed_globals): # Once we know we're trying to call a particular operation # of a service we can go ahead and load the parameters. + event = 'building-argument-table-parser.%s.%s' % (self._parent_name, + self._name) + self._emit(event, argument_table=self.arg_table, args=args) operation_parser = self._create_operation_parser(self.arg_table) self._add_help(operation_parser) parsed_args, remaining = operation_parser.parse_known_args(args) @@ -457,8 +467,16 @@ def __call__(self, args, parsed_globals): parsed_globals=parsed_globals) call_parameters = self._build_call_parameters(parsed_args, self.arg_table) - return self._operation_caller.invoke( - self._operation_object, call_parameters, parsed_globals) + event = 'calling-service-operation.%s.%s' % (self._parent_name, + self._name) + self._emit(event, service_operation=self, + call_parameters=call_parameters, + parsed_args=parsed_args, parsed_globals=parsed_globals) + if self._run_operation: + return self._operation_caller.invoke( + self._operation_object, call_parameters, parsed_globals) + else: + return 0 def create_help_command(self): return OperationHelpCommand( @@ -485,6 +503,7 @@ def _build_call_parameters(self, args, arg_table): value = parsed_args[py_name] value = self._unpack_arg(arg_object, value) arg_object.add_to_params(service_params, value) + return service_params def _unpack_arg(self, cli_argument, value): diff --git a/awscli/customizations/arguments.py b/awscli/customizations/arguments.py new file mode 100644 index 000000000000..873be8fd34ed --- /dev/null +++ b/awscli/customizations/arguments.py @@ -0,0 +1,38 @@ +from awscli.arguments import CustomArgument + + +class OverrideRequiredArgsArgument(CustomArgument): + """An argument that if specified makes all other arguments not required + + By not required, it refers to not having an error thrown when the + parser parses the arguments. To obtain this argument's property of + ignoring required arguments, subclass from this class and fill out + the ``ARG_DATA`` parameter as described below. + """ + + # ``ARG_DATA`` follows the same format as a member of ``ARG_TABLE`` in + # ``BasicCommand`` class as specified in + # ``awscli/customizations/commands.py``. + # + # For example, an ``ARG_DATA`` variable would be filled out as: + # + # ARG_DATA = + # {'name': 'my-argument', + # 'help_text': 'This is argument ensures the argument is specified' + # 'no other arguments are required'} + ARG_DATA = {} + + def __init__(self, session): + self._session = session + self._register_argument_action() + super(OverrideRequiredArgsArgument, self).__init__(**self.ARG_DATA) + + def _register_argument_action(self): + self._session.register('building-argument-table-parser', + self.override_required_args) + + def override_required_args(self, argument_table, args, **kwargs): + name_in_cmdline = '--' + self.name + if name_in_cmdline in args: + for arg_name in argument_table.keys(): + argument_table[arg_name].required = False diff --git a/awscli/customizations/cliinputjson.py b/awscli/customizations/cliinputjson.py new file mode 100644 index 000000000000..1d2509ec5bb3 --- /dev/null +++ b/awscli/customizations/cliinputjson.py @@ -0,0 +1,66 @@ +import json + +from awscli.paramfile import get_paramfile +from awscli.argprocess import ParamError +from awscli.customizations.arguments import OverrideRequiredArgsArgument + + +def register_cli_input_json(cli): + cli.register('building-argument-table', add_cli_input_json) + + +def add_cli_input_json(operation, argument_table, **kwargs): + cli_input_json_argument = CliInputJSONArgument(operation) + cli_input_json_argument.add_to_arg_table(argument_table) + + +class CliInputJSONArgument(OverrideRequiredArgsArgument): + """This argument inputs a JSON string as the entire input for a command. + + Ideally, the value to this argument should be a filled out JSON file + generated by ``--generate-cli-skeleton``. The items in the JSON string + will not clobber other arguments entered into the command line. + """ + ARG_DATA = { + 'name': 'cli-input-json', + 'help_text': 'Performs service operation based on the JSON string ' + 'provided. The JSON string follows the format provided ' + 'by ``--generate-cli-skeleton``. If other arguments are ' + 'provided on the command line, it will not clobber their ' + 'values.' + } + + def __init__(self, operation_object): + self._operation_object = operation_object + super(CliInputJSONArgument, self).__init__( + self._operation_object.session) + + def _register_argument_action(self): + self._operation_object.session.register( + 'calling-service-operation', self.add_to_call_parameters) + super(CliInputJSONArgument, self)._register_argument_action() + + def add_to_call_parameters(self, service_operation, call_parameters, + parsed_args, parsed_globals, **kwargs): + + # Check if ``--cli-input-json`` was specified in the command line. + input_json = getattr(parsed_args, 'cli_input_json', None) + if input_json is not None: + # Retrieve the JSON from the file if needed. + retrieved_json = get_paramfile(input_json) + try: + # Try to load the JSON string into a python dictionary + input_data = json.loads(retrieved_json) + except ValueError as e: + raise ParamError( + self.name, "Invalid JSON: %s\nJSON received: %s" + % (e, retrieved_json)) + # Add the members from the input JSON to the call parameters. + self._update_call_parameters(call_parameters, input_data) + + def _update_call_parameters(self, call_parameters, input_data): + for input_key in input_data.keys(): + # Only add the values to ``call_parameters`` if not already + # present. + if input_key not in call_parameters: + call_parameters[input_key] = input_data[input_key] diff --git a/awscli/customizations/generatecliskeleton.py b/awscli/customizations/generatecliskeleton.py new file mode 100644 index 000000000000..3ebf6f62b1ec --- /dev/null +++ b/awscli/customizations/generatecliskeleton.py @@ -0,0 +1,68 @@ +import json +import sys + +from botocore.utils import ArgumentGenerator + +from awscli.customizations.arguments import OverrideRequiredArgsArgument + + +def register_generate_cli_skeleton(cli): + cli.register('building-argument-table', add_generate_skeleton) + + +def add_generate_skeleton(operation, argument_table, **kwargs): + generate_cli_skeleton_argument = GenerateCliSkeletonArgument(operation) + generate_cli_skeleton_argument.add_to_arg_table(argument_table) + + +class GenerateCliSkeletonArgument(OverrideRequiredArgsArgument): + """This argument writes a generated JSON skeleton to stdout + + The argument, if present in the command line, will prevent the intended + command from taking place. Instead, it will generate a JSON skeleton and + print it to standard output. This JSON skeleton then can be filled out + and can be used as input to ``--input-cli-json`` in order to run the + command with the filled out JSON skeleton. + """ + ARG_DATA = { + 'name': 'generate-cli-skeleton', + 'help_text': 'Prints a sample input JSON to standard output. Note the ' + 'specified operation is not run if this argument is' + 'specified. The sample input can be used as an argument ' + 'for ``--cli-input-json``.', + 'action': 'store_true', + 'group_name': 'generate_cli_skeleton' + } + + def __init__(self, operation_object): + self._operation_object = operation_object + super(GenerateCliSkeletonArgument, self).__init__( + self._operation_object.session) + + def _register_argument_action(self): + self._operation_object.session.register( + 'calling-service-operation', self.generate_json_skeleton) + super(GenerateCliSkeletonArgument, self)._register_argument_action() + + def generate_json_skeleton(self, service_operation, call_parameters, + parsed_args, parsed_globals, **kwargs): + + # Only perform the method if the ``--generate-cli-skeleton`` was + # included in the command line. + if getattr(parsed_args, 'generate_cli_skeleton', False): + + # Ensure the operation will not be called from botocore. + service_operation.disable_call_operation() + + # Obtain the model of the operation + operation_model = self._operation_object.model + + # Generate the skeleton based on the ``input_shape``. + argument_generator = ArgumentGenerator() + operation_input_shape = operation_model.input_shape + skeleton = argument_generator.generate_skeleton( + operation_input_shape) + + # Write the generated skeleton to standard output. + sys.stdout.write(json.dumps(skeleton, indent=4)) + sys.stdout.write('\n') diff --git a/awscli/handlers.py b/awscli/handlers.py index 7eceb4ebcda3..48bc157f9529 100644 --- a/awscli/handlers.py +++ b/awscli/handlers.py @@ -48,6 +48,9 @@ from awscli.customizations.cloudsearchdomain import register_cloudsearchdomain from awscli.customizations.s3endpoint import register_s3_endpoint from awscli.customizations.s3errormsg import register_s3_error_msg +from awscli.customizations.cliinputjson import register_cli_input_json +from awscli.customizations.generatecliskeleton import \ + register_generate_cli_skeleton def awscli_initialize(event_handlers): @@ -100,3 +103,5 @@ def awscli_initialize(event_handlers): emr_initialize(event_handlers) register_cloudsearchdomain(event_handlers) register_s3_endpoint(event_handlers) + register_generate_cli_skeleton(event_handlers) + register_cli_input_json(event_handlers) From b6d164a34f5b201dae7f433f8eaddbae544ca4af Mon Sep 17 00:00:00 2001 From: kyleknap Date: Mon, 20 Oct 2014 22:43:44 -0700 Subject: [PATCH 02/12] Add unit test for input JSON skeleton feature --- awscli/customizations/arguments.py | 12 +++ awscli/customizations/cliinputjson.py | 14 +++ tests/unit/customizations/test_arguments.py | 48 +++++++++ .../unit/customizations/test_cliinputjson.py | 102 ++++++++++++++++++ .../test_generatecliskeleton.py | 83 ++++++++++++++ tests/unit/test_clidriver.py | 34 ++++++ tests/unit/test_completer.py | 3 +- 7 files changed, 295 insertions(+), 1 deletion(-) create mode 100644 tests/unit/customizations/test_arguments.py create mode 100644 tests/unit/customizations/test_cliinputjson.py create mode 100644 tests/unit/customizations/test_generatecliskeleton.py diff --git a/awscli/customizations/arguments.py b/awscli/customizations/arguments.py index 873be8fd34ed..5119525d40e7 100644 --- a/awscli/customizations/arguments.py +++ b/awscli/customizations/arguments.py @@ -1,3 +1,15 @@ +# Copyright 2014 Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). You +# may not use this file except in compliance with the License. A copy of +# the License is located at +# +# http://aws.amazon.com/apache2.0/ +# +# or in the "license" file accompanying this file. This file is +# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +# ANY KIND, either express or implied. See the License for the specific +# language governing permissions and limitations under the License. from awscli.arguments import CustomArgument diff --git a/awscli/customizations/cliinputjson.py b/awscli/customizations/cliinputjson.py index 1d2509ec5bb3..95ba810e1687 100644 --- a/awscli/customizations/cliinputjson.py +++ b/awscli/customizations/cliinputjson.py @@ -1,3 +1,15 @@ +# Copyright 2014 Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). You +# may not use this file except in compliance with the License. A copy of +# the License is located at +# +# http://aws.amazon.com/apache2.0/ +# +# or in the "license" file accompanying this file. This file is +# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +# ANY KIND, either express or implied. See the License for the specific +# language governing permissions and limitations under the License. import json from awscli.paramfile import get_paramfile @@ -48,6 +60,8 @@ def add_to_call_parameters(self, service_operation, call_parameters, if input_json is not None: # Retrieve the JSON from the file if needed. retrieved_json = get_paramfile(input_json) + if retrieved_json is None: + retrieved_json = input_json try: # Try to load the JSON string into a python dictionary input_data = json.loads(retrieved_json) diff --git a/tests/unit/customizations/test_arguments.py b/tests/unit/customizations/test_arguments.py new file mode 100644 index 000000000000..ca96db0efb05 --- /dev/null +++ b/tests/unit/customizations/test_arguments.py @@ -0,0 +1,48 @@ +# Copyright 2014 Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). You +# may not use this file except in compliance with the License. A copy of +# the License is located at +# +# http://aws.amazon.com/apache2.0/ +# +# or in the "license" file accompanying this file. This file is +# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +# ANY KIND, either express or implied. See the License for the specific +# language governing permissions and limitations under the License. +import mock + +from awscli.testutils import unittest +from awscli.customizations.arguments import OverrideRequiredArgsArgument + + +class TestOverrideRequiredArgsArgument(unittest.TestCase): + def setUp(self): + self.session = mock.Mock() + OverrideRequiredArgsArgument.ARG_DATA = {'name': 'my-argument'} + self.argument = OverrideRequiredArgsArgument(self.session) + + # Set up a sample argument_table + self.argument_table = {} + self.mock_arg = mock.Mock() + self.mock_arg.required = True + self.argument_table['mock-arg'] = self.mock_arg + + def tearDown(self): + OverrideRequiredArgsArgument.ARG_DATA = {} + + def test_register_argument_action(self): + register_args = self.session.register.call_args + self.assertEqual(register_args[0][0], 'building-argument-table-parser') + self.assertEqual(register_args[0][1], + self.argument.override_required_args) + + def test_override_required_args_if_in_cmdline(self): + args = ['--my-argument'] + self.argument.override_required_args(self.argument_table, args) + self.assertFalse(self.mock_arg.required) + + def test_no_override_required_args_if_not_in_cmdline(self): + args = [] + self.argument.override_required_args(self.argument_table, args) + self.assertTrue(self.mock_arg.required) diff --git a/tests/unit/customizations/test_cliinputjson.py b/tests/unit/customizations/test_cliinputjson.py new file mode 100644 index 000000000000..f8b4abfbcefa --- /dev/null +++ b/tests/unit/customizations/test_cliinputjson.py @@ -0,0 +1,102 @@ +# Copyright 2014 Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). You +# may not use this file except in compliance with the License. A copy of +# the License is located at +# +# http://aws.amazon.com/apache2.0/ +# +# or in the "license" file accompanying this file. This file is +# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +# ANY KIND, either express or implied. See the License for the specific +# language governing permissions and limitations under the License. +import mock +import os +import shutil +import tempfile + +from awscli.testutils import unittest +from awscli.argprocess import ParamError +from awscli.customizations.cliinputjson import CliInputJSONArgument + + +class TestCliInputJSONArgument(unittest.TestCase): + def setUp(self): + self.operation_object = mock.Mock() + self.argument = CliInputJSONArgument(self.operation_object) + + # Create the various forms the data could come in. The two main forms + # are as a string and or as a path to a file. + self.input_json = \ + '{\n "A": "foo",\n "B": "bar"\n}\n' + + # Make a temporary file + self.temp_dir = tempfile.mkdtemp() + self.temp_file = os.path.join(self.temp_dir, 'foo.json') + with open(self.temp_file, 'w') as f: + f.write(self.input_json) + + def tearDown(self): + shutil.rmtree(self.temp_dir) + + def test_register_argument_action(self): + register_args = self.operation_object.session.register.call_args_list + self.assertEqual(register_args[0][0][0], 'calling-service-operation') + self.assertEqual(register_args[0][0][1], + self.argument.add_to_call_parameters) + + def test_add_to_call_parameters_no_file(self): + parsed_args = mock.Mock() + # Make the value a JSON string + parsed_args.cli_input_json = self.input_json + call_parameters = {} + self.argument.add_to_call_parameters( + service_operation=None, call_parameters=call_parameters, + parsed_args=parsed_args, parsed_globals=None + ) + self.assertEqual(call_parameters, {'A': 'foo', 'B': 'bar'}) + + def test_add_to_call_parameters_with_file(self): + parsed_args = mock.Mock() + # Make the value a file with JSON located inside. + parsed_args.cli_input_json = 'file://' + self.temp_file + call_parameters = {} + self.argument.add_to_call_parameters( + service_operation=None, call_parameters=call_parameters, + parsed_args=parsed_args, parsed_globals=None + ) + self.assertEqual(call_parameters, {'A': 'foo', 'B': 'bar'}) + + def test_add_to_call_parameters_bad_json(self): + parsed_args = mock.Mock() + # Create a bad JSON input + parsed_args.cli_input_json = self.input_json + ',' + call_parameters = {} + with self.assertRaises(ParamError): + self.argument.add_to_call_parameters( + service_operation=None, call_parameters=call_parameters, + parsed_args=parsed_args, parsed_globals=None + ) + + def test_add_to_call_parameters_no_clobber(self): + parsed_args = mock.Mock() + parsed_args.cli_input_json = self.input_json + # The value for ``A`` should not be clobbered by the input JSON + call_parameters = {'A': 'baz'} + self.argument.add_to_call_parameters( + service_operation=None, call_parameters=call_parameters, + parsed_args=parsed_args, parsed_globals=None + ) + self.assertEqual(call_parameters, {'A': 'baz', 'B': 'bar'}) + + def test_no_add_to_call_parameters(self): + parsed_args = mock.Mock() + parsed_args.cli_input_json = None + call_parameters = {'A': 'baz'} + self.argument.add_to_call_parameters( + service_operation=None, call_parameters=call_parameters, + parsed_args=parsed_args, parsed_globals=None + ) + # Nothing should have been added to the call parameters because + # ``cli_input_json`` is not in the ``parsed_args`` + self.assertEqual(call_parameters, {'A': 'baz'}) diff --git a/tests/unit/customizations/test_generatecliskeleton.py b/tests/unit/customizations/test_generatecliskeleton.py new file mode 100644 index 000000000000..34618a09a666 --- /dev/null +++ b/tests/unit/customizations/test_generatecliskeleton.py @@ -0,0 +1,83 @@ +# Copyright 2014 Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). You +# may not use this file except in compliance with the License. A copy of +# the License is located at +# +# http://aws.amazon.com/apache2.0/ +# +# or in the "license" file accompanying this file. This file is +# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +# ANY KIND, either express or implied. See the License for the specific +# language governing permissions and limitations under the License. +import six +import mock + +from botocore.model import DenormalizedStructureBuilder + +from awscli.testutils import unittest +from awscli.customizations.generatecliskeleton import \ + GenerateCliSkeletonArgument + + +class TestGenerateCliSkeleton(unittest.TestCase): + def setUp(self): + self.operation_object = mock.Mock() + self.argument = GenerateCliSkeletonArgument(self.operation_object) + + # Create a mock service operation object + self.service_operation = mock.Mock() + + # Make an arbitrary input model shape. + self.input_shape = { + 'A': { + 'type': 'structure', + 'members': { + 'B': {'type': 'string'}, + } + } + } + shape = DenormalizedStructureBuilder().with_members( + self.input_shape).build_model() + self.operation_object.model.input_shape = shape + + # This is what the json should should look like after being + # generated to standard output. + self.ref_json_output = \ + '{\n "A": {\n "B": ""\n }\n}\n' + + def test_register_argument_action(self): + register_args = self.operation_object.session.register.call_args_list + self.assertEqual(register_args[0][0][0], 'calling-service-operation') + self.assertEqual(register_args[0][0][1], + self.argument.generate_json_skeleton) + + def test_generate_json_skeleton(self): + parsed_args = mock.Mock() + parsed_args.generate_cli_skeleton = True + with mock.patch('sys.stdout', six.StringIO()) as mock_stdout: + self.argument.generate_json_skeleton( + service_operation=self.service_operation, call_parameters=None, + parsed_args=parsed_args, parsed_globals=None + ) + # Ensure the service operation's ``disable_call_operation`` was + # called to prevent the botocore operation is not called. + self.assertTrue( + self.service_operation.disable_call_operation.called) + # Ensure the contents printed to standard output are correct. + self.assertEqual(self.ref_json_output, mock_stdout.getvalue()) + + def test_no_generate_json_skeleton(self): + parsed_args = mock.Mock() + parsed_args.generate_cli_skeleton = False + with mock.patch('sys.stdout', six.StringIO()) as mock_stdout: + self.argument.generate_json_skeleton( + service_operation=self.service_operation, call_parameters=None, + parsed_args=parsed_args, parsed_globals=None + ) + # Ensure that the service operation ``disable_call_operation`` was + # not called so that the botocore operation is called. + self.assertFalse( + self.service_operation.disable_call_operation.called) + # Ensure nothing is printed to standard output + self.assertEqual('', mock_stdout.getvalue()) diff --git a/tests/unit/test_clidriver.py b/tests/unit/test_clidriver.py index 9c1512f617c0..aa3130cbc865 100644 --- a/tests/unit/test_clidriver.py +++ b/tests/unit/test_clidriver.py @@ -245,10 +245,12 @@ def test_expected_events_are_emitted_in_order(self): 'top-level-args-parsed', 'building-command-table.s3', 'building-argument-table.s3.list-objects', + 'building-argument-table-parser.s3.list-objects', 'operation-args-parsed.s3.list-objects', 'load-cli-arg.s3.list-objects.bucket', 'process-cli-arg.s3.list-objects', 'load-cli-arg.s3.list-objects.key', + 'calling-service-operation.s3.list-objects' ]) def test_create_help_command(self): @@ -377,6 +379,14 @@ def inject_command_schema(self, command_table, session, **kwargs): command_table['foo'] = command + def force_no_call_operation(self, service_operation, call_parameters, + parsed_args, parsed_globals, **kwargs): + service_operation.disable_call_operation() + + def force_call_operation(self, service_operation, call_parameters, + parsed_args, parsed_globals, **kwargs): + service_operation.enable_call_operation() + def test_aws_with_endpoint_url(self): with mock.patch('botocore.service.Service.get_endpoint') as endpoint: http_response = models.Response() @@ -619,6 +629,30 @@ def raise_exception(*args, **kwargs): 'Unable to locate credentials. ' 'You can configure credentials by running "aws configure".') + def test_disable_call_operation(self): + self.driver = create_clidriver() + # Disable the call made to the operation. + self.driver.session.register( + 'calling-service-operation', self.force_no_call_operation) + + stdout, stderr, rc = self.run_cmd('ec2 describe-instances') + self.assertEqual(rc, 0) + # Check that command did not run. If it ran, we would expect to see + # an output listing the reservations. + self.assertEqual('', stdout) + + def test_enable_call_operation(self): + self.driver = create_clidriver() + # Enable the call made to the operation. + self.driver.session.register( + 'calling-service-operation', self.force_call_operation) + + stdout, stderr, rc = self.run_cmd('ec2 describe-instances') + self.assertEqual(rc, 0) + # Check that command did run. If it ran, we would expect to see + # an output listing the reservations. + self.assertIn('Reservations', stdout) + class TestHTTPParamFileDoesNotExist(BaseAWSCommandParamsTest): diff --git a/tests/unit/test_completer.py b/tests/unit/test_completer.py index bd41e0e0f063..3839171d0b32 100644 --- a/tests/unit/test_completer.py +++ b/tests/unit/test_completer.py @@ -62,7 +62,8 @@ set(['--filters', '--dry-run', '--no-dry-run', '--endpoint-url', '--no-verify-ssl', '--no-paginate', '--no-sign-request', '--output', '--profile', '--starting-token', '--max-items', - '--region', '--version', '--color', '--query', '--page-size'])), + '--region', '--version', '--color', '--query', '--page-size', + '--generate-cli-skeleton', '--cli-input-json'])), ('aws s3', -1, set(['cp', 'mv', 'rm', 'mb', 'rb', 'ls', 'sync', 'website'])), ('aws s3 m', -1, set(['mv', 'mb'])), ('aws s3 cp -', -1, set(['--no-guess-mime-type', '--dryrun', From 3549e70b96a3586a20b4dd50924d38a92a27cd61 Mon Sep 17 00:00:00 2001 From: kyleknap Date: Wed, 22 Oct 2014 11:53:22 -0700 Subject: [PATCH 03/12] Added integration test for json skeleton feature --- .../customizations/test_cliinputjson.py | 96 +++++++++++++++++++ .../test_generatecliskeleton.py | 50 ++++++++++ 2 files changed, 146 insertions(+) create mode 100644 tests/integration/customizations/test_cliinputjson.py create mode 100644 tests/integration/customizations/test_generatecliskeleton.py diff --git a/tests/integration/customizations/test_cliinputjson.py b/tests/integration/customizations/test_cliinputjson.py new file mode 100644 index 000000000000..a9ffd2dbf7e0 --- /dev/null +++ b/tests/integration/customizations/test_cliinputjson.py @@ -0,0 +1,96 @@ +# Copyright 2014 Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). You +# may not use this file except in compliance with the License. A copy of +# the License is located at +# +# http://aws.amazon.com/apache2.0/ +# +# or in the "license" file accompanying this file. This file is +# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +# ANY KIND, either express or implied. See the License for the specific +# language governing permissions and limitations under the License. +import os +import time +import tempfile +import random +import shutil + +import botocore.session + +from awscli.testutils import unittest, aws + + +class TestIntegCliInputJson(unittest.TestCase): + """This tests various services to if it properly uses generated input JSON. + + The s3 service was chosen becuase its operations do not take a lot of time. + These tests are essentially smoke tests. They are testing that the + ``--cli-input-json`` works. It is by no means exhaustive. + """ + def setUp(self): + self.session = botocore.session.get_session() + self.region = 'us-west-2' + + # Set up a s3 bucket. + self.s3 = self.session.create_client('s3', region_name=self.region) + self.bucket_name = 'cliinputjsontest%s-%s' % ( + int(time.time()), random.randint(1, 1000000)) + self.s3.create_bucket( + Bucket=self.bucket_name, + CreateBucketConfiguration={'LocationConstraint': self.region} + ) + + # Add an object to the bucket. + self.obj_name = 'foo' + self.s3.put_object( + Bucket=self.bucket_name, + Key=self.obj_name, + Body='bar' + ) + + # Create a temporary sample input json file. + self.input_json = '{"Bucket": "%s", "Key": "%s"}' % \ + (self.bucket_name, self.obj_name) + + self.temp_dir = tempfile.mkdtemp() + self.temp_file = os.path.join(self.temp_dir, 'foo.json') + with open(self.temp_file, 'w') as f: + f.write(self.input_json) + + def tearDown(self): + shutil.rmtree(self.temp_dir) + self.s3.delete_object( + Bucket=self.bucket_name, + Key=self.obj_name + ) + self.s3.delete_bucket(Bucket=self.bucket_name) + + def test_cli_input_json_no_exta_args(self): + # Run a head command using the input json + p = aws('s3api head-object --cli-input-json file://%s' + % self.temp_file) + # The head object command should find the object specified by the + # input json file. + self.assertEqual(p.rc, 0) + + def test_cli_input_json_exta_args(self): + # Check that the object can be found. + p = aws('s3api head-object --cli-input-json file://%s' + % self.temp_file) + self.assertEqual(p.rc, 0) + + # Override the ``key`` argument. Should produce a failure because + # the key ``bar`` does not exist. + p = aws('s3api head-object --key bar --cli-input-json file://%s' + % self.temp_file) + self.assertEqual(p.rc, 255) + self.assertIn('Not Found', p.stderr) + + def test_cli_input_json_not_from_file(self): + # Check that the input json can be used without having to use a file. + p = aws( + 's3api head-object --cli-input-json ' + '\'{"Bucket": "%s", "Key": "%s"}\'' % + (self.bucket_name, self.obj_name)) + self.assertEqual(p.rc, 0) diff --git a/tests/integration/customizations/test_generatecliskeleton.py b/tests/integration/customizations/test_generatecliskeleton.py new file mode 100644 index 000000000000..5bdf464edd7f --- /dev/null +++ b/tests/integration/customizations/test_generatecliskeleton.py @@ -0,0 +1,50 @@ +# Copyright 2014 Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). You +# may not use this file except in compliance with the License. A copy of +# the License is located at +# +# http://aws.amazon.com/apache2.0/ +# +# or in the "license" file accompanying this file. This file is +# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +# ANY KIND, either express or implied. See the License for the specific +# language governing permissions and limitations under the License. +from awscli.testutils import unittest, aws + + +class TestIntegGenerateCliSkeleton(unittest.TestCase): + """This tests various services to see if the generated skeleton is correct + + These the operations and services selected are arbitrary. Tried to pick + operations that we do not except too much change. So the test does not + fail often when the services are updated. These are essentially smoke + tests. It is not trying to test the different types of input shapes that + can be generated in the skeleton. It is only testing wheter the skeleton + generator argument works for various services. + """ + def test_generate_cli_skeleton_s3api(self): + p = aws('s3api delete-object --generate-cli-skeleton') + self.assertEqual(p.rc, 0) + self.assertEqual( + p.stdout, + '{\n "Bucket": "", \n "Key": "", \n "MFA": "", \n ' + '"VersionId": ""\n}\n' + ) + + def test_generate_cli_skeleton_sqs(self): + p = aws('sqs change-message-visibility --generate-cli-skeleton') + self.assertEqual(p.rc, 0) + self.assertEqual( + p.stdout, + '{\n "QueueUrl": "", \n "ReceiptHandle": "", \n ' + '"VisibilityTimeout": 0\n}\n' + ) + + def test_generate_cli_skeleton_iam(self): + p = aws('iam create-group --generate-cli-skeleton') + self.assertEqual(p.rc, 0) + self.assertEqual( + p.stdout, + '{\n "Path": "", \n "GroupName": ""\n}\n' + ) From 210686cf35fb560ef79ba6f31cb3bd71b8c176bf Mon Sep 17 00:00:00 2001 From: kyleknap Date: Wed, 22 Oct 2014 13:31:07 -0700 Subject: [PATCH 04/12] Clean up the input json skeleton code --- awscli/argprocess.py | 2 ++ awscli/clidriver.py | 11 ++++++++++- awscli/customizations/arguments.py | 11 +++++++---- awscli/customizations/cliinputjson.py | 2 ++ awscli/customizations/generatecliskeleton.py | 2 +- .../customizations/test_cliinputjson.py | 19 ++++++++++--------- .../test_generatecliskeleton.py | 12 ++++++------ tests/unit/customizations/test_arguments.py | 6 +----- .../unit/customizations/test_cliinputjson.py | 3 +-- .../test_generatecliskeleton.py | 2 +- 10 files changed, 41 insertions(+), 29 deletions(-) diff --git a/awscli/argprocess.py b/awscli/argprocess.py index 9f8191003cd3..cfa2d1c7afc8 100644 --- a/awscli/argprocess.py +++ b/awscli/argprocess.py @@ -70,6 +70,7 @@ def unpack_argument(session, service_name, operation_name, cli_argument, value): """ param_name = getattr(cli_argument, 'name', 'anonymous') + value_override = session.emit_first_non_none_response( 'load-cli-arg.%s.%s.%s' % (service_name, operation_name, @@ -79,6 +80,7 @@ def unpack_argument(session, service_name, operation_name, cli_argument, value): if value_override is not None: value = value_override + return value diff --git a/awscli/clidriver.py b/awscli/clidriver.py index d54e9597f9de..be9e91b3cd37 100644 --- a/awscli/clidriver.py +++ b/awscli/clidriver.py @@ -433,9 +433,19 @@ def __init__(self, name, parent_name, operation_object, operation_caller, self._run_operation = True def disable_call_operation(self): + """ + If called, the service operation will not run the botocore + operation at the end of the ``__call__`` method. By default, the + botocore operation runs if this method is not called. + """ self._run_operation = False def enable_call_operation(self): + """ + If called, the service operation will run the botocore + operation at the end of the ``__call__`` method. By default, the + botocore operation runs even if this method is not called. + """ self._run_operation = True @property @@ -503,7 +513,6 @@ def _build_call_parameters(self, args, arg_table): value = parsed_args[py_name] value = self._unpack_arg(arg_object, value) arg_object.add_to_params(service_params, value) - return service_params def _unpack_arg(self, cli_argument, value): diff --git a/awscli/customizations/arguments.py b/awscli/customizations/arguments.py index 5119525d40e7..702d5d2fc502 100644 --- a/awscli/customizations/arguments.py +++ b/awscli/customizations/arguments.py @@ -17,9 +17,10 @@ class OverrideRequiredArgsArgument(CustomArgument): """An argument that if specified makes all other arguments not required By not required, it refers to not having an error thrown when the - parser parses the arguments. To obtain this argument's property of - ignoring required arguments, subclass from this class and fill out - the ``ARG_DATA`` parameter as described below. + parser does not find an argument that is required on the command line. + To obtain this argument's property of ignoring required arguments, + subclass from this class and fill out the ``ARG_DATA`` parameter as + described below. Note this class is really only useful for subclassing. """ # ``ARG_DATA`` follows the same format as a member of ``ARG_TABLE`` in @@ -32,7 +33,7 @@ class OverrideRequiredArgsArgument(CustomArgument): # {'name': 'my-argument', # 'help_text': 'This is argument ensures the argument is specified' # 'no other arguments are required'} - ARG_DATA = {} + ARG_DATA = {'name': 'no-required-args'} def __init__(self, session): self._session = session @@ -45,6 +46,8 @@ def _register_argument_action(self): def override_required_args(self, argument_table, args, **kwargs): name_in_cmdline = '--' + self.name + # Set all ``Argument`` objects in ``argumnet_table`` to not required + # if this argument's name is present in the command line. if name_in_cmdline in args: for arg_name in argument_table.keys(): argument_table[arg_name].required = False diff --git a/awscli/customizations/cliinputjson.py b/awscli/customizations/cliinputjson.py index 95ba810e1687..e094cba92b0c 100644 --- a/awscli/customizations/cliinputjson.py +++ b/awscli/customizations/cliinputjson.py @@ -60,6 +60,8 @@ def add_to_call_parameters(self, service_operation, call_parameters, if input_json is not None: # Retrieve the JSON from the file if needed. retrieved_json = get_paramfile(input_json) + # Nothing was retrieved from the file. So assume the argument + # is already a JSON string. if retrieved_json is None: retrieved_json = input_json try: diff --git a/awscli/customizations/generatecliskeleton.py b/awscli/customizations/generatecliskeleton.py index 3ebf6f62b1ec..b168b0a699f5 100644 --- a/awscli/customizations/generatecliskeleton.py +++ b/awscli/customizations/generatecliskeleton.py @@ -27,7 +27,7 @@ class GenerateCliSkeletonArgument(OverrideRequiredArgsArgument): ARG_DATA = { 'name': 'generate-cli-skeleton', 'help_text': 'Prints a sample input JSON to standard output. Note the ' - 'specified operation is not run if this argument is' + 'specified operation is not run if this argument is ' 'specified. The sample input can be used as an argument ' 'for ``--cli-input-json``.', 'action': 'store_true', diff --git a/tests/integration/customizations/test_cliinputjson.py b/tests/integration/customizations/test_cliinputjson.py index a9ffd2dbf7e0..29a3b6e84563 100644 --- a/tests/integration/customizations/test_cliinputjson.py +++ b/tests/integration/customizations/test_cliinputjson.py @@ -22,7 +22,7 @@ class TestIntegCliInputJson(unittest.TestCase): - """This tests various services to if it properly uses generated input JSON. + """This tests to see if a service properly uses the generated input JSON. The s3 service was chosen becuase its operations do not take a lot of time. These tests are essentially smoke tests. They are testing that the @@ -68,29 +68,30 @@ def tearDown(self): def test_cli_input_json_no_exta_args(self): # Run a head command using the input json - p = aws('s3api head-object --cli-input-json file://%s' - % self.temp_file) + p = aws('s3api head-object --cli-input-json file://%s --region %s' + % (self.temp_file, self.region)) # The head object command should find the object specified by the # input json file. self.assertEqual(p.rc, 0) def test_cli_input_json_exta_args(self): # Check that the object can be found. - p = aws('s3api head-object --cli-input-json file://%s' - % self.temp_file) + p = aws('s3api head-object --cli-input-json file://%s --region %s' + % (self.temp_file, self.region)) self.assertEqual(p.rc, 0) # Override the ``key`` argument. Should produce a failure because # the key ``bar`` does not exist. - p = aws('s3api head-object --key bar --cli-input-json file://%s' - % self.temp_file) + p = aws('s3api head-object --key bar --cli-input-json file://%s ' + '--region %s' + % (self.temp_file, self.region)) self.assertEqual(p.rc, 255) self.assertIn('Not Found', p.stderr) def test_cli_input_json_not_from_file(self): # Check that the input json can be used without having to use a file. p = aws( - 's3api head-object --cli-input-json ' + 's3api head-object --region %s --cli-input-json ' '\'{"Bucket": "%s", "Key": "%s"}\'' % - (self.bucket_name, self.obj_name)) + (self.region, self.bucket_name, self.obj_name)) self.assertEqual(p.rc, 0) diff --git a/tests/integration/customizations/test_generatecliskeleton.py b/tests/integration/customizations/test_generatecliskeleton.py index 5bdf464edd7f..2602aee6b0c2 100644 --- a/tests/integration/customizations/test_generatecliskeleton.py +++ b/tests/integration/customizations/test_generatecliskeleton.py @@ -16,12 +16,12 @@ class TestIntegGenerateCliSkeleton(unittest.TestCase): """This tests various services to see if the generated skeleton is correct - These the operations and services selected are arbitrary. Tried to pick - operations that we do not except too much change. So the test does not - fail often when the services are updated. These are essentially smoke - tests. It is not trying to test the different types of input shapes that - can be generated in the skeleton. It is only testing wheter the skeleton - generator argument works for various services. + The operations and services selected are arbitrary. Tried to pick + operations that do not have many input options for the sake of readablity + and maintenance. These are essentially smoke tests. It is not trying to + test the different types of input shapes that can be generated in the + skeleton. It is only testing wheter the skeleton generator argument works + for various services. """ def test_generate_cli_skeleton_s3api(self): p = aws('s3api delete-object --generate-cli-skeleton') diff --git a/tests/unit/customizations/test_arguments.py b/tests/unit/customizations/test_arguments.py index ca96db0efb05..91941af97047 100644 --- a/tests/unit/customizations/test_arguments.py +++ b/tests/unit/customizations/test_arguments.py @@ -19,7 +19,6 @@ class TestOverrideRequiredArgsArgument(unittest.TestCase): def setUp(self): self.session = mock.Mock() - OverrideRequiredArgsArgument.ARG_DATA = {'name': 'my-argument'} self.argument = OverrideRequiredArgsArgument(self.session) # Set up a sample argument_table @@ -28,9 +27,6 @@ def setUp(self): self.mock_arg.required = True self.argument_table['mock-arg'] = self.mock_arg - def tearDown(self): - OverrideRequiredArgsArgument.ARG_DATA = {} - def test_register_argument_action(self): register_args = self.session.register.call_args self.assertEqual(register_args[0][0], 'building-argument-table-parser') @@ -38,7 +34,7 @@ def test_register_argument_action(self): self.argument.override_required_args) def test_override_required_args_if_in_cmdline(self): - args = ['--my-argument'] + args = ['--no-required-args'] self.argument.override_required_args(self.argument_table, args) self.assertFalse(self.mock_arg.required) diff --git a/tests/unit/customizations/test_cliinputjson.py b/tests/unit/customizations/test_cliinputjson.py index f8b4abfbcefa..79dadfaabdf2 100644 --- a/tests/unit/customizations/test_cliinputjson.py +++ b/tests/unit/customizations/test_cliinputjson.py @@ -27,8 +27,7 @@ def setUp(self): # Create the various forms the data could come in. The two main forms # are as a string and or as a path to a file. - self.input_json = \ - '{\n "A": "foo",\n "B": "bar"\n}\n' + self.input_json = '{"A": "foo", "B": "bar"}' # Make a temporary file self.temp_dir = tempfile.mkdtemp() diff --git a/tests/unit/customizations/test_generatecliskeleton.py b/tests/unit/customizations/test_generatecliskeleton.py index 34618a09a666..c92dd6909e52 100644 --- a/tests/unit/customizations/test_generatecliskeleton.py +++ b/tests/unit/customizations/test_generatecliskeleton.py @@ -61,7 +61,7 @@ def test_generate_json_skeleton(self): parsed_args=parsed_args, parsed_globals=None ) # Ensure the service operation's ``disable_call_operation`` was - # called to prevent the botocore operation is not called. + # called to ensure the botocore operation is not called. self.assertTrue( self.service_operation.disable_call_operation.called) # Ensure the contents printed to standard output are correct. From f8413719865e9a15a7fbc98aed2ec574f3559259 Mon Sep 17 00:00:00 2001 From: kyleknap Date: Thu, 23 Oct 2014 20:48:30 -0700 Subject: [PATCH 05/12] Update JSON code based on feedback --- awscli/clidriver.py | 3 +++ awscli/customizations/arguments.py | 2 +- .../customizations/test_cliinputjson.py | 21 +++++++++++++++++++ 3 files changed, 25 insertions(+), 1 deletion(-) diff --git a/awscli/clidriver.py b/awscli/clidriver.py index be9e91b3cd37..6ba463f1157c 100644 --- a/awscli/clidriver.py +++ b/awscli/clidriver.py @@ -486,6 +486,9 @@ def __call__(self, args, parsed_globals): return self._operation_caller.invoke( self._operation_object, call_parameters, parsed_globals) else: + # This is the value usually returned by the ``invoke()`` method + # of the operation caller. It represents the return code of the + # operation. return 0 def create_help_command(self): diff --git a/awscli/customizations/arguments.py b/awscli/customizations/arguments.py index 702d5d2fc502..b3f559ac66a7 100644 --- a/awscli/customizations/arguments.py +++ b/awscli/customizations/arguments.py @@ -46,7 +46,7 @@ def _register_argument_action(self): def override_required_args(self, argument_table, args, **kwargs): name_in_cmdline = '--' + self.name - # Set all ``Argument`` objects in ``argumnet_table`` to not required + # Set all ``Argument`` objects in ``argument_table`` to not required # if this argument's name is present in the command line. if name_in_cmdline in args: for arg_name in argument_table.keys(): diff --git a/tests/integration/customizations/test_cliinputjson.py b/tests/integration/customizations/test_cliinputjson.py index 29a3b6e84563..d5538a204af8 100644 --- a/tests/integration/customizations/test_cliinputjson.py +++ b/tests/integration/customizations/test_cliinputjson.py @@ -95,3 +95,24 @@ def test_cli_input_json_not_from_file(self): '\'{"Bucket": "%s", "Key": "%s"}\'' % (self.region, self.bucket_name, self.obj_name)) self.assertEqual(p.rc, 0) + + def test_cli_input_json_missing_required(self): + # Check that the operation properly throws an error if the json is + # missing any required arguments and the argument is not on the + # command line. + p = aws( + 's3api head-object --region %s --cli-input-json ' + '\'{"Key": "%s"}\'' % + (self.region, self.obj_name)) + self.assertEqual(p.rc, 255) + self.assertIn('Missing', p.stderr) + + def test_cli_input_json_has_extra_unknown_args(self): + # Check that the operation properly throws an error if the json + # has an extra argument that is not defined by the model. + p = aws( + 's3api head-object --region %s --cli-input-json ' + '\'{"Bucket": "%s", "Key": "%s", "Foo": "bar"}\'' % + (self.region, self.bucket_name, self.obj_name)) + self.assertEqual(p.rc, 255) + self.assertIn('Unknown', p.stderr) From eb66503d89058a05c97c0ce4f0ae7c0f0eb87c46 Mon Sep 17 00:00:00 2001 From: James Saryerwinnie Date: Fri, 24 Oct 2014 17:42:26 -0700 Subject: [PATCH 06/12] Add generate skeleton integ test --- .../test_generatecliskeleton.py | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/tests/integration/customizations/test_generatecliskeleton.py b/tests/integration/customizations/test_generatecliskeleton.py index 2602aee6b0c2..865fff58e9aa 100644 --- a/tests/integration/customizations/test_generatecliskeleton.py +++ b/tests/integration/customizations/test_generatecliskeleton.py @@ -10,9 +10,39 @@ # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF # ANY KIND, either express or implied. See the License for the specific # language governing permissions and limitations under the License. +import json + +from nose.tools import assert_equal + +from awscli.clidriver import create_clidriver from awscli.testutils import unittest, aws +def test_can_generate_skeletons_for_all_service_comands(): + driver = create_clidriver() + help_command = driver.create_help_command() + for command_name, command_obj in help_command.command_table.items(): + sub_help = command_obj.create_help_command() + for sub_name, sub_command in sub_help.command_table.items(): + op_help = sub_command.create_help_command() + arg_table = op_help.arg_table + if 'generate-cli-skeleton' in arg_table: + yield _test_gen_skeleton, command_name, sub_name + + +def _test_gen_skeleton(command_name, operation_name): + p = aws('%s %s --generate-cli-skeleton' % (command_name, operation_name)) + assert_equal(p.rc, 0, 'Received non zero RC (%s) for command: %s %s' + % (p.rc, command_name, operation_name)) + try: + parsed = json.loads(p.stdout) + except ValueError as e: + raise AssertionError( + "Could not generate CLI skeleton for command: %s %s\n" + "stdout:\n%s\n" + "stderr:\n%s\n" % (command_name, operation_name)) + + class TestIntegGenerateCliSkeleton(unittest.TestCase): """This tests various services to see if the generated skeleton is correct From ecc2a0dd317623b2b1ed4730c56e461c046ed7f5 Mon Sep 17 00:00:00 2001 From: kyleknap Date: Sun, 26 Oct 2014 13:34:54 -0700 Subject: [PATCH 07/12] Accepts input shape of None --- awscli/customizations/generatecliskeleton.py | 9 +++++++-- .../customizations/test_generatecliskeleton.py | 14 ++++++++++++++ 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/awscli/customizations/generatecliskeleton.py b/awscli/customizations/generatecliskeleton.py index b168b0a699f5..bc26a5110245 100644 --- a/awscli/customizations/generatecliskeleton.py +++ b/awscli/customizations/generatecliskeleton.py @@ -60,8 +60,13 @@ def generate_json_skeleton(self, service_operation, call_parameters, # Generate the skeleton based on the ``input_shape``. argument_generator = ArgumentGenerator() operation_input_shape = operation_model.input_shape - skeleton = argument_generator.generate_skeleton( - operation_input_shape) + # If the ``input_shape`` is ``None``, generate an empty + # dictionary. + if operation_input_shape is None: + skeleton = {} + else: + skeleton = argument_generator.generate_skeleton( + operation_input_shape) # Write the generated skeleton to standard output. sys.stdout.write(json.dumps(skeleton, indent=4)) diff --git a/tests/unit/customizations/test_generatecliskeleton.py b/tests/unit/customizations/test_generatecliskeleton.py index c92dd6909e52..a3280c161f7e 100644 --- a/tests/unit/customizations/test_generatecliskeleton.py +++ b/tests/unit/customizations/test_generatecliskeleton.py @@ -81,3 +81,17 @@ def test_no_generate_json_skeleton(self): self.service_operation.disable_call_operation.called) # Ensure nothing is printed to standard output self.assertEqual('', mock_stdout.getvalue()) + + def test_generate_json_skeleton_no_input_shape(self): + parsed_args = mock.Mock() + parsed_args.generate_cli_skeleton = True + # Set the input shape to ``None``. + self.operation_object.model.input_shape = None + with mock.patch('sys.stdout', six.StringIO()) as mock_stdout: + self.argument.generate_json_skeleton( + service_operation=self.service_operation, call_parameters=None, + parsed_args=parsed_args, parsed_globals=None + ) + # Ensure the contents printed to standard output are correct, + # which should be an empty dictionary. + self.assertEqual('{}\n', mock_stdout.getvalue()) From 9a0898bf4732a90efd81dba850569c2dec84de63 Mon Sep 17 00:00:00 2001 From: kyleknap Date: Sun, 26 Oct 2014 14:50:20 -0700 Subject: [PATCH 08/12] Fix arguments with no required setters --- awscli/clidriver.py | 4 ++-- awscli/customizations/arguments.py | 2 +- awscli/customizations/ec2addcount.py | 7 ++++++- awscli/customizations/ec2decryptpassword.py | 7 ++++++- awscli/customizations/iamvirtmfa.py | 11 ++++++----- awscli/customizations/paginate.py | 7 ++++++- awscli/customizations/streamingoutputarg.py | 15 ++++++++++++--- .../customizations/test_generatecliskeleton.py | 14 +++++++++----- tests/unit/test_clidriver.py | 2 +- 9 files changed, 49 insertions(+), 20 deletions(-) diff --git a/awscli/clidriver.py b/awscli/clidriver.py index 6ba463f1157c..6d21699eea24 100644 --- a/awscli/clidriver.py +++ b/awscli/clidriver.py @@ -457,8 +457,8 @@ def arg_table(self): def __call__(self, args, parsed_globals): # Once we know we're trying to call a particular operation # of a service we can go ahead and load the parameters. - event = 'building-argument-table-parser.%s.%s' % (self._parent_name, - self._name) + event = 'before-building-argument-table-parser.%s.%s' % \ + (self._parent_name, self._name) self._emit(event, argument_table=self.arg_table, args=args) operation_parser = self._create_operation_parser(self.arg_table) self._add_help(operation_parser) diff --git a/awscli/customizations/arguments.py b/awscli/customizations/arguments.py index b3f559ac66a7..ee47dbe778c2 100644 --- a/awscli/customizations/arguments.py +++ b/awscli/customizations/arguments.py @@ -41,7 +41,7 @@ def __init__(self, session): super(OverrideRequiredArgsArgument, self).__init__(**self.ARG_DATA) def _register_argument_action(self): - self._session.register('building-argument-table-parser', + self._session.register('before-building-argument-table-parser', self.override_required_args) def override_required_args(self, argument_table, args, **kwargs): diff --git a/awscli/customizations/ec2addcount.py b/awscli/customizations/ec2addcount.py index e16a881e923d..d96cd3d44835 100644 --- a/awscli/customizations/ec2addcount.py +++ b/awscli/customizations/ec2addcount.py @@ -40,6 +40,7 @@ def __init__(self, operation, name): self.argument_model = model.Shape('CountArgument', {'type': 'string'}) self._operation = operation self._name = name + self._required = False @property def cli_name(self): @@ -51,7 +52,11 @@ def cli_type_name(self): @property def required(self): - return False + return self._required + + @required.setter + def required(self, value): + self._required = value @property def documentation(self): diff --git a/awscli/customizations/ec2decryptpassword.py b/awscli/customizations/ec2decryptpassword.py index 64f775efa67c..0a27a347acb7 100644 --- a/awscli/customizations/ec2decryptpassword.py +++ b/awscli/customizations/ec2decryptpassword.py @@ -45,6 +45,7 @@ def __init__(self, operation, name): self._operation = operation self._name = name self._key_path = None + self._required = False @property def cli_type_name(self): @@ -52,7 +53,11 @@ def cli_type_name(self): @property def required(self): - return False + return self._required + + @required.setter + def required(self, value): + self._required = value @property def documentation(self): diff --git a/awscli/customizations/iamvirtmfa.py b/awscli/customizations/iamvirtmfa.py index e8338b923d34..23c994fe1a4f 100644 --- a/awscli/customizations/iamvirtmfa.py +++ b/awscli/customizations/iamvirtmfa.py @@ -54,11 +54,12 @@ class FileArgument(StatefulArgument): def add_to_params(self, parameters, value): # Validate the file here so we can raise an error prior # calling the service. - outfile = os.path.expandvars(value) - outfile = os.path.expanduser(outfile) - if not os.access(os.path.dirname(outfile), os.W_OK): - raise ValueError('Unable to write to file: %s' % outfile) - self._value = outfile + if value is not None: + outfile = os.path.expandvars(value) + outfile = os.path.expanduser(outfile) + if not os.access(os.path.dirname(outfile), os.W_OK): + raise ValueError('Unable to write to file: %s' % outfile) + self._value = outfile class IAMVMFAWrapper(object): diff --git a/awscli/customizations/paginate.py b/awscli/customizations/paginate.py index cfc6891092bb..fe2e84e0181d 100644 --- a/awscli/customizations/paginate.py +++ b/awscli/customizations/paginate.py @@ -140,6 +140,7 @@ def __init__(self, name, documentation, operation, parse_type): self._name = name self._documentation = documentation self._parse_type = parse_type + self._required = False @property def cli_name(self): @@ -151,7 +152,11 @@ def cli_type_name(self): @property def required(self): - return False + return self._required + + @required.setter + def required(self, value): + self._required = value @property def documentation(self): diff --git a/awscli/customizations/streamingoutputarg.py b/awscli/customizations/streamingoutputarg.py index 8f61c139915e..6a24f2b1d26b 100644 --- a/awscli/customizations/streamingoutputarg.py +++ b/awscli/customizations/streamingoutputarg.py @@ -52,6 +52,7 @@ def __init__(self, response_key, operation, name, buffer_size=None): self._response_key = response_key self._output_file = None self._name = name + self._required = True @property def cli_name(self): @@ -66,15 +67,23 @@ def cli_type_name(self): @property def required(self): - return True + return self._required + + @required.setter + def required(self, value): + self._required = value @property def documentation(self): return self.HELP def add_to_parser(self, parser): - parser.add_argument(self._name, metavar=self.py_name, - help=self.HELP) + if self.required: + # Positional arguments cannot be made optional. So if this argument + # is ever not required we do not add it to the arg parser. Note + # that by default the argument is required. + parser.add_argument(self._name, metavar=self.py_name, + help=self.HELP) def add_to_params(self, parameters, value): self._output_file = value diff --git a/tests/integration/customizations/test_generatecliskeleton.py b/tests/integration/customizations/test_generatecliskeleton.py index 865fff58e9aa..b089c001a098 100644 --- a/tests/integration/customizations/test_generatecliskeleton.py +++ b/tests/integration/customizations/test_generatecliskeleton.py @@ -23,11 +23,15 @@ def test_can_generate_skeletons_for_all_service_comands(): help_command = driver.create_help_command() for command_name, command_obj in help_command.command_table.items(): sub_help = command_obj.create_help_command() - for sub_name, sub_command in sub_help.command_table.items(): - op_help = sub_command.create_help_command() - arg_table = op_help.arg_table - if 'generate-cli-skeleton' in arg_table: - yield _test_gen_skeleton, command_name, sub_name + # This avoids command objects like ``PreviewModeCommand`` that + # do not exhibit any visible functionality (i.e. provides a command + # for the CLI). + if hasattr(sub_help, 'command_table'): + for sub_name, sub_command in sub_help.command_table.items(): + op_help = sub_command.create_help_command() + arg_table = op_help.arg_table + if 'generate-cli-skeleton' in arg_table: + yield _test_gen_skeleton, command_name, sub_name def _test_gen_skeleton(command_name, operation_name): diff --git a/tests/unit/test_clidriver.py b/tests/unit/test_clidriver.py index aa3130cbc865..8d973171328c 100644 --- a/tests/unit/test_clidriver.py +++ b/tests/unit/test_clidriver.py @@ -245,7 +245,7 @@ def test_expected_events_are_emitted_in_order(self): 'top-level-args-parsed', 'building-command-table.s3', 'building-argument-table.s3.list-objects', - 'building-argument-table-parser.s3.list-objects', + 'before-building-argument-table-parser.s3.list-objects', 'operation-args-parsed.s3.list-objects', 'load-cli-arg.s3.list-objects.bucket', 'process-cli-arg.s3.list-objects', From 2610ed292269994cc57395d3f6c5327d95ba64a2 Mon Sep 17 00:00:00 2001 From: kyleknap Date: Sun, 26 Oct 2014 16:16:32 -0700 Subject: [PATCH 09/12] Unit and integration test pass for json skeleton --- awscli/customizations/cloudsearchdomain.py | 5 +++-- tests/unit/customizations/test_arguments.py | 3 ++- tests/unit/customizations/test_cloudsearchdomain.py | 4 +++- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/awscli/customizations/cloudsearchdomain.py b/awscli/customizations/cloudsearchdomain.py index c21ce9d151c0..53e6ce195367 100644 --- a/awscli/customizations/cloudsearchdomain.py +++ b/awscli/customizations/cloudsearchdomain.py @@ -23,7 +23,8 @@ def register_cloudsearchdomain(cli): validate_endpoint_url) -def validate_endpoint_url(parsed_globals, **kwargs): - if parsed_globals.endpoint_url is None: +def validate_endpoint_url(parsed_args, parsed_globals, **kwargs): + if parsed_globals.endpoint_url is None and \ + not getattr(parsed_args, 'generate_cli_skeleton', False): raise ValueError( "--endpoint-url is required for cloudsearchdomain commands") diff --git a/tests/unit/customizations/test_arguments.py b/tests/unit/customizations/test_arguments.py index 91941af97047..ade75bf70545 100644 --- a/tests/unit/customizations/test_arguments.py +++ b/tests/unit/customizations/test_arguments.py @@ -29,7 +29,8 @@ def setUp(self): def test_register_argument_action(self): register_args = self.session.register.call_args - self.assertEqual(register_args[0][0], 'building-argument-table-parser') + self.assertEqual(register_args[0][0], + 'before-building-argument-table-parser') self.assertEqual(register_args[0][1], self.argument.override_required_args) diff --git a/tests/unit/customizations/test_cloudsearchdomain.py b/tests/unit/customizations/test_cloudsearchdomain.py index e545af32fd41..f64f690d2981 100644 --- a/tests/unit/customizations/test_cloudsearchdomain.py +++ b/tests/unit/customizations/test_cloudsearchdomain.py @@ -58,9 +58,11 @@ def test_endpoint_not_required_for_help(self): class TestCloudsearchDomainHandler(unittest.TestCase): def test_validate_endpoint_url_is_none(self): parsed_globals = mock.Mock() + parsed_args = mock.Mock() parsed_globals.endpoint_url = None + parsed_args.generate_cli_skeleton = False with self.assertRaises(ValueError): - validate_endpoint_url(parsed_globals) + validate_endpoint_url(parsed_args, parsed_globals) if __name__ == "__main__": From 7db6e6a244535333a8943bd6931613ba78524549 Mon Sep 17 00:00:00 2001 From: kyleknap Date: Mon, 27 Oct 2014 14:11:02 -0700 Subject: [PATCH 10/12] Refactor the clidriver event system --- awscli/clidriver.py | 56 +++++++++---------- awscli/customizations/cliinputjson.py | 13 +++-- awscli/customizations/cloudsearchdomain.py | 11 ++-- awscli/customizations/generatecliskeleton.py | 30 +++++++--- awscli/customizations/iamvirtmfa.py | 11 ++-- awscli/customizations/streamingoutputarg.py | 8 +-- awscli/handlers.py | 2 +- .../unit/customizations/test_cliinputjson.py | 2 +- .../customizations/test_cloudsearchdomain.py | 7 +-- .../test_generatecliskeleton.py | 23 ++++---- tests/unit/test_clidriver.py | 50 ++++++++--------- 11 files changed, 109 insertions(+), 104 deletions(-) diff --git a/awscli/clidriver.py b/awscli/clidriver.py index 6d21699eea24..48bdaff99e0f 100644 --- a/awscli/clidriver.py +++ b/awscli/clidriver.py @@ -430,23 +430,6 @@ def __init__(self, name, parent_name, operation_object, operation_caller, self._operation_object = operation_object self._operation_caller = operation_caller self._service_object = service_object - self._run_operation = True - - def disable_call_operation(self): - """ - If called, the service operation will not run the botocore - operation at the end of the ``__call__`` method. By default, the - botocore operation runs if this method is not called. - """ - self._run_operation = False - - def enable_call_operation(self): - """ - If called, the service operation will run the botocore - operation at the end of the ``__call__`` method. By default, the - botocore operation runs even if this method is not called. - """ - self._run_operation = True @property def arg_table(self): @@ -477,19 +460,32 @@ def __call__(self, args, parsed_globals): parsed_globals=parsed_globals) call_parameters = self._build_call_parameters(parsed_args, self.arg_table) - event = 'calling-service-operation.%s.%s' % (self._parent_name, - self._name) - self._emit(event, service_operation=self, - call_parameters=call_parameters, - parsed_args=parsed_args, parsed_globals=parsed_globals) - if self._run_operation: + event = 'calling-command.%s.%s' % (self._parent_name, + self._name) + override = self._emit_first_non_none_response( + event, + call_parameters=call_parameters, + parsed_args=parsed_args, + parsed_globals=parsed_globals + ) + # There are two possible values for override. It can be some type + # of exception that will be raised if detected or it can represent + # the desired return code. Note that a return code of 0 represents + # a success. + if override is not None: + if isinstance(override, Exception): + # If the override value provided back is an exception then + # raise the exception + raise override + else: + # This is the value usually returned by the ``invoke()`` + # method of the operation caller. It represents the return + # code of the operation. + return override + else: + # No override value was supplied. return self._operation_caller.invoke( self._operation_object, call_parameters, parsed_globals) - else: - # This is the value usually returned by the ``invoke()`` method - # of the operation caller. It represents the return code of the - # operation. - return 0 def create_help_command(self): return OperationHelpCommand( @@ -556,6 +552,10 @@ def _emit(self, name, **kwargs): session = self._service_object.session return session.emit(name, **kwargs) + def _emit_first_non_none_response(self, name, **kwargs): + session = self._service_object.session + return session.emit_first_non_none_response(name, **kwargs) + def _create_operation_parser(self, arg_table): parser = ArgTableArgParser(arg_table) return parser diff --git a/awscli/customizations/cliinputjson.py b/awscli/customizations/cliinputjson.py index e094cba92b0c..e986840eec44 100644 --- a/awscli/customizations/cliinputjson.py +++ b/awscli/customizations/cliinputjson.py @@ -22,8 +22,11 @@ def register_cli_input_json(cli): def add_cli_input_json(operation, argument_table, **kwargs): - cli_input_json_argument = CliInputJSONArgument(operation) - cli_input_json_argument.add_to_arg_table(argument_table) + # This argument cannot support operations with streaming output which + # is designated by the argument name `outfile`. + if 'outfile' not in argument_table: + cli_input_json_argument = CliInputJSONArgument(operation) + cli_input_json_argument.add_to_arg_table(argument_table) class CliInputJSONArgument(OverrideRequiredArgsArgument): @@ -49,11 +52,11 @@ def __init__(self, operation_object): def _register_argument_action(self): self._operation_object.session.register( - 'calling-service-operation', self.add_to_call_parameters) + 'calling-command', self.add_to_call_parameters) super(CliInputJSONArgument, self)._register_argument_action() - def add_to_call_parameters(self, service_operation, call_parameters, - parsed_args, parsed_globals, **kwargs): + def add_to_call_parameters(self, call_parameters, parsed_args, + parsed_globals, **kwargs): # Check if ``--cli-input-json`` was specified in the command line. input_json = getattr(parsed_args, 'cli_input_json', None) diff --git a/awscli/customizations/cloudsearchdomain.py b/awscli/customizations/cloudsearchdomain.py index 53e6ce195367..5aebcaf7fbd1 100644 --- a/awscli/customizations/cloudsearchdomain.py +++ b/awscli/customizations/cloudsearchdomain.py @@ -19,12 +19,11 @@ """ def register_cloudsearchdomain(cli): - cli.register('operation-args-parsed.cloudsearchdomain', - validate_endpoint_url) + cli.register_last('calling-command.cloudsearchdomain', + validate_endpoint_url) -def validate_endpoint_url(parsed_args, parsed_globals, **kwargs): - if parsed_globals.endpoint_url is None and \ - not getattr(parsed_args, 'generate_cli_skeleton', False): - raise ValueError( +def validate_endpoint_url(parsed_globals, **kwargs): + if parsed_globals.endpoint_url is None: + return ValueError( "--endpoint-url is required for cloudsearchdomain commands") diff --git a/awscli/customizations/generatecliskeleton.py b/awscli/customizations/generatecliskeleton.py index bc26a5110245..8edb7822cdd3 100644 --- a/awscli/customizations/generatecliskeleton.py +++ b/awscli/customizations/generatecliskeleton.py @@ -1,3 +1,15 @@ +# Copyright 2014 Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). You +# may not use this file except in compliance with the License. A copy of +# the License is located at +# +# http://aws.amazon.com/apache2.0/ +# +# or in the "license" file accompanying this file. This file is +# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +# ANY KIND, either express or implied. See the License for the specific +# language governing permissions and limitations under the License. import json import sys @@ -11,8 +23,11 @@ def register_generate_cli_skeleton(cli): def add_generate_skeleton(operation, argument_table, **kwargs): - generate_cli_skeleton_argument = GenerateCliSkeletonArgument(operation) - generate_cli_skeleton_argument.add_to_arg_table(argument_table) + # This argument cannot support operations with streaming output which + # is designated by the argument name `outfile`. + if 'outfile' not in argument_table: + generate_cli_skeleton_argument = GenerateCliSkeletonArgument(operation) + generate_cli_skeleton_argument.add_to_arg_table(argument_table) class GenerateCliSkeletonArgument(OverrideRequiredArgsArgument): @@ -41,19 +56,16 @@ def __init__(self, operation_object): def _register_argument_action(self): self._operation_object.session.register( - 'calling-service-operation', self.generate_json_skeleton) + 'calling-command.*', self.generate_json_skeleton) super(GenerateCliSkeletonArgument, self)._register_argument_action() - def generate_json_skeleton(self, service_operation, call_parameters, - parsed_args, parsed_globals, **kwargs): + def generate_json_skeleton(self, call_parameters, parsed_args, + parsed_globals, **kwargs): # Only perform the method if the ``--generate-cli-skeleton`` was # included in the command line. if getattr(parsed_args, 'generate_cli_skeleton', False): - # Ensure the operation will not be called from botocore. - service_operation.disable_call_operation() - # Obtain the model of the operation operation_model = self._operation_object.model @@ -71,3 +83,5 @@ def generate_json_skeleton(self, service_operation, call_parameters, # Write the generated skeleton to standard output. sys.stdout.write(json.dumps(skeleton, indent=4)) sys.stdout.write('\n') + # This is the return code + return 0 diff --git a/awscli/customizations/iamvirtmfa.py b/awscli/customizations/iamvirtmfa.py index 23c994fe1a4f..e8338b923d34 100644 --- a/awscli/customizations/iamvirtmfa.py +++ b/awscli/customizations/iamvirtmfa.py @@ -54,12 +54,11 @@ class FileArgument(StatefulArgument): def add_to_params(self, parameters, value): # Validate the file here so we can raise an error prior # calling the service. - if value is not None: - outfile = os.path.expandvars(value) - outfile = os.path.expanduser(outfile) - if not os.access(os.path.dirname(outfile), os.W_OK): - raise ValueError('Unable to write to file: %s' % outfile) - self._value = outfile + outfile = os.path.expandvars(value) + outfile = os.path.expanduser(outfile) + if not os.access(os.path.dirname(outfile), os.W_OK): + raise ValueError('Unable to write to file: %s' % outfile) + self._value = outfile class IAMVMFAWrapper(object): diff --git a/awscli/customizations/streamingoutputarg.py b/awscli/customizations/streamingoutputarg.py index 6a24f2b1d26b..536e640e64c2 100644 --- a/awscli/customizations/streamingoutputarg.py +++ b/awscli/customizations/streamingoutputarg.py @@ -78,12 +78,8 @@ def documentation(self): return self.HELP def add_to_parser(self, parser): - if self.required: - # Positional arguments cannot be made optional. So if this argument - # is ever not required we do not add it to the arg parser. Note - # that by default the argument is required. - parser.add_argument(self._name, metavar=self.py_name, - help=self.HELP) + parser.add_argument(self._name, metavar=self.py_name, + help=self.HELP) def add_to_params(self, parameters, value): self._output_file = value diff --git a/awscli/handlers.py b/awscli/handlers.py index 48bc157f9529..e9d2ce941f60 100644 --- a/awscli/handlers.py +++ b/awscli/handlers.py @@ -73,6 +73,7 @@ def awscli_initialize(event_handlers): # param_shorthand.add_example_fn) event_handlers.register('doc-examples.*.*', add_examples) + register_cli_input_json(event_handlers) event_handlers.register('building-argument-table.s3api.*', add_streaming_output_arg) event_handlers.register('building-argument-table.ec2.run-instances', @@ -104,4 +105,3 @@ def awscli_initialize(event_handlers): register_cloudsearchdomain(event_handlers) register_s3_endpoint(event_handlers) register_generate_cli_skeleton(event_handlers) - register_cli_input_json(event_handlers) diff --git a/tests/unit/customizations/test_cliinputjson.py b/tests/unit/customizations/test_cliinputjson.py index 79dadfaabdf2..b8c0240357bc 100644 --- a/tests/unit/customizations/test_cliinputjson.py +++ b/tests/unit/customizations/test_cliinputjson.py @@ -40,7 +40,7 @@ def tearDown(self): def test_register_argument_action(self): register_args = self.operation_object.session.register.call_args_list - self.assertEqual(register_args[0][0][0], 'calling-service-operation') + self.assertEqual(register_args[0][0][0], 'calling-command') self.assertEqual(register_args[0][0][1], self.argument.add_to_call_parameters) diff --git a/tests/unit/customizations/test_cloudsearchdomain.py b/tests/unit/customizations/test_cloudsearchdomain.py index f64f690d2981..04eb230f423c 100644 --- a/tests/unit/customizations/test_cloudsearchdomain.py +++ b/tests/unit/customizations/test_cloudsearchdomain.py @@ -58,11 +58,10 @@ def test_endpoint_not_required_for_help(self): class TestCloudsearchDomainHandler(unittest.TestCase): def test_validate_endpoint_url_is_none(self): parsed_globals = mock.Mock() - parsed_args = mock.Mock() parsed_globals.endpoint_url = None - parsed_args.generate_cli_skeleton = False - with self.assertRaises(ValueError): - validate_endpoint_url(parsed_args, parsed_globals) + # Method should return instantiated exception. + self.assertTrue(isinstance(validate_endpoint_url(parsed_globals), + ValueError)) if __name__ == "__main__": diff --git a/tests/unit/customizations/test_generatecliskeleton.py b/tests/unit/customizations/test_generatecliskeleton.py index a3280c161f7e..161ee9c40a55 100644 --- a/tests/unit/customizations/test_generatecliskeleton.py +++ b/tests/unit/customizations/test_generatecliskeleton.py @@ -48,7 +48,7 @@ def setUp(self): def test_register_argument_action(self): register_args = self.operation_object.session.register.call_args_list - self.assertEqual(register_args[0][0][0], 'calling-service-operation') + self.assertEqual(register_args[0][0][0], 'calling-command.*') self.assertEqual(register_args[0][0][1], self.argument.generate_json_skeleton) @@ -56,31 +56,28 @@ def test_generate_json_skeleton(self): parsed_args = mock.Mock() parsed_args.generate_cli_skeleton = True with mock.patch('sys.stdout', six.StringIO()) as mock_stdout: - self.argument.generate_json_skeleton( + rc = self.argument.generate_json_skeleton( service_operation=self.service_operation, call_parameters=None, parsed_args=parsed_args, parsed_globals=None ) - # Ensure the service operation's ``disable_call_operation`` was - # called to ensure the botocore operation is not called. - self.assertTrue( - self.service_operation.disable_call_operation.called) # Ensure the contents printed to standard output are correct. self.assertEqual(self.ref_json_output, mock_stdout.getvalue()) + # Ensure it is the correct return code of zero. + self.assertEqual(rc, 0) def test_no_generate_json_skeleton(self): parsed_args = mock.Mock() parsed_args.generate_cli_skeleton = False with mock.patch('sys.stdout', six.StringIO()) as mock_stdout: - self.argument.generate_json_skeleton( + rc = self.argument.generate_json_skeleton( service_operation=self.service_operation, call_parameters=None, parsed_args=parsed_args, parsed_globals=None ) - # Ensure that the service operation ``disable_call_operation`` was - # not called so that the botocore operation is called. - self.assertFalse( - self.service_operation.disable_call_operation.called) # Ensure nothing is printed to standard output self.assertEqual('', mock_stdout.getvalue()) + # Ensure nothing is returned because it was never called. + self.assertEqual(rc, None) + def test_generate_json_skeleton_no_input_shape(self): parsed_args = mock.Mock() @@ -88,10 +85,12 @@ def test_generate_json_skeleton_no_input_shape(self): # Set the input shape to ``None``. self.operation_object.model.input_shape = None with mock.patch('sys.stdout', six.StringIO()) as mock_stdout: - self.argument.generate_json_skeleton( + rc = self.argument.generate_json_skeleton( service_operation=self.service_operation, call_parameters=None, parsed_args=parsed_args, parsed_globals=None ) # Ensure the contents printed to standard output are correct, # which should be an empty dictionary. self.assertEqual('{}\n', mock_stdout.getvalue()) + # Ensure it is the correct return code of zero. + self.assertEqual(rc, 0) diff --git a/tests/unit/test_clidriver.py b/tests/unit/test_clidriver.py index 8d973171328c..4e16303d2142 100644 --- a/tests/unit/test_clidriver.py +++ b/tests/unit/test_clidriver.py @@ -250,7 +250,7 @@ def test_expected_events_are_emitted_in_order(self): 'load-cli-arg.s3.list-objects.bucket', 'process-cli-arg.s3.list-objects', 'load-cli-arg.s3.list-objects.key', - 'calling-service-operation.s3.list-objects' + 'calling-command.s3.list-objects' ]) def test_create_help_command(self): @@ -379,14 +379,6 @@ def inject_command_schema(self, command_table, session, **kwargs): command_table['foo'] = command - def force_no_call_operation(self, service_operation, call_parameters, - parsed_args, parsed_globals, **kwargs): - service_operation.disable_call_operation() - - def force_call_operation(self, service_operation, call_parameters, - parsed_args, parsed_globals, **kwargs): - service_operation.enable_call_operation() - def test_aws_with_endpoint_url(self): with mock.patch('botocore.service.Service.get_endpoint') as endpoint: http_response = models.Response() @@ -629,29 +621,33 @@ def raise_exception(*args, **kwargs): 'Unable to locate credentials. ' 'You can configure credentials by running "aws configure".') - def test_disable_call_operation(self): + def test_override_calling_command(self): self.driver = create_clidriver() - # Disable the call made to the operation. - self.driver.session.register( - 'calling-service-operation', self.force_no_call_operation) - stdout, stderr, rc = self.run_cmd('ec2 describe-instances') - self.assertEqual(rc, 0) - # Check that command did not run. If it ran, we would expect to see - # an output listing the reservations. - self.assertEqual('', stdout) + # Make a function that will return an override such that its value + # is used over whatever is returned by the invoker which is usually + # zero. + def override_with_rc(**kwargs): + return 20 + + self.driver.session.register('calling-command', override_with_rc) + rc = self.driver.main('ec2 describe-instances'.split()) + # Check that the overriden rc is as expected. + self.assertEqual(rc, 20) - def test_enable_call_operation(self): + def test_override_calling_command_error(self): self.driver = create_clidriver() - # Enable the call made to the operation. - self.driver.session.register( - 'calling-service-operation', self.force_call_operation) - stdout, stderr, rc = self.run_cmd('ec2 describe-instances') - self.assertEqual(rc, 0) - # Check that command did run. If it ran, we would expect to see - # an output listing the reservations. - self.assertIn('Reservations', stdout) + # Make a function that will return an error. The handler will cause + # an error to be returned and later raised. + def override_with_error(**kwargs): + return ValueError() + + self.driver.session.register('calling-command', override_with_error) + # An exception should be thrown as a result of the handler, which + # will result in 255 rc. + rc = self.driver.main('ec2 describe-instances'.split()) + self.assertEqual(rc, 255) class TestHTTPParamFileDoesNotExist(BaseAWSCommandParamsTest): From a16c8787f818f4ccf3a53c8072a7cf2754c86ee0 Mon Sep 17 00:00:00 2001 From: kyleknap Date: Mon, 27 Oct 2014 18:13:16 -0700 Subject: [PATCH 11/12] Updated the travis yml for unittest2 --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index aa013eb5e0c9..f706fb66a281 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,7 +4,7 @@ python: - "2.7" - "3.3" install: - - if [[ $TRAVIS_PYTHON_VERSION == '2.6' ]]; then pip install unittest2; fi + - if [[ $TRAVIS_PYTHON_VERSION == '2.6' ]]; then pip install unittest2==0.5.1; fi - pip install -r requirements.txt - python setup.py develop - pip install coverage python-coveralls From eed1362c47e73ee684d3b442258efae6e810154d Mon Sep 17 00:00:00 2001 From: kyleknap Date: Fri, 31 Oct 2014 15:39:46 -0700 Subject: [PATCH 12/12] Updated CHANGELOG --- CHANGELOG.rst | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 9a64aaec78e5..910f89f07f54 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -2,6 +2,18 @@ CHANGELOG ========= +Next Release (TBD) +================== + +* feature:``--generate-cli-skeleton``: Generates a JSON skeleton to fill out + and be used as input to ``--cli-input-json``. + (`issue 963 `_) +* feature:``--cli-input-json``: Runs an operation using a global JSON file + that supplies all of the operation's arguments. This JSON file can + be generated by ``--generate-cli-skeleton``. + (`issue 963 `_) + + 1.5.4 =====