Repository: boto/botocore
Pull request #643 (2015-09-03 22:34:11)

Waiter docstrings
This adds docstrings to waiter's ``wait`` method. It also renames the waiter class returned from client to a class that falls in line with what is in the documentation e.g. ``S3.Waiter.bucket_exists``.
```py

In [1]: import botocore

In [2]: session = botocore.session.get_session()

In [3]: client = session.create_client('s3')

In [4]: waiter = client.get_waiter('bucket_exists')

In [5]: waiter.__class__.__name__
Out[5]: 'S3.Waiter.bucket_exists'

In [6]: help(waiter.wait)
```

Note that it includes: https://github.com/boto/botocore/pull/642 so that can be ignored in the diff
I will update this PR once it gets merged so that it will get excluded.

cc @jamesls @mtdowling @rayluo 


jamesls: Just curious, what are the perf differences with this change?

Also, I think it would be worth adding a resource leak test to ensure that we don't leak memory when creating many waiters in a loop.  Especially now that we're generating docs/classes on demand.
kyleknap: Good point on the performance test. I will do some testing. And add some tests to perf test suite no matter what I find.

Patch:
diff --git a/botocore/client.py b/botocore/client.py
index 38717ef25..995fb776b 100644
--- a/botocore/client.py
+++ b/botocore/client.py
@@ -27,7 +27,7 @@ from botocore.paginate import Paginator
 from botocore.signers import RequestSigner
 from botocore.utils import CachedProperty
 from botocore.utils import get_service_module_name
-from botocore.docs.method import LazyLoadedDocstring
+from botocore.docs.docstring import ClientMethodDocstring
 
 
 logger = logging.getLogger(__name__)
@@ -271,7 +271,7 @@ class ClientCreator(object):
 
         # Add the docstring to the client method
         operation_model = service_model.operation_model(operation_name)
-        docstring = LazyLoadedDocstring(
+        docstring = ClientMethodDocstring(
             operation_model=operation_model,
             method_name=operation_name,
             event_emitter=self._event_emitter,
diff --git a/botocore/docs/docstring.py b/botocore/docs/docstring.py
new file mode 100644
index 000000000..de07e83e3
--- /dev/null
+++ b/botocore/docs/docstring.py
@@ -0,0 +1,90 @@
+# Copyright 2015 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 botocore.docs.method import document_model_driven_method
+from botocore.docs.waiter import document_wait_method
+from botocore.docs.bcdoc.restdoc import DocumentStructure
+
+
+class LazyLoadedDocstring(str):
+    """Used for lazily loading docstrings
+
+    You can instantiate this class and assign it to a __doc__ value.
+    The docstring will not be generated till accessed via __doc__ or
+    help(). Note that all docstring classes **must** subclass from
+    this class. It cannot be used directly as a docstring.
+    """
+    def __init__(self, *args, **kwargs):
+        """
+        The args and kwargs are the same as the underlying document
+        generation function. These just get proxied to the underlying
+        function.
+        """
+        super(LazyLoadedDocstring, self).__init__()
+        self._gen_args = args
+        self._gen_kwargs = kwargs
+        self._docstring = None
+
+    def __new__(cls, *args, **kwargs):
+        # Needed in order to sub class from str with args and kwargs
+        return super(LazyLoadedDocstring, cls).__new__(cls)
+
+    def _write_docstring(self, *args, **kwargs):
+        raise NotImplementedError(
+            '_write_docstring is not implemented. Please subclass from '
+            'this class and provide your own _write_docstring method'
+        )
+
+    def expandtabs(self, tabsize=8):
+        """Expands tabs to spaces
+
+        So this is a big hack in order to get lazy loaded docstring work
+        for the ``help()``. In the ``help()`` function, ``pydoc`` and
+        ``inspect`` are used. At some point the ``inspect.cleandoc``
+        method is called. To clean the docs ``expandtabs`` is called
+        and that is where we override the method to generate and return the
+        docstrings.
+        """
+        if self._docstring is None:
+            self._generate()
+        return self._docstring.expandtabs(tabsize)
+
+    def __str__(self):
+        return self._generate()
+
+    # __doc__ of target will use either __repr__ or __str__ of this class.
+    __repr__ = __str__
+
+    def _generate(self):
+        # Generate the docstring if it is not already cached.
+        if self._docstring is None:
+            self._docstring = self._create_docstring()
+        return self._docstring
+
+    def _create_docstring(self):
+        docstring_structure = DocumentStructure('docstring')
+        # Call the document method function with the args and kwargs
+        # passed to the class.
+        self._write_docstring(
+            docstring_structure, *self._gen_args,
+            **self._gen_kwargs)
+        return docstring_structure.flush_structure().decode('utf-8')
+
+
+class ClientMethodDocstring(LazyLoadedDocstring):
+    def _write_docstring(self, *args, **kwargs):
+        document_model_driven_method(*args, **kwargs)
+
+
+class WaiterDocstring(LazyLoadedDocstring):
+    def _write_docstring(self, *args, **kwargs):
+        document_wait_method(*args, **kwargs)
diff --git a/botocore/docs/method.py b/botocore/docs/method.py
index eaa4ccbc1..819b9b5b4 100644
--- a/botocore/docs/method.py
+++ b/botocore/docs/method.py
@@ -16,7 +16,6 @@ from botocore.docs.params import RequestParamsDocumenter
 from botocore.docs.params import ResponseParamsDocumenter
 from botocore.docs.example import ResponseExampleDocumenter
 from botocore.docs.example import RequestExampleDocumenter
-from botocore.docs.bcdoc.restdoc import DocumentStructure
 
 
 def get_instance_public_methods(instance):
@@ -236,62 +235,3 @@ def document_model_driven_method(section, method_name, operation_model,
                 include=include_output, exclude=exclude_output)
     else:
         return_section.write(':returns: None')
-
-
-class LazyLoadedDocstring(str):
-    """Used for lazily loading docstrings
-
-    You can instantiate this class and assign it to a __doc__ value.
-    The docstring will not be generated till accessed via __doc__ or
-    help().
-    """
-    def __init__(self, *args, **kwargs):
-        """
-        The args and kwargs are the same as the underlying document
-        generation function. These just get proxied to the underlying
-        function.
-        """
-        super(LazyLoadedDocstring, self).__init__()
-        self._gen_args = args
-        self._gen_kwargs = kwargs
-        self._docstring = None
-        self._docstring_writer = document_model_driven_method
-
-    def __new__(cls, *args, **kwargs):
-        # Needed in order to sub class from str with args and kwargs
-        return super(LazyLoadedDocstring, cls).__new__(cls)
-
-    def expandtabs(self, tabsize=8):
-        """Expands tabs to spaces
-
-        So this is a big hack in order to get lazy loaded docstring work
-        for the ``help()``. In the ``help()`` function, ``pydoc`` and
-        ``inspect`` are used. At some point the ``inspect.cleandoc``
-        method is called. To clean the docs ``expandtabs`` is called
-        and that is where we override the method to generate and return the
-        docstrings.
-        """
-        if self._docstring is None:
-            self._generate().expandtabs(tabsize)
-        return self._docstring.expandtabs(tabsize)
-
-    def __str__(self):
-        return self._generate()
-
-    # __doc__ of target will use either __repr__ or __str__ of this class.
-    __repr__ = __str__
-
-    def _generate(self):
-        # Generate the docstring if it is not already cached.
-        if self._docstring is None:
-            self._docstring = self._create_docstring()
-        return self._docstring
-
-    def _create_docstring(self):
-        docstring_structure = DocumentStructure('docstring')
-        # Call the document method function with the args and kwargs
-        # passed to the class.
-        self._docstring_writer(
-            docstring_structure, *self._gen_args,
-            **self._gen_kwargs)
-        return docstring_structure.flush_structure().decode('utf-8')
diff --git a/botocore/docs/waiter.py b/botocore/docs/waiter.py
index e588e8408..2fd3ec758 100644
--- a/botocore/docs/waiter.py
+++ b/botocore/docs/waiter.py
@@ -11,6 +11,7 @@
 # ANY KIND, either express or implied. See the License for the specific
 # language governing permissions and limitations under the License.
 from botocore import xform_name
+from botocore.utils import get_service_module_name
 from botocore.docs.method import document_model_driven_method
 
 
@@ -31,14 +32,14 @@ class WaiterDocumenter(object):
         for waiter_name in self._service_waiter_model.waiter_names:
             section.style.li(
                 ':py:class:`%s.Waiter.%s`' % (
-                    self._client.__class__.__name__, xform_name(waiter_name)))
+                    self._client.__class__.__name__, waiter_name))
             self._add_single_waiter(section, waiter_name)
 
     def _add_single_waiter(self, section, waiter_name):
         section = section.add_new_section(waiter_name)
         section.style.start_sphinx_py_class(
             class_name='%s.Waiter.%s' % (
-                self._client.__class__.__name__, xform_name(waiter_name)))
+                self._client.__class__.__name__, waiter_name))
 
         # Add example on how to instantiate waiter.
         section.style.start_codeblock()
@@ -50,26 +51,51 @@ class WaiterDocumenter(object):
 
         # Add information on the wait() method
         section.style.new_line()
-        self._add_wait_method(section, waiter_name)
-
-    def _add_wait_method(self, section, waiter_name):
-        waiter_model = self._service_waiter_model.get_waiter(waiter_name)
-        operation_model = self._client.meta.service_model.operation_model(
-            waiter_model.operation)
-
-        wait_description = (
-            'This polls :py:meth:`{0}.Client.{1}` every {2} '
-            'seconds until a successful state is reached. An error is '
-            'returned after {3} failed checks.'.format(
-                self._client.__class__.__name__,
-                xform_name(waiter_model.operation),
-                waiter_model.delay, waiter_model.max_attempts)
-        )
-
-        document_model_driven_method(
-            section, 'wait', operation_model,
+        document_wait_method(
+            section=section,
+            waiter_name=waiter_name,
             event_emitter=self._client.meta.events,
-            method_description=wait_description,
-            example_prefix='waiter.wait',
-            document_output=False
+            service_model=self._client.meta.service_model,
+            service_waiter_model=self._service_waiter_model
         )
+
+
+def document_wait_method(section, waiter_name, event_emitter,
+                         service_model, service_waiter_model,
+                         include_signature=True):
+    """Documents a the wait method of a waiter
+
+    :param section: The section to write to
+
+    :param waiter_name: The name of the waiter
+
+    :param event_emitter: The event emitter to use to emit events
+
+    :param service_model: The service model
+
+    :param service_waiter_model: The waiter model associated to the service
+
+    :param include_signature: Whether or not to include the signature.
+        It is useful for generating docstrings.
+    """
+    waiter_model = service_waiter_model.get_waiter(waiter_name)
+    operation_model = service_model.operation_model(
+        waiter_model.operation)
+
+    wait_description = (
+        'Polls :py:meth:`{0}.Client.{1}` every {2} '
+        'seconds until a successful state is reached. An error is '
+        'returned after {3} failed checks.'.format(
+            get_service_module_name(service_model),
+            xform_name(waiter_model.operation),
+            waiter_model.delay, waiter_model.max_attempts)
+    )
+
+    document_model_driven_method(
+        section, 'wait', operation_model,
+        event_emitter=event_emitter,
+        method_description=wait_description,
+        example_prefix='waiter.wait',
+        document_output=False,
+        include_signature=include_signature
+    )
diff --git a/botocore/waiter.py b/botocore/waiter.py
index 91c49d0d5..8e4568d7d 100644
--- a/botocore/waiter.py
+++ b/botocore/waiter.py
@@ -14,6 +14,8 @@ import jmespath
 import logging
 import time
 
+from botocore.utils import get_service_module_name
+from botocore.docs.docstring import WaiterDocstring
 from .exceptions import WaiterError, ClientError, WaiterConfigError
 from . import xform_name
 
@@ -43,7 +45,32 @@ def create_waiter_with_client(waiter_name, waiter_model, client):
     operation_name = xform_name(single_waiter_config.operation)
     operation_method = NormalizedOperationMethod(
         getattr(client, operation_name))
-    return Waiter(
+
+    # Create a new wait method that will serve as a proxy to the underlying
+    # Waiter.wait method. This is needed to attach a docstring to the
+    # method.
+    def wait(self, **kwargs):
+        Waiter.wait(self, **kwargs)
+
+    wait.__doc__ = WaiterDocstring(
+        waiter_name=waiter_name,
+        event_emitter=client.meta.events,
+        service_model=client.meta.service_model,
+        service_waiter_model=waiter_model,
+        include_signature=False
+    )
+
+    # Rename the waiter class based on the type of waiter.
+    waiter_class_name = str('%s.Waiter.%s' % (
+        get_service_module_name(client.meta.service_model),
+        waiter_name))
+
+    # Create the new waiter class
+    documented_waiter_cls = type(
+        waiter_class_name, (Waiter,), {'wait': wait})
+
+    # Return an instance of the new waiter class.
+    return documented_waiter_cls(
         waiter_name, single_waiter_config, operation_method
     )
 


Test patch:
diff --git a/tests/cmd-runner b/tests/cmd-runner
index 74bbbe194..3b1cb8869 100644
--- a/tests/cmd-runner
+++ b/tests/cmd-runner
@@ -77,6 +77,7 @@ class CommandRunner(object):
     def __init__(self):
         self._session = botocore.session.get_session()
         self._clients = []
+        self._waiters = []
 
     def run(self):
         while True:
@@ -112,6 +113,27 @@ class CommandRunner(object):
         # Frees the memory associated with all clients.
         self._clients = []
 
+    def _do_create_waiter(self, args):
+        # Args:
+        # [0] client name used in its instantiation
+        # [1] waiter name used in its instantiation
+        client = self._session.create_client(args[0])
+        self._waiters.append(client.get_waiter(args[1]))
+
+    def _do_create_multiple_waiters(self, args):
+        # Args:
+        # [0] num_waiters - Number of clients to create in a loop
+        # [1] client name used in its instantiation
+        # [2] waiter name used in its instantiation
+        num_waiters = args[0]
+        client = self._session.create_client(args[1])
+        for _ in range(int(num_waiters)):
+            self._waiters.append(client.get_waiter(args[2]))
+
+    def _do_free_waiters(self, args):
+        # Frees the memory associated with all of the waiters.
+        self._waiters = []
+
     def _do_make_aws_request(self, args):
         # Create a client and make a number of AWS requests.
         # Args:
diff --git a/tests/functional/leak/test_resource_leaks.py b/tests/functional/leak/test_resource_leaks.py
index cedaab7e4..bc1a7b20b 100644
--- a/tests/functional/leak/test_resource_leaks.py
+++ b/tests/functional/leak/test_resource_leaks.py
@@ -56,3 +56,29 @@ class TestDoesNotLeakMemory(BaseClientDriverTest):
         self.record_memory()
         start, end = self.memory_samples
         self.assertTrue((end - start) < self.MAX_GROWTH_BYTES, (end - start))
+
+    def test_create_single_waiter_memory_constant(self):
+        self.cmd('create_waiter', 's3', 'bucket_exists')
+        self.cmd('free_waiters')
+        self.record_memory()
+        for _ in range(100):
+            self.cmd('create_waiter', 's3', 'bucket_exists')
+            self.cmd('free_waiters')
+        self.record_memory()
+        start, end = self.memory_samples
+        self.assertTrue((end - start) < self.MAX_GROWTH_BYTES, (end - start))
+
+    def test_create_memory_waiters_in_loop(self):
+        # See ``test_create_memory_clients_in_loop`` to understand why
+        # waiters are first initialized and then freed. Same reason applies.
+        self.cmd('create_multiple_waiters', '200', 's3', 'bucket_exists')
+        self.cmd('free_waiters')
+        self.record_memory()
+        # 500 waiters in batches of 50.
+        for _ in range(10):
+            self.cmd(
+                'create_multiple_waiters', '50', 's3', 'bucket_exists')
+            self.cmd('free_waiters')
+        self.record_memory()
+        start, end = self.memory_samples
+        self.assertTrue((end - start) < self.MAX_GROWTH_BYTES, (end - start))
diff --git a/tests/unit/docs/test_docstring.py b/tests/unit/docs/test_docstring.py
new file mode 100644
index 000000000..0f3073c74
--- /dev/null
+++ b/tests/unit/docs/test_docstring.py
@@ -0,0 +1,85 @@
+# Copyright 2015 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 tests import unittest, mock
+from botocore.docs.docstring import LazyLoadedDocstring
+from botocore.docs.docstring import ClientMethodDocstring
+from botocore.docs.docstring import WaiterDocstring
+
+
+class MockedLazyLoadedDocstring(LazyLoadedDocstring):
+    def __init__(self, *args, **kwargs):
+        super(MockedLazyLoadedDocstring, self).__init__(*args, **kwargs)
+        self.mocked_writer_method = mock.Mock()
+
+    def _write_docstring(self, *args, **kwargs):
+        self.mocked_writer_method(*args, **kwargs)
+
+
+class TestLazyLoadedDocstring(unittest.TestCase):
+    def test_raises_not_implemented(self):
+        with self.assertRaises(NotImplementedError):
+            str(LazyLoadedDocstring())
+
+    def test_expandtabs(self):
+        docstring = MockedLazyLoadedDocstring()
+        docstring.mocked_writer_method.side_effect = (
+            lambda section: section.write('foo\t'))
+        self.assertEqual('foo ', docstring.expandtabs(1))
+
+    def test_str(self):
+        docstring = MockedLazyLoadedDocstring()
+        docstring.mocked_writer_method.side_effect = (
+            lambda section: section.write('foo'))
+        self.assertEqual('foo', str(docstring))
+
+    def test_repr(self):
+        docstring = MockedLazyLoadedDocstring()
+        docstring.mocked_writer_method.side_effect = (
+            lambda section: section.write('foo'))
+        self.assertEqual('foo', repr(docstring))
+
+    def test_is_lazy_loaded(self):
+        docstring = MockedLazyLoadedDocstring()
+        str(docstring)
+        str(docstring)
+        # The mock.ANY represents the DocumentStructure that is filled out.
+        docstring.mocked_writer_method.assert_called_once_with(mock.ANY)
+
+    def test_args_kwargs_passed(self):
+        args = ['foo', 'bar']
+        kwargs = {'biz': 'baz'}
+        docstring = MockedLazyLoadedDocstring(*args, **kwargs)
+        str(docstring)
+        # The mock.ANY represents the DocumentStructure that is filled out.
+        docstring.mocked_writer_method.assert_called_with(
+            mock.ANY, *args, **kwargs)
+
+
+class TestClientMethodDocstring(unittest.TestCase):
+    def test_use_correct_docstring_writer(self):
+        with mock.patch(
+                'botocore.docs.docstring'
+                '.document_model_driven_method') as mock_writer:
+            docstring = ClientMethodDocstring()
+            str(docstring)
+            self.assertTrue(mock_writer.called)
+
+
+class TestWaiterDocstring(unittest.TestCase):
+    def test_use_correct_docstring_writer(self):
+        with mock.patch(
+                'botocore.docs.docstring'
+                '.document_wait_method') as mock_writer:
+            docstring = WaiterDocstring()
+            str(docstring)
+            self.assertTrue(mock_writer.called)
diff --git a/tests/unit/docs/test_method.py b/tests/unit/docs/test_method.py
index aa7221b5e..db9b8df66 100644
--- a/tests/unit/docs/test_method.py
+++ b/tests/unit/docs/test_method.py
@@ -10,7 +10,7 @@
 # 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 tests import unittest, mock
+from tests import unittest
 from tests.unit.docs import BaseDocsTest
 from botocore.hooks import HierarchicalEmitter
 from botocore.docs.method import document_model_driven_signature
@@ -18,7 +18,6 @@ from botocore.docs.method import document_custom_signature
 from botocore.docs.method import document_custom_method
 from botocore.docs.method import document_model_driven_method
 from botocore.docs.method import get_instance_public_methods
-from botocore.docs.method import LazyLoadedDocstring
 from botocore.docs.utils import DocumentedShape
 
 
@@ -302,44 +301,3 @@ class TestDocumentModelDrivenMethod(BaseDocsTest):
             '\'Bar\': \'string\'',
             '- **Bar** *(string) --*',
         ])
-
-
-class TestLazyLoadedDocstring(unittest.TestCase):
-    def setUp(self):
-        self.document_method_patch = mock.patch(
-            'botocore.docs.method.document_model_driven_method')
-        self.document_method = self.document_method_patch.start()
-
-    def tearDown(self):
-        self.document_method_patch.stop()
-
-    def test_expandtabs(self):
-        self.document_method.side_effect = lambda section: section.write(
-            'foo\t')
-        docstring = LazyLoadedDocstring()
-        self.assertEqual('foo ', docstring.expandtabs(1))
-
-    def test_str(self):
-        self.document_method.side_effect = lambda section: section.write('foo')
-        docstring = LazyLoadedDocstring()
-        self.assertEqual('foo', str(docstring))
-
-    def test_repr(self):
-        self.document_method.side_effect = lambda section: section.write('foo')
-        docstring = LazyLoadedDocstring()
-        self.assertEqual('foo', repr(docstring))
-
-    def test_is_lazy_loaded(self):
-        docstring = LazyLoadedDocstring()
-        str(docstring)
-        str(docstring)
-        # The mock.ANY represents the DocumentStructure that is filled out.
-        self.document_method.assert_called_once_with(mock.ANY)
-
-    def test_args_kwargs_passed(self):
-        args = ['foo', 'bar']
-        kwargs = {'biz': 'baz'}
-        docstring = LazyLoadedDocstring(*args, **kwargs)
-        str(docstring)
-        # The mock.ANY represents the DocumentStructure that is filled out.
-        self.document_method.assert_called_with(mock.ANY, *args, **kwargs)
diff --git a/tests/unit/docs/test_service.py b/tests/unit/docs/test_service.py
index 0336eb2d7..99891bee5 100644
--- a/tests/unit/docs/test_service.py
+++ b/tests/unit/docs/test_service.py
@@ -54,7 +54,7 @@ class TestServiceDocumenter(BaseDocsTest):
             '=======',
             'Waiters',
             '=======',
-            '.. py:class:: MyService.Waiter.sample_operation_complete',
+            '.. py:class:: MyService.Waiter.SampleOperationComplete',
             '  .. py:method:: wait(**kwargs)'
         ]
         for line in lines:
diff --git a/tests/unit/docs/test_waiter.py b/tests/unit/docs/test_waiter.py
index 64f04d31f..23459d4c2 100644
--- a/tests/unit/docs/test_waiter.py
+++ b/tests/unit/docs/test_waiter.py
@@ -32,12 +32,12 @@ class TestWaiterDocumenter(BaseDocsTest):
             'Waiters',
             '=======',
             'The available waiters are:',
-            '* :py:class:`MyService.Waiter.sample_operation_complete`',
-            '.. py:class:: MyService.Waiter.sample_operation_complete',
+            '* :py:class:`MyService.Waiter.SampleOperationComplete`',
+            '.. py:class:: MyService.Waiter.SampleOperationComplete',
             '  ::',
             '    waiter = client.get_waiter(\'sample_operation_complete\')',
             '  .. py:method:: wait(**kwargs)',
-            ('    This polls :py:meth:`MyService.Client.sample_operation` '
+            ('    Polls :py:meth:`MyService.Client.sample_operation` '
              'every 15 seconds until a successful state is reached. An error '
              'is returned after 40 failed checks.'),
             '    **Request Syntax**',
diff --git a/tests/unit/test_waiters.py b/tests/unit/test_waiters.py
index c89b4d789..26ed7f275 100644
--- a/tests/unit/test_waiters.py
+++ b/tests/unit/test_waiters.py
@@ -16,11 +16,13 @@ from tests import unittest, BaseEnvVar
 import mock
 
 import botocore
+from botocore.compat import six
 from botocore.exceptions import ClientError, WaiterConfigError, WaiterError
 from botocore.waiter import Waiter, WaiterModel, SingleWaiterConfig
 from botocore.waiter import create_waiter_with_client
 from botocore.waiter import NormalizedOperationMethod
 from botocore.loaders import Loader
+from botocore.model import ServiceModel
 
 
 class TestWaiterModel(unittest.TestCase):
@@ -458,14 +460,74 @@ class TestCreateWaiter(unittest.TestCase):
             },
         }
         self.waiter_model = WaiterModel(self.waiter_config)
+        self.service_json_model = {
+            'metadata': {
+                'serviceFullName': 'Amazon MyService'
+            },
+            'operations': {
+                'Foo': {
+                    'name': 'Foo',
+                    'input': {'shape': 'FooInputOutput'},
+                    'output': {'shape': 'FooInputOutput'}
+                }
+            },
+            'shapes': {
+                'FooInputOutput': {
+                    'type': 'structure',
+                    'members': {
+                        'bar': {
+                            'shape': 'String',
+                            'documentation': 'Documents bar'
+                        }
+                    }
+                },
+                'String': {
+                    'type': 'string'
+                }
+            }
+        }
+        self.service_model = ServiceModel(self.service_json_model, 'myservice')
+        self.client = mock.Mock()
+        self.client.meta.service_model = self.service_model
 
     def test_can_create_waiter_from_client(self):
         waiter_name = 'WaiterName'
-        client = mock.Mock()
         waiter = create_waiter_with_client(
-            waiter_name, self.waiter_model, client)
+            waiter_name, self.waiter_model, self.client)
         self.assertIsInstance(waiter, Waiter)
 
+    def test_waiter_class_name(self):
+        waiter_name = 'WaiterName'
+        waiter = create_waiter_with_client(
+            waiter_name, self.waiter_model, self.client)
+        self.assertEqual(
+            waiter.__class__.__name__,
+            'MyService.Waiter.WaiterName'
+        )
+
+    def test_waiter_help_documentation(self):
+        waiter_name = 'WaiterName'
+        waiter = create_waiter_with_client(
+            waiter_name, self.waiter_model, self.client)
+        with mock.patch('sys.stdout', six.StringIO()) as mock_stdout:
+            help(waiter.wait)
+        content = mock_stdout.getvalue()
+        lines = [
+            ('    Polls :py:meth:`MyService.Client.foo` every 1 '
+             'seconds until a successful state is reached. An error '
+             'is returned after 1 failed checks.'),
+            '    **Request Syntax** ',
+            '    ::',
+            '      waiter.wait(',
+            "          bar='string'",
+            '      )',
+            '    :type bar: string',
+            '    :param bar: Documents bar',
+            '    :returns: None',
+        ]
+        for line in lines:
+            self.assertIn(line, content)
+
 
 class TestOperationMethods(unittest.TestCase):
     def test_normalized_op_method_makes_call(self):
@@ -506,6 +568,14 @@ class ServiceWaiterFunctionalTest(BaseEnvVar):
         return WaiterModel(self.loader.load_service_model(
             service, type_name='waiters-2', api_version=api_version))
 
+    def get_service_model(self, service, api_version=None):
+        """Get the service model for the service."""
+        return ServiceModel(
+            self.loader.load_service_model(
+                service, type_name='service-2', api_version=api_version),
+            service_name=service
+        )
+
 
 class CloudFrontWaitersTest(ServiceWaiterFunctionalTest):
     def setUp(self):
@@ -517,6 +587,8 @@ class CloudFrontWaitersTest(ServiceWaiterFunctionalTest):
     def assert_distribution_deployed_call_count(self, api_version=None):
         waiter_name = 'DistributionDeployed'
         waiter_model = self.get_waiter_model(self.service, api_version)
+        self.client.meta.service_model = self.get_service_model(
+            self.service, api_version)
         self.client.get_distribution.side_effect = [
             {'Distribution': {'Status': 'Deployed'}}
         ]
@@ -528,6 +600,8 @@ class CloudFrontWaitersTest(ServiceWaiterFunctionalTest):
     def assert_invalidation_completed_call_count(self, api_version=None):
         waiter_name = 'InvalidationCompleted'
         waiter_model = self.get_waiter_model(self.service, api_version)
+        self.client.meta.service_model = self.get_service_model(
+            self.service, api_version)
         self.client.get_invalidation.side_effect = [
             {'Invalidation': {'Status': 'Completed'}}
         ]
@@ -540,6 +614,8 @@ class CloudFrontWaitersTest(ServiceWaiterFunctionalTest):
             self, api_version=None):
         waiter_name = 'StreamingDistributionDeployed'
         waiter_model = self.get_waiter_model(self.service, api_version)
+        self.client.meta.service_model = self.get_service_model(
+            self.service, api_version)
         self.client.get_streaming_distribution.side_effect = [
             {'StreamingDistribution': {'Status': 'Deployed'}}
         ]