Repository: aws/aws-cli

Issue:
aws s3 sync does not raise error if target bucket does not exist
When running aws s3 sync with a local directory, and a target bucket that does not exist, no error text is displayed, and the tool has a return code of 0

This means we cannot tell the difference between a successful sync, and a sync that failed as the destination bucket does not exist. 



Discussion:

Resolving patch:
diff --git a/awscli/customizations/s3/dochandler.py b/awscli/customizations/s3/dochandler.py
new file mode 100644
--- /dev/null
+++ b/awscli/customizations/s3/dochandler.py
@@ -0,0 +1,89 @@
+import os
+
+from bcdoc.clidocs import CLIDocumentEventHandler
+
+import awscli
+
+
+class S3DocumentEventHandler(CLIDocumentEventHandler):
+    """
+    This is the document handler for both the service, s3, and
+    the commands. It is the basis for the help command and generating docs.
+    """
+    def doc_title(self, help_command, **kwargs):
+        doc = help_command.doc
+        command = help_command.obj
+        doc.style.h1(command._name)
+
+    def doc_description(self, help_command, **kwargs):
+        doc = help_command.doc
+        command = help_command.obj
+        doc.style.h2('Description')
+        doc.include_doc_string(command.documentation)
+        if help_command.obj._name == 's3':
+            doc_dir = os.path.join(
+                os.path.dirname(os.path.abspath(awscli.__file__)),
+                'examples', help_command.event_class.lower())
+            # The file is named '_concepts.rst' so that it doesn't
+            # collide with any s3 commands, in the rare chance we
+            # create a subcommand called "concepts".
+            doc_path = os.path.join(doc_dir, '_concepts.rst')
+            if os.path.isfile(doc_path):
+                help_command.doc.style.h2('Important Concepts')
+                fp = open(doc_path)
+                for line in fp.readlines():
+                    help_command.doc.write(line)
+
+    def doc_synopsis_start(self, help_command, **kwargs):
+        if help_command.obj._name != 's3':
+            doc = help_command.doc
+            command = help_command.obj
+            doc.style.h2('Synopsis')
+            doc.style.start_codeblock()
+            doc.writeln('%s %s' % (command._name, command.usage))
+
+    def doc_synopsis_option(self, arg_name, help_command, **kwargs):
+        doc = help_command.doc
+        argument = help_command.arg_table[arg_name]
+        option_str = argument._name
+        if 'nargs' in argument.options:
+            if argument.options['nargs'] == '+':
+                option_str += " <value> [<value>...]"
+            else:
+                for i in range(argument.options['nargs']):
+                    option_str += " <value>"
+        doc.writeln('[--%s]' % option_str)
+
+    def doc_synopsis_end(self, help_command, **kwargs):
+        if help_command.obj._name != 's3':
+            doc = help_command.doc
+            doc.style.end_codeblock()
+
+    def doc_options_start(self, help_command, **kwargs):
+        if help_command.obj._name != 's3':
+            doc = help_command.doc
+            doc.style.h2('Options')
+            if len(help_command.arg_table) == 0:
+                doc.write('*None*\n')
+
+    def doc_option(self, arg_name, help_command, **kwargs):
+        doc = help_command.doc
+        argument = help_command.arg_table[arg_name]
+        doc.write('``--%s``\n' % argument._name)
+        doc.style.indent()
+        doc.include_doc_string(argument.documentation)
+        doc.style.dedent()
+        doc.style.new_paragraph()
+
+    def doc_subitems_start(self, help_command, **kwargs):
+        if help_command.command_table:
+            doc = help_command.doc
+            doc.style.h2('Available Commands')
+            doc.style.toctree()
+
+    def doc_subitem(self, command_name, help_command, **kwargs):
+        doc = help_command.doc
+        doc.style.tocitem(command_name)
+
+
+
diff --git a/awscli/customizations/s3/s3.py b/awscli/customizations/s3/s3.py
--- a/awscli/customizations/s3/s3.py
+++ b/awscli/customizations/s3/s3.py
@@ -28,7 +28,7 @@
 from awscli.customizations.s3.description import add_command_descriptions, \
     add_param_descriptions
 from awscli.customizations.s3.utils import find_bucket_key, check_error
-from bcdoc.clidocs import CLIDocumentEventHandler
+from awscli.customizations.s3.dochandler import S3DocumentEventHandler
 
 
 class AppendFilter(argparse.Action):
@@ -54,13 +54,11 @@ def __call__(self, parser, namespace, values, option_string=None):
         setattr(namespace, self.dest, filter_list)
 
 
-"""
-This is a dictionary useful for automatically adding the different commands,
-the amount of arguments it takes, and the optional parameters that can appear
-on the same line as the command.  It also contains descriptions and usage
-keys for help command and doc generation.
-"""
-cmd_dict = {'cp': {'options': {'nargs': 2},
+# This is a dictionary useful for automatically adding the different commands,
+# the amount of arguments it takes, and the optional parameters that can appear
+# on the same line as the command.  It also contains descriptions and usage
+# keys for help command and doc generation.
+CMD_DICT = {'cp': {'options': {'nargs': 2},
                    'params': ['dryrun', 'quiet', 'recursive',
                               'include', 'exclude', 'acl',
                               'no-guess-mime-type',
@@ -94,16 +92,14 @@ def __call__(self, parser, namespace, values, option_string=None):
             'rb': {'options': {'nargs': 1}, 'params': ['force']}
             }
 
-add_command_descriptions(cmd_dict)
+add_command_descriptions(CMD_DICT)
 
 
-"""
-This is a dictionary useful for keeping track of the parameters passed to
-add_argument when the parameter is added to the parser.  The documents
-key is a description of what the parameter does and is used for the help
-command and doc generation.
-"""
-params_dict = {'dryrun': {'options': {'action': 'store_true'}},
+# This is a dictionary useful for keeping track of the parameters passed to
+# add_argument when the parameter is added to the parser.  The documents
+# key is a description of what the parameter does and is used for the help
+# command and doc generation.
+PARAMS_DICT = {'dryrun': {'options': {'action': 'store_true'}},
                'delete': {'options': {'action': 'store_true'}},
                'quiet': {'options': {'action': 'store_true'}},
                'force': {'options': {'action': 'store_true'}},
@@ -134,7 +130,7 @@ def __call__(self, parser, namespace, values, option_string=None):
                'expires': {'options': {'nargs': 1}},
                }
 
-add_param_descriptions(params_dict)
+add_param_descriptions(PARAMS_DICT)
 
 
 def awscli_initialize(cli):
@@ -145,9 +141,8 @@ def awscli_initialize(cli):
     file
     """
     cli.register("building-command-table.main", add_s3)
-    cli.register("building-operation-table.s3", add_commands)
     cli.register("doc-examples.S3.*", add_s3_examples)
-    for cmd in cmd_dict.keys():
+    for cmd in CMD_DICT.keys():
         cli.register("building-parameter-table.s3.%s" % cmd, add_cmd_params)
 
 
@@ -168,26 +163,15 @@ def add_s3(command_table, session, **kwargs):
     command_table['s3'] = S3('s3', session)
 
 
-def add_commands(operation_table, session, **kwargs):
-    """
-    This create the S3Command objects for each command
-    """
-    for cmd in cmd_dict.keys():
-        operation_table[cmd] = S3Command(cmd, session,
-                                         cmd_dict[cmd]['options'],
-                                         cmd_dict[cmd]['description'],
-                                         cmd_dict[cmd]['usage'])
-
-
 def add_cmd_params(parameter_table, command, **kwargs):
     """
     This creates the ParameterArgument object for each possible parameter
     in a specified command
     """
-    for param in cmd_dict[command]['params']:
+    for param in CMD_DICT[command]['params']:
         parameter_table[param] = S3Parameter(param,
-                                             params_dict[param]['options'],
-                                             params_dict[param]['documents'])
+                                             PARAMS_DICT[param]['options'],
+                                             PARAMS_DICT[param]['documents'])
 
 
 def add_s3_examples(help_command, **kwargs):
@@ -208,87 +192,6 @@ def add_s3_examples(help_command, **kwargs):
             help_command.doc.write(line)
 
 
-class S3DocumentEventHandler(CLIDocumentEventHandler):
-    """
-    This is the document handler for both the service, s3, and
-    the commands. It is the basis for the help command and generating docs.
-    """
-    def doc_title(self, help_command, **kwargs):
-        doc = help_command.doc
-        command = help_command.obj
-        doc.style.h1(command._name)
-
-    def doc_description(self, help_command, **kwargs):
-        doc = help_command.doc
-        command = help_command.obj
-        doc.style.h2('Description')
-        doc.include_doc_string(command.documentation)
-        if help_command.obj._name == 's3':
-            doc_dir = os.path.join(
-                os.path.dirname(os.path.abspath(awscli.__file__)),
-                'examples', help_command.event_class.lower())
-            # The file is named '_concepts.rst' so that it doesn't
-            # collide with any s3 commands, in the rare chance we
-            # create a subcommand called "concepts".
-            doc_path = os.path.join(doc_dir, '_concepts.rst')
-            if os.path.isfile(doc_path):
-                help_command.doc.style.h2('Important Concepts')
-                fp = open(doc_path)
-                for line in fp.readlines():
-                    help_command.doc.write(line)
-
-    def doc_synopsis_start(self, help_command, **kwargs):
-        if help_command.obj._name != 's3':
-            doc = help_command.doc
-            command = help_command.obj
-            doc.style.h2('Synopsis')
-            doc.style.start_codeblock()
-            doc.writeln('%s %s' % (command._name, command.usage))
-
-    def doc_synopsis_option(self, arg_name, help_command, **kwargs):
-        doc = help_command.doc
-        argument = help_command.arg_table[arg_name]
-        option_str = argument._name
-        if 'nargs' in argument.options:
-            if argument.options['nargs'] == '+':
-                option_str += " <value> [<value>...]"
-            else:
-                for i in range(argument.options['nargs']):
-                    option_str += " <value>"
-        doc.writeln('[--%s]' % option_str)
-
-    def doc_synopsis_end(self, help_command, **kwargs):
-        if help_command.obj._name != 's3':
-            doc = help_command.doc
-            doc.style.end_codeblock()
-
-    def doc_options_start(self, help_command, **kwargs):
-        if help_command.obj._name != 's3':
-            doc = help_command.doc
-            doc.style.h2('Options')
-            if len(help_command.arg_table) == 0:
-                doc.write('*None*\n')
-
-    def doc_option(self, arg_name, help_command, **kwargs):
-        doc = help_command.doc
-        argument = help_command.arg_table[arg_name]
-        doc.write('``--%s``\n' % argument._name)
-        doc.style.indent()
-        doc.include_doc_string(argument.documentation)
-        doc.style.dedent()
-        doc.style.new_paragraph()
-
-    def doc_subitems_start(self, help_command, **kwargs):
-        if help_command.command_table:
-            doc = help_command.doc
-            doc.style.h2('Available Commands')
-            doc.style.toctree()
-
-    def doc_subitem(self, command_name, help_command, **kwargs):
-        doc = help_command.doc
-        doc.style.tocitem(command_name)
-
-
 class S3HelpCommand(HelpCommand):
     """
     This is a wrapper to handle the interactions between the commmand and the
@@ -314,11 +217,10 @@ class S3(object):
     The service for the plugin.
     """
 
-    def __init__(self, name, session, op_table={}):
+    def __init__(self, name, session):
         self._name = name
         self._service_object = S3Service()
         self._session = session
-        self.op_table = op_table
         self.documentation = "This provides higher level S3 commands for " \
                              "the AWS CLI."
 
@@ -327,50 +229,57 @@ def __call__(self, args, parsed_globals):
         This function instantiates the operations table to be filled with
         commands.  Creates a parser based off of the commands in the
         operations table.  Parses the valid arguments and passes the
-        remaining off to a corresponding ``S3Command`` object to be called
+        remaining off to a corresponding ``S3SubCommand`` object to be called
         on.
         """
-        self._create_operations_table()
-        service_parser = self._create_service_parser(self.op_table)
+        subcommand_table = self._create_subcommand_table()
+        service_parser = self._create_service_parser(subcommand_table)
         parsed_args, remaining = service_parser.parse_known_args(args)
-        return self.op_table[parsed_args.operation](remaining, parsed_globals)
+        return subcommand_table[parsed_args.operation](
+            remaining, parsed_globals)
 
-    def _create_service_parser(self, operation_table):
+    def _create_service_parser(self, subcommand_table):
         """
         Creates the parser required to parse the commands on the
         command line
         """
         return ServiceArgParser(
-            operations_table=operation_table, service_name=self._name)
+            operations_table=subcommand_table, service_name=self._name)
 
-    def _create_operations_table(self):
+    def _create_subcommand_table(self):
         """
-        Creates an empty dictionary to be filled with ``S3Command`` objects
+        Creates an empty dictionary to be filled with ``S3SubCommand`` objects
         when the event is emmitted.
         """
+        subcommand_table = {}
+        for cmd in CMD_DICT.keys():
+            subcommand_table[cmd] = S3SubCommand(
+                cmd, self._session, CMD_DICT[cmd]['options'],
+                CMD_DICT[cmd]['description'], CMD_DICT[cmd]['usage'])
 
         self._session.emit('building-operation-table.%s' % self._name,
-                           operation_table=self.op_table,
+                           operation_table=subcommand_table,
                            session=self._session)
-        self.op_table['help'] = S3HelpCommand(self._session, self,
-                                              command_table=self.op_table,
-                                              arg_table=None)
+        subcommand_table['help'] = S3HelpCommand(self._session, self,
+                                                command_table=subcommand_table,
+                                                arg_table=None)
+        return subcommand_table
 
     def create_help_command(self):
         """
         This function returns a help command object with a filled command
         table.  This command is necessary for generating html docs.
         """
-        command_table = {}
-        add_commands(command_table, self._session)
+        subcommand_table = self._create_subcommand_table()
+        del subcommand_table['help']
         return S3HelpCommand(self._session, self,
-                             command_table=command_table,
+                             command_table=subcommand_table,
                              arg_table=None)
 
 
-class S3Command(object):
+class S3SubCommand(object):
     """
-    This is the object corresponding to a S3 command.
+    This is the object corresponding to a S3 subcommand.
     """
 
     def __init__(self, name, session, options, documentation="", usage=""):
@@ -658,6 +567,27 @@ def add_paths(self, paths):
             self.parameters['dest'] = paths[1]
         elif len(paths) == 1:
             self.parameters['dest'] = paths[0]
+        self.check_dest_path(self.parameters['dest'])
+
+    def check_dest_path(self, destination):
+        if destination.startswith('s3://') and \
+                self.cmd in ['ls', 'cp', 'sync', 'mv']:
+            bucket, key = find_bucket_key(destination[5:])
+            # A bucket is not always provided (like 'aws s3 ls')
+            # so only verify the bucket exists if we actually have
+            # a bucket.
+            if bucket:
+                self._verify_bucket_exists(bucket)
+
+    def _verify_bucket_exists(self, bucket_name):
+        session = self.session
+        service = session.get_service('s3')
+        endpoint = service.get_endpoint(self.parameters['region'])
+        operation = service.get_operation('ListObjects')
+        # This will raise an exception if the bucket does not exist.
+        html_response, response_data = operation.call(endpoint,
+                                                      bucket=bucket_name,
+                                                      max_keys=0)
 
     def check_path_type(self, paths):
         """
@@ -671,7 +601,7 @@ def check_path_type(self, paths):
                          'local': [], 'locallocal': []}
         paths_type = ''
         usage = "usage: aws s3 %s %s" % (self.cmd,
-                                         cmd_dict[self.cmd]['usage'])
+                                         CMD_DICT[self.cmd]['usage'])
         for i in range(len(paths)):
             if paths[i].startswith('s3://'):
                 paths_type = paths_type + 's3'
@@ -687,7 +617,7 @@ def check_src_path(self, paths):
         This checks the source paths to deem if they are valid.  The check
         performed in S3 is first it lists the objects using the source path.
         If there is an error like the bucket does not exist, the error will be
-        caught with ``check_error()`` funciton.  If the operation is on a
+        caught with ``check_error()`` function.  If the operation is on a
         single object in s3, it checks that a list of object was returned and
         that the first object listed is the name of the specified in the
         command line.  If the operation is on objects under a common prefix,
@@ -752,7 +682,7 @@ def check_force(self, args, parsed_globals):
                 bucket, key = find_bucket_key(self.parameters['src'][5:])
                 path = 's3://'+bucket
                 try:
-                    del_objects = S3Command('rm', self.session, {'nargs': 1})
+                    del_objects = S3SubCommand('rm', self.session, {'nargs': 1})
                     del_objects([path, '--recursive'], parsed_globals)
                 except:
                     pass
@@ -782,6 +712,3 @@ def check_region(self, parsed_globals):
             self.parameters['region'] = parsed_region
         else:
             self.parameters['region'] = region
-
-
-


Test patch:
diff --git a/tests/integration/__init__.py b/tests/integration/__init__.py
--- a/tests/integration/__init__.py
+++ b/tests/integration/__init__.py
@@ -33,11 +33,13 @@ def __init__(self, rc, stdout, stderr, memory_usage=None):
         self.rc = rc
         self.stdout = stdout
         self.stderr = stderr
+        LOG.debug("rc: %s", rc)
+        LOG.debug("stdout: %s", stdout)
+        LOG.debug("stderr: %s", stderr)
         if memory_usage is None:
             memory_usage = []
         self.memory_usage = memory_usage
 
-
     @property
     def json(self):
         return json.loads(self.stdout)
diff --git a/tests/integration/customizations/s3/test_plugin.py b/tests/integration/customizations/s3/test_plugin.py
--- a/tests/integration/customizations/s3/test_plugin.py
+++ b/tests/integration/customizations/s3/test_plugin.py
@@ -206,6 +206,11 @@ def test_mv_with_large_file(self):
         self.assertTrue(os.path.exists(foo_txt))
         self.assertEqual(os.path.getsize(foo_txt), len(file_contents))
 
+    def test_mv_to_nonexistent_bucket(self):
+        full_path = self.files.create_file('foo.txt', 'this is foo.txt')
+        p = aws('s3 mv %s s3://bad-noexist-13143242/foo.txt' % (full_path,))
+        self.assertEqual(p.rc, 255)
+
 
 class TestCp(BaseS3CLICommand):
 
@@ -260,6 +265,11 @@ def test_download_large_file(self):
         self.assert_no_errors(p)
         self.assertEqual(os.path.getsize(local_foo_txt), len(foo_contents))
 
+    def test_cp_to_nonexistent_bucket(self):
+        foo_txt = self.files.create_file('foo.txt', 'this is foo.txt')
+        p = aws('s3 cp %s s3://noexist-bucket-foo-bar123/foo.txt' % (foo_txt,))
+        self.assertEqual(p.rc, 255)
+
 
 class TestSync(BaseS3CLICommand):
     def test_sync_to_from_s3(self):
@@ -287,6 +297,14 @@ def test_sync_to_from_s3(self):
         with open(bar_txt, 'r') as f:
             self.assertEqual(f.read(), 'bar contents')
 
+    def test_sync_to_nonexistent_bucket(self):
+        foo_txt = self.files.create_file('foo.txt', 'foo contents')
+        bar_txt = self.files.create_file('bar.txt', 'bar contents')
+
+        # Sync the directory and the bucket.
+        p = aws('s3 sync %s s3://noexist-bkt-nme-1412' % (self.files.rootdir,))
+        self.assertEqual(p.rc, 255)
+
 
 class TestUnicode(BaseS3CLICommand):
     """
@@ -337,6 +355,10 @@ def test_ls_bucket(self):
         p = aws('s3 ls')
         self.assert_no_errors(p)
 
+    def test_ls_non_existent_bucket(self):
+        p = aws('s3 ls s3://foobara99842u4wbts829381')
+        self.assertEqual(p.rc, 255)
+
 
 class TestMbRb(BaseS3CLICommand):
     """
diff --git a/tests/unit/__init__.py b/tests/unit/__init__.py
--- a/tests/unit/__init__.py
+++ b/tests/unit/__init__.py
@@ -64,7 +64,7 @@ def _store_params(self, params):
             if isinstance(value, Payload):
                 self.last_params[key] = value.getvalue()
 
-    def assert_params_for_cmd(self, cmd, params, expected_rc=0,
+    def assert_params_for_cmd(self, cmd, params=None, expected_rc=0,
                               stderr_contains=None):
         logging.debug("Calling cmd: %s", cmd)
         driver = create_clidriver()
@@ -85,5 +85,6 @@ def assert_params_for_cmd(self, cmd, params, expected_rc=0,
             rc, expected_rc,
             "Unexpected rc (expected: %s, actual: %s) for command: %s" % (
                 expected_rc, rc, cmd))
-        self.assertDictEqual(params, self.last_params)
+        if params is not None:
+            self.assertDictEqual(params, self.last_params)
         return rc
diff --git a/tests/unit/customizations/s3/test_copy_params.py b/tests/unit/customizations/s3/test_copy_params.py
--- a/tests/unit/customizations/s3/test_copy_params.py
+++ b/tests/unit/customizations/s3/test_copy_params.py
@@ -58,6 +58,8 @@ def _store_params(self, params):
         params['payload'] = None
 
     def register_uri(self):
+        httpretty.register_uri(httpretty.GET, re.compile('.*'), body='',
+                               status_code=200)
         httpretty.register_uri(httpretty.PUT, re.compile('.*'), body='',
                                etag='"120ea8a25e5d487bf68b5f7096440019"',
                                content_length=0)
@@ -155,7 +157,7 @@ def test_grants_bad(self):
         # being processed correctly at the moment.  Need to track
         # this down.
         #self.assert_params_for_cmd(cmdline, {}, expected_rc=0)
-        self.assert_params_for_cmd(cmdline, {}, expected_rc=0)
+        self.assert_params_for_cmd(cmdline, expected_rc=0)
 
     def test_content_type(self):
         cmdline = self.prefix
diff --git a/tests/unit/customizations/s3/test_s3.py b/tests/unit/customizations/s3/test_s3.py
--- a/tests/unit/customizations/s3/test_s3.py
+++ b/tests/unit/customizations/s3/test_s3.py
@@ -16,13 +16,12 @@
 import sys
 from tests import unittest
 
-import awscli
 import botocore.session
 from mock import Mock, MagicMock, patch
 
-from awscli.customizations.s3.s3 import AppendFilter, cmd_dict, \
-    params_dict, awscli_initialize, add_s3, add_commands, add_cmd_params, \
-    S3, S3Command, S3Parameter, CommandArchitecture, CommandParameters
+from awscli.customizations.s3.s3 import AppendFilter, \
+    awscli_initialize, add_s3, add_cmd_params, \
+    S3, S3SubCommand, S3Parameter, CommandArchitecture, CommandParameters
 from tests.unit.customizations.s3 import make_loc_files, clean_loc_files, \
     make_s3_files, s3_cleanup, S3HandlerBaseTest
 from tests.unit.customizations.s3.fake_session import FakeSession
@@ -30,10 +29,6 @@
 
 
 class AppendFilterTest(unittest.TestCase):
-    """
-    This ensures that the custom action produces the correct format
-    for the namespace variable
-    """
     def test_call(self):
         parser = argparse.ArgumentParser()
 
@@ -82,17 +77,6 @@ def test_s3(self):
         for service in self.services.keys():
             self.assertIn(service, ['s3'])
 
-    def test_command(self):
-        """
-        Ensures that all of the commands generated in the table
-        are elgible commands.
-        """
-        self.commands = {}
-        commands_list = ['cp', 'mv', 'rm', 'sync', 'ls', 'mb', 'rb']
-        add_commands(self.commands, True)
-        for command in self.commands.keys():
-            self.assertIn(command, commands_list)
-
     def test_parameters(self):
         """
         Ensures that all of the parameters generated for a specified
@@ -112,24 +96,9 @@ def test_parameters(self):
                 self.assertIn(param, params_list)
 
 
-class S3Test(unittest.TestCase):
+class S3SubCommandTest(unittest.TestCase):
     """
-    This test to ensure the command object can be called from
-    parsing a command in the service object.
-    """
-    def setUp(self):
-        self.mock = MagicMock(return_value='test service')
-
-    def test_call(self):
-        session = botocore.session.get_session()
-        s3 = S3('s3', session, {'s3': self.mock})
-        response = s3(['s3'], [])
-        self.assertEqual(response, "test service")
-
-
-class S3CommandTest(unittest.TestCase):
-    """
-    This checks top make sure that the S3Command properly handles commands
+    This checks top make sure that the S3SubCommand properly handles commands
     passed to it.
     """
     def setUp(self):
@@ -147,7 +116,7 @@ def test_call(self):
         This just checks to make sure no exceptions get thrown for a
         proper command.
         """
-        s3_command = S3Command('ls', self.session, {'nargs': 1})
+        s3_command = S3SubCommand('ls', self.session, {'nargs': 1})
         s3_command(['s3://'], [])
 
     def test_call_error(self):
@@ -155,7 +124,7 @@ def test_call_error(self):
         This checks to make sure an improper command throws an
         exception.
         """
-        s3_command = S3Command('ls', self.session,  {'nargs': 1})
+        s3_command = S3SubCommand('ls', self.session,  {'nargs': 1})
         with self.assertRaises(Exception):
             s3_command(['s3://', '--sfdf'], [])
 
@@ -220,11 +189,9 @@ def test_create_instructions(self):
                                                 's3_handler'])
 
     def test_run_cp_put(self):
-        """
-        This ensures that the architecture sets up correctly for a ``cp`` put
-        command.  It is just just a dry run, but all of the components need
-        to be wired correctly for it to work.
-        """
+        # This ensures that the architecture sets up correctly for a ``cp`` put
+        # command.  It is just just a dry run, but all of the components need
+        # to be wired correctly for it to work.
         s3_file = 's3://' + self.bucket + '/' + 'text1.txt'
         local_file = self.loc_files[0]
         rel_local_file = os.path.relpath(local_file)
@@ -239,11 +206,9 @@ def test_run_cp_put(self):
         self.assertIn(output_str, self.output.getvalue())
 
     def test_run_cp_get(self):
-        """
-        This ensures that the architecture sets up correctly for a ``cp`` get
-        command.  It is just just a dry run, but all of the components need
-        to be wired correctly for it to work.
-        """
+        # This ensures that the architecture sets up correctly for a ``cp`` get
+        # command.  It is just just a dry run, but all of the components need
+        # to be wired correctly for it to work.
         s3_file = 's3://' + self.bucket + '/' + 'text1.txt'
         local_file = self.loc_files[0]
         rel_local_file = os.path.relpath(local_file)
@@ -258,11 +223,9 @@ def test_run_cp_get(self):
         self.assertIn(output_str, self.output.getvalue())
 
     def test_run_cp_copy(self):
-        """
-        This ensures that the architecture sets up correctly for a ``cp`` copy
-        command.  It is just just a dry run, but all of the components need
-        to be wired correctly for it to work.
-        """
+        # This ensures that the architecture sets up correctly for a ``cp`` copy
+        # command.  It is just just a dry run, but all of the components need
+        # to be wired correctly for it to work.
         s3_file = 's3://' + self.bucket + '/' + 'text1.txt'
         filters = [['--include', '*']]
         params = {'dir_op': False, 'dryrun': True, 'quiet': False,
@@ -275,11 +238,9 @@ def test_run_cp_copy(self):
         self.assertIn(output_str, self.output.getvalue())
 
     def test_run_mv(self):
-        """
-        This ensures that the architecture sets up correctly for a ``mv``
-        command.  It is just just a dry run, but all of the components need
-        to be wired correctly for it to work.
-        """
+        # This ensures that the architecture sets up correctly for a ``mv``
+        # command.  It is just just a dry run, but all of the components need
+        # to be wired correctly for it to work.
         s3_file = 's3://' + self.bucket + '/' + 'text1.txt'
         filters = [['--include', '*']]
         params = {'dir_op': False, 'dryrun': True, 'quiet': False,
@@ -292,11 +253,9 @@ def test_run_mv(self):
         self.assertIn(output_str, self.output.getvalue())
 
     def test_run_remove(self):
-        """
-        This ensures that the architecture sets up correctly for a ``rm``
-        command.  It is just just a dry run, but all of the components need
-        to be wired correctly for it to work.
-        """
+        # This ensures that the architecture sets up correctly for a ``rm``
+        # command.  It is just just a dry run, but all of the components need
+        # to be wired correctly for it to work.
         s3_file = 's3://' + self.bucket + '/' + 'text1.txt'
         filters = [['--include', '*']]
         params = {'dir_op': False, 'dryrun': True, 'quiet': False,
@@ -309,11 +268,9 @@ def test_run_remove(self):
         self.assertIn(output_str, self.output.getvalue())
 
     def test_run_sync(self):
-        """
-        This ensures that the architecture sets up correctly for a ``sync``
-        command.  It is just just a dry run, but all of the components need
-        to be wired correctly for it to work.
-        """
+        # This ensures that the architecture sets up correctly for a ``sync``
+        # command.  It is just just a dry run, but all of the components need
+        # to be wired correctly for it to work.
         s3_file = 's3://' + self.bucket + '/' + 'text1.txt'
         local_file = self.loc_files[0]
         s3_prefix = 's3://' + self.bucket + '/'
@@ -330,12 +287,9 @@ def test_run_sync(self):
         self.assertIn(output_str, self.output.getvalue())
 
     def test_run_ls(self):
-        """
-        This ensures that the architecture sets up correctly for a ``ls``
-        command.  It is just just a dry run, but all of the components need
-        to be wired correctly for it to work.
-        """
-        s3_file = 's3://' + self.bucket + '/' + 'text1.txt'
+        # This ensures that the architecture sets up correctly for a ``ls``
+        # command.  It is just just a dry run, but all of the components need
+        # to be wired correctly for it to work.
         s3_prefix = 's3://' + self.bucket + '/'
         params = {'dir_op': True, 'dryrun': True, 'quiet': False,
                   'src': s3_prefix, 'dest': s3_prefix, 'paths_type': 's3',
@@ -345,11 +299,9 @@ def test_run_ls(self):
         cmd_arc.run()
 
     def test_run_mb(self):
-        """
-        This ensures that the architecture sets up correctly for a ``rb``
-        command.  It is just just a dry run, but all of the components need
-        to be wired correctly for it to work.
-        """
+        # This ensures that the architecture sets up correctly for a ``rb``
+        # command.  It is just just a dry run, but all of the components need
+        # to be wired correctly for it to work.
         s3_prefix = 's3://' + self.bucket + '/'
         params = {'dir_op': True, 'dryrun': True, 'quiet': False,
                   'src': s3_prefix, 'dest': s3_prefix, 'paths_type': 's3',
@@ -361,11 +313,9 @@ def test_run_mb(self):
         self.assertIn(output_str, self.output.getvalue())
 
     def test_run_rb(self):
-        """
-        This ensures that the architecture sets up correctly for a ``mb``
-        command.  It is just just a dry run, but all of the components need
-        to be wired correctly for it to work.
-        """
+        # This ensures that the architecture sets up correctly for a ``mb``
+        # command.  It is just just a dry run, but all of the components need
+        # to be wired correctly for it to work.
         s3_prefix = 's3://' + self.bucket + '/'
         params = {'dir_op': True, 'dryrun': True, 'quiet': False,
                   'src': s3_prefix, 'dest': s3_prefix, 'paths_type': 's3',
@@ -393,36 +343,10 @@ def tearDown(self):
         clean_loc_files(self.loc_files)
         s3_cleanup(self.bucket, self.session)
 
-    def test_add_paths(self):
-        """
-        This ensures that the paths are assigned properly in the
-        class's parameters dictionary.
-        """
-        s3_file = 's3://' + self.bucket + '/' + 'text1.txt'
-        path1 = [s3_file, 's3://some_file']
-        path2 = [s3_file]
-        cmd_params = CommandParameters(self.session, 'cp', {})
-        cmd_params2 = CommandParameters(self.session, 'rm', {})
-        cmd_params.check_region([])
-        cmd_params2.check_region([])
-        cmd_params.add_paths(path1)
-        cmd_params2.add_paths(path2)
-        ref_params = {'dir_op': False, 'src': s3_file,
-                      'dest': 's3://some_file',
-                      'region': self.session.get_config()['region'],
-                      'paths_type': 's3s3'}
-        self.assertEqual(cmd_params.parameters, ref_params)
-        ref_params2 = {'dir_op': False, 'src': s3_file, 'dest': s3_file,
-                       'region': self.session.get_config()['region'],
-                       'paths_type': 's3'}
-        self.assertEqual(cmd_params2.parameters, ref_params2)
-
     def test_check_path_type_pass(self):
-        """
-        This tests the class's ability to determine whether the correct
-        path types have been passed for a particular command.  It test every
-        possible combination that is correct for every command.
-        """
+        # This tests the class's ability to determine whether the correct
+        # path types have been passed for a particular command.  It test every
+        # possible combination that is correct for every command.
         cmds = {'cp': ['locals3', 's3s3', 's3local'],
                 'mv': ['locals3', 's3s3', 's3local'],
                 'rm': ['s3'], 'ls': ['s3'], 'mb': ['s3'], 'rb': ['s3'],
@@ -445,12 +369,9 @@ def test_check_path_type_pass(self):
                 cmd_param.check_path_type(combos[path_args])
 
     def test_check_path_type_fail(self):
-        """
-        This tests the class's ability to determine whether the correct
-        path types have been passed for a particular command. It test every
-        possible combination that is incorrect for every command.
-        """
-
+        # This tests the class's ability to determine whether the correct
+        # path types have been passed for a particular command. It test every
+        # possible combination that is incorrect for every command.
         cmds = {'cp': ['local', 'locallocal', 's3'],
                 'mv': ['local', 'locallocal', 's3'],
                 'rm': ['local', 'locallocal', 's3s3', 'locals3', 's3local'],
@@ -477,12 +398,10 @@ def test_check_path_type_fail(self):
                     cmd_param.check_path_type(combos[path_args])
 
     def test_check_src_path_pass(self):
-        """
-        This tests to see if all of the checks on the source path works.  It
-        does so by testing if s3 objects and and prefixes exist as well as
-        local files and directories.  All of these should not throw an
-        exception.
-        """
+        # This tests to see if all of the checks on the source path works.  It
+        # does so by testing if s3 objects and and prefixes exist as well as
+        # local files and directories.  All of these should not throw an
+        # exception.
         s3_file = 's3://' + self.bucket + '/' + 'text1.txt'
         local_file = self.loc_files[0]
         s3_prefix = 's3://' + self.bucket
@@ -502,18 +421,15 @@ def test_check_src_path_pass(self):
             cmd_parameter.check_src_path(filename[0])
 
     def test_check_src_path_fail(self):
-        """
-        This tests to see if all of the checks on the source path works.  It
-        does so by testing if s3 objects and and prefixes do not exist as well
-        as local files and directories.  All of these should throw an
-        exception.
-        """
+        # This tests to see if all of the checks on the source path works.  It
+        # does so by testing if s3 objects and and prefixes do not exist as well
+        # as local files and directories.  All of these should throw an
+        # exception.
         local_file = self.loc_files[0]
         local_dir = self.loc_files[3]
         fake_s3_file = 's3://' + self.bucket + '/' + 'text1.tx'
         fake_local_file = local_file[:-1]
         fake_s3_prefix = 's3://' + self.bucket + '/' + 'fake/'
-        fake_local_dir = local_dir + os.sep + 'fake' + os.sep
 
         # :var files: a list of tuples where the first element is a single
         #     element list of file paths. The second element is a boolean
@@ -531,21 +447,17 @@ def test_check_src_path_fail(self):
                 cmd_parameter.check_src_path(filename[0])
 
     def test_check_force(self):
-        """
-        This checks to make sure that the force parameter is run. If
-        successful. The delete command will fail as the bucket is empty
-        and be caught by the exception.
-        """
+        # This checks to make sure that the force parameter is run. If
+        # successful. The delete command will fail as the bucket is empty
+        # and be caught by the exception.
         cmd_params = CommandParameters(self.session, 'rb', {'force': True})
         cmd_params.parameters['src'] = 's3://mybucket'
         cmd_params.check_force(None, None)
 
     def test_region(self):
-        """
-        This tests the ability to specify the region and throw an error
-        if a region is never specified whether if it is an environment
-        variable, config file, or parsed global.
-        """
+        # This tests the ability to specify the region and throw an error
+        # if a region is never specified whether if it is an environment
+        # variable, config file, or parsed global.
         cmd_params = CommandParameters(self.session, 'mb', {})
         parser = argparse.ArgumentParser()
         parser.add_argument('--region', nargs=1)
@@ -569,11 +481,9 @@ def tearDown(self):
         super(HelpDocTest, self).tearDown()
 
     def test_s3_help(self):
-        """
-        This tests the help command for the s3 service. This
-        checks to make sure the appropriate descriptions are
-        added including the tutorial.
-        """
+        # This tests the help command for the s3 service. This
+        # checks to make sure the appropriate descriptions are
+        # added including the tutorial.
         s3 = S3('s3', self.session)
         parser = argparse.ArgumentParser()
         parser.add_argument('--paginate', action='store_true')
@@ -585,13 +495,11 @@ def test_s3_help(self):
         self.assert_contains("* rb")
 
     def test_s3command_help(self):
-        """
-        This tests the help command for an s3 command. This
-        checks to make sure the command prints appropriate
-        parts.  Note the examples are not included because
-        the event was not registered.
-        """
-        s3command = S3Command('cp', self.session, {'nargs': 2})
+        # This tests the help command for an s3 command. This
+        # checks to make sure the command prints appropriate
+        # parts.  Note the examples are not included because
+        # the event was not registered.
+        s3command = S3SubCommand('cp', self.session, {'nargs': 2})
         parser = argparse.ArgumentParser()
         parser.add_argument('--paginate', action='store_true')
         parsed_global = parser.parse_args(['--paginate'])
@@ -602,16 +510,15 @@ def test_s3command_help(self):
         self.assert_contains("Displays the operations that would be")
 
     def test_help(self):
-        """
-        This ensures that the file appropriately redirects to help object
-        if help is the only argument left to be parsed.  There should not
-        have any contents in the docs.
-        """
-        s3_command = S3Command('sync', self.session, {'nargs': 2})
+        # This ensures that the file appropriately redirects to help object
+        # if help is the only argument left to be parsed.  There should not
+        # have any contents in the docs.
+        s3_command = S3SubCommand('sync', self.session, {'nargs': 2})
         s3_command(['help'], [])
         self.assert_contains('sync')
         self.assert_contains("Synopsis")
 
+
 if __name__ == "__main__":
     unittest.main()