Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions botocore/waiter.py
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,31 @@ def __init__(self, name, config, operation_method):
self.name = name
self.config = config

@staticmethod
def _validate_waiter_config(sleep_amount, max_attempts):
if (
isinstance(sleep_amount, bool)
or not isinstance(sleep_amount, (int, float))
or sleep_amount < 0
):
raise WaiterConfigError(
error_msg=(
f'Invalid value for WaiterConfig option Delay: '
f'{sleep_amount!r}. Expected a non-negative number.'
)
)
if (
isinstance(max_attempts, bool)
or not isinstance(max_attempts, int)
or max_attempts < 1
):
raise WaiterConfigError(
error_msg=(
f'Invalid value for WaiterConfig option MaxAttempts: '
f'{max_attempts!r}. Expected a positive integer.'
)
)

@with_current_context(partial(register_feature_id, 'WAITER'))
def wait(self, **kwargs):
acceptors = list(self.config.acceptors)
Expand All @@ -342,6 +367,7 @@ def wait(self, **kwargs):
config = kwargs.pop('WaiterConfig', {})
sleep_amount = config.get('Delay', self.config.delay)
max_attempts = config.get('MaxAttempts', self.config.max_attempts)
self._validate_waiter_config(sleep_amount, max_attempts)
last_matched_acceptor = None
num_attempts = 0

Expand Down
102 changes: 102 additions & 0 deletions tests/unit/test_waiters.py
Original file line number Diff line number Diff line change
Expand Up @@ -732,6 +732,108 @@ def test_waiter_invocation_config_honors_max_attempts(self):

self.assertEqual(operation_method.call_count, 2)

def test_waiter_config_rejects_negative_delay(self):
config = self.create_waiter_config()
operation_method = mock.Mock()
waiter = Waiter('MyWaiter', config, operation_method)
with self.assertRaises(WaiterConfigError):
waiter.wait(WaiterConfig={'Delay': -1})
operation_method.assert_not_called()

def test_waiter_config_rejects_non_numeric_delay(self):
config = self.create_waiter_config()
operation_method = mock.Mock()
waiter = Waiter('MyWaiter', config, operation_method)
with self.assertRaises(WaiterConfigError):
waiter.wait(WaiterConfig={'Delay': 'fast'})
operation_method.assert_not_called()

def test_waiter_config_rejects_zero_max_attempts(self):
config = self.create_waiter_config()
operation_method = mock.Mock()
waiter = Waiter('MyWaiter', config, operation_method)
with self.assertRaises(WaiterConfigError):
waiter.wait(WaiterConfig={'MaxAttempts': 0})
operation_method.assert_not_called()

def test_waiter_config_rejects_negative_max_attempts(self):
config = self.create_waiter_config()
operation_method = mock.Mock()
waiter = Waiter('MyWaiter', config, operation_method)
with self.assertRaises(WaiterConfigError):
waiter.wait(WaiterConfig={'MaxAttempts': -1})
operation_method.assert_not_called()

def test_waiter_config_rejects_non_integer_max_attempts(self):
config = self.create_waiter_config()
operation_method = mock.Mock()
waiter = Waiter('MyWaiter', config, operation_method)
with self.assertRaises(WaiterConfigError):
waiter.wait(WaiterConfig={'MaxAttempts': 'abc'})
operation_method.assert_not_called()

def test_waiter_config_rejects_none_delay(self):
config = self.create_waiter_config()
operation_method = mock.Mock()
waiter = Waiter('MyWaiter', config, operation_method)
with self.assertRaises(WaiterConfigError):
waiter.wait(WaiterConfig={'Delay': None})
operation_method.assert_not_called()

def test_waiter_config_rejects_boolean_delay(self):
config = self.create_waiter_config()
operation_method = mock.Mock()
waiter = Waiter('MyWaiter', config, operation_method)
with self.assertRaises(WaiterConfigError):
waiter.wait(WaiterConfig={'Delay': True})
operation_method.assert_not_called()

def test_waiter_config_rejects_boolean_max_attempts(self):
config = self.create_waiter_config()
operation_method = mock.Mock()
waiter = Waiter('MyWaiter', config, operation_method)
with self.assertRaises(WaiterConfigError):
waiter.wait(WaiterConfig={'MaxAttempts': True})
operation_method.assert_not_called()

def test_waiter_config_rejects_float_max_attempts(self):
config = self.create_waiter_config()
operation_method = mock.Mock()
waiter = Waiter('MyWaiter', config, operation_method)
with self.assertRaises(WaiterConfigError):
waiter.wait(WaiterConfig={'MaxAttempts': 1.5})
operation_method.assert_not_called()

def test_waiter_config_accepts_valid_values(self):
config = self.create_waiter_config(
acceptors=[
{'state': 'success', 'matcher': 'status', 'expected': 200},
],
)
operation_method = mock.Mock()
self.client_responses_are(
{'ResponseMetadata': {'HTTPStatusCode': 200}},
for_operation=operation_method,
)
waiter = Waiter('MyWaiter', config, operation_method)
waiter.wait(WaiterConfig={'Delay': 0, 'MaxAttempts': 1})
self.assertEqual(operation_method.call_count, 1)

def test_waiter_config_accepts_float_delay(self):
config = self.create_waiter_config(
acceptors=[
{'state': 'success', 'matcher': 'status', 'expected': 200},
],
)
operation_method = mock.Mock()
self.client_responses_are(
{'ResponseMetadata': {'HTTPStatusCode': 200}},
for_operation=operation_method,
)
waiter = Waiter('MyWaiter', config, operation_method)
waiter.wait(WaiterConfig={'Delay': 0.5, 'MaxAttempts': 1})
self.assertEqual(operation_method.call_count, 1)


class TestCreateWaiter(unittest.TestCase):
def setUp(self):
Expand Down