Repository: boto/botocore
Pull request #442 (2015-01-26 21:41:32)

Add compat for legacy operation methods on waiters
While pulling in https://github.com/boto/botocore/pull/437,
I noticed that we aren't handling the 'error' matcher properly
because of a CLI customization that will raise an error on
non 200 responses.  This handles that case so, for example, the
command
`aws rds wait db-instance-deleted --db-instance-identifier foo`
will work as expected.  Previously, the error was being propogated
and the CLI command would exit with a non-zero RC and a client error
message.

cc @kyleknap @danielgtaylor 


Patch:
diff --git a/botocore/waiter.py b/botocore/waiter.py
index b49c9a19b..fbc425ecf 100644
--- a/botocore/waiter.py
+++ b/botocore/waiter.py
@@ -96,10 +96,28 @@ class LegacyOperationMethod(object):
         self._endpoint = endpoint
 
     def __call__(self, **kwargs):
-        http, parsed = self._operation_object.call(
-            self._endpoint, **kwargs)
+        try:
+            http, parsed = self._operation_object.call(
+                self._endpoint, **kwargs)
+        except Exception as e:
+            # In theory, a handler can raise an type of exception.
+            # We're going to make a best effort attempt to handle
+            # the ClientError attributes, but not require that
+            # the exception is an instance of ClientError.
+            if self._looks_like_client_error(e):
+                return {
+                    'Error': {
+                        'Code': e.error_code,
+                        'Message': e.error_message,
+                    }
+                }
+            else:
+                raise
         return parsed
 
+    def _looks_like_client_error(self, e):
+        return hasattr(e, 'error_code') and hasattr(e, 'error_message')
+
 
 class WaiterModel(object):
     SUPPORTED_VERSION = 2
@@ -317,6 +335,8 @@ class Waiter(object):
                     raise WaiterError(name=self.name,
                                       reason='Unexpected error encountered.')
             if current_state == 'success':
+                logger.debug("Waiting complete, waiter matched the "
+                             "success state.")
                 return
             if current_state == 'failure':
                 raise WaiterError(


Test patch:
diff --git a/tests/unit/test_waiters.py b/tests/unit/test_waiters.py
index a1a74468d..4d11a33f0 100644
--- a/tests/unit/test_waiters.py
+++ b/tests/unit/test_waiters.py
@@ -508,6 +508,28 @@ class TestOperationMethods(unittest.TestCase):
         operation_object.call.assert_called_with(
             endpoint, Foo='a', Bar='b')
 
+    def test_legacy_method_handles_exceptions(self):
+        operation_object = mock.Mock()
+        exception = Exception()
+        exception.error_message = 'Foo'
+        exception.error_code = 'MyCode'
+        operation_object.call.side_effect = exception
+        endpoint = mock.Mock()
+        op = LegacyOperationMethod(operation_object, endpoint)
+        response = op(Foo='a', Bar='b')
+        self.assertEqual(response,
+                         {'Error': {'Code': 'MyCode', 'Message': 'Foo'}})
+
+    def test_legacy_method_with_unknown_exception(self):
+        operation_object = mock.Mock()
+        # A generic exception missing the error_message and error_code
+        # attrs will just be reraised.
+        operation_object.call.side_effect = ValueError
+        endpoint = mock.Mock()
+        op = LegacyOperationMethod(operation_object, endpoint)
+        with self.assertRaises(ValueError):
+            op(Foo='a', Bar='b')
+
 
 class ServiceWaiterFunctionalTest(BaseEnvVar):
     """