Repository: boto/botocore
Pull request #557 (2015-06-11 22:12:51)

Adding documentation customizations.
This commit adds two new customization classes:

1. HideParamFromOperations - hides a parameter from one or more
   operations.
2. AppendParamDocumentation - appends documentation to a parameter.

Documentation customizations have been added for each customization
listed in handlers.py.

@kyleknap @jamesls 


Patch:
diff --git a/botocore/client.py b/botocore/client.py
index c76b2898f..eac1e54cd 100644
--- a/botocore/client.py
+++ b/botocore/client.py
@@ -46,17 +46,16 @@ class ClientCreator(object):
     def create_client(self, service_name, region_name, is_secure=True,
                       endpoint_url=None, verify=None,
                       credentials=None, scoped_config=None,
-                      api_version=None,
                       client_config=None):
-        service_model = self._load_service_model(service_name, api_version)
+        service_model = self._load_service_model(service_name)
         cls = self._create_client_class(service_name, service_model)
         client_args = self._get_client_args(
             service_model, region_name, is_secure, endpoint_url,
             verify, credentials, scoped_config, client_config)
         return cls(**client_args)
 
-    def create_client_class(self, service_name, api_version=None):
-        service_model = self._load_service_model(service_name, api_version)
+    def create_client_class(self, service_name):
+        service_model = self._load_service_model(service_name)
         return self._create_client_class(service_name, service_model)
 
     def _create_client_class(self, service_name, service_model):
@@ -70,9 +69,8 @@ class ClientCreator(object):
         cls = type(str(service_name), tuple(bases), class_attributes)
         return cls
 
-    def _load_service_model(self, service_name, api_version=None):
-        json_model = self._loader.load_service_model(service_name, 'service-2',
-                                                     api_version=api_version)
+    def _load_service_model(self, service_name):
+        json_model = self._loader.load_service_model(service_name, 'service-2')
         service_model = ServiceModel(json_model, service_name=service_name)
         self._register_retries(service_model)
         return service_model
diff --git a/botocore/docs/utils.py b/botocore/docs/utils.py
index cb976b6c4..e0af0086b 100644
--- a/botocore/docs/utils.py
+++ b/botocore/docs/utils.py
@@ -102,11 +102,12 @@ class AutoPopulatedParam(object):
         self.param_description = param_description
         if param_description is None:
             self.param_description = (
-                'Note this parameter is autopopulated. There is no '
-                'need to include in method call.\n')
+                'Please note that this parameter is automatically populated if'
+                'it is not provided. Including this parameter is not '
+                'required\n')
 
     def document_auto_populated_param(self, event_name, section, **kwargs):
-        """This documents the auto populated parameters
+        """Documents auto populated parameters
 
         It will remove any required marks for the parameter, remove the
         parameter from the example, and add a snippet about the parameter
@@ -124,3 +125,55 @@ class AutoPopulatedParam(object):
             section = section.get_section('structure-value')
             if self.name in section.available_sections:
                 section.delete_section(self.name)
+
+
+class HideParamFromOperations(object):
+    """Hides a single parameter from multiple operations.
+
+    This method will remove a parameter from documentation and from
+    examples. This method is typically used for things that are
+    automatically populated because a user would be unable to provide
+    a value (e.g., a checksum of a serialized XML request body)."""
+    def __init__(self, service_name, parameter_name, operation_names):
+        """
+        :type service_name: str
+        :param service_name: Name of the service to modify.
+
+        :type parameter_name: str
+        :param parameter_name: Name of the parameter to modify.
+
+        :type operation_names: list
+        :param operation_names: Operation names to modify.
+        """
+        self._parameter_name = parameter_name
+        self._params_events = set()
+        self._example_events = set()
+        # Build up the sets of relevant event names.
+        param_template = 'docs.request-params.%s.%s.complete-section'
+        example_template = 'docs.request-example.%s.%s.complete-section'
+        for name in operation_names:
+            self._params_events.add(param_template % (service_name, name))
+            self._example_events.add(example_template % (service_name, name))
+
+    def hide_param(self, event_name, section, **kwargs):
+        if event_name in self._example_events:
+            # Modify the structure value for example events.
+            section = section.get_section('structure-value')
+        elif event_name not in self._params_events:
+            return
+        if self._parameter_name in section.available_sections:
+            section.delete_section(self._parameter_name)
+
+
+class AppendParamDocumentation(object):
+    """Appends documentation to a specific parameter"""
+    def __init__(self, parameter_name, doc_string):
+        self._parameter_name = parameter_name
+        self._doc_string = doc_string
+
+    def append_documentation(self, event_name, section, **kwargs):
+        if self._parameter_name in section.available_sections:
+            section = section.get_section(self._parameter_name)
+            description_section = section.get_section(
+                'param-documentation')
+            description_section.writeln(self._doc_string)
diff --git a/botocore/handlers.py b/botocore/handlers.py
index 8d62f9dd7..86d488394 100644
--- a/botocore/handlers.py
+++ b/botocore/handlers.py
@@ -18,14 +18,14 @@ This module contains builtin handlers for events emitted by botocore.
 
 import base64
 import hashlib
-import functools
 import logging
-import re
 import xml.etree.cElementTree
 import copy
 
 from botocore.compat import unquote, json, quote, six
 from botocore.docs.utils import AutoPopulatedParam
+from botocore.docs.utils import HideParamFromOperations
+from botocore.docs.utils import AppendParamDocumentation
 from botocore.signers import add_generate_presigned_url
 from botocore.signers import add_generate_presigned_post
 
@@ -304,6 +304,12 @@ def base64_encode_user_data(params, **kwargs):
             params['UserData']).decode('utf-8')
 
 
+def document_base64_encoding():
+    description = 'UserData will be automatically base64 encoded if necessary.'
+    append = AppendParamDocumentation('UserData', description)
+    return append.append_documentation
+
+
 def fix_route53_ids(params, model, **kwargs):
     """
     Check for and split apart Route53 resource IDs, setting
@@ -441,8 +447,33 @@ BUILTIN_HANDLERS = [
      base64_encode_user_data),
     ('before-parameter-build.route53', fix_route53_ids),
     ('before-parameter-build.glacier', inject_account_id),
+
+    # Glacier documentation customizations
     ('docs.*.glacier.*.complete-section',
-     AutoPopulatedParam('accountId').document_auto_populated_param),
+     AutoPopulatedParam('accountId', 'Note: this parameter is set to "-" by \
+                         default if no value is not specified.')
+     .document_auto_populated_param),
     ('docs.*.glacier.*.complete-section',
-     AutoPopulatedParam('checksum').document_auto_populated_param)
+     AutoPopulatedParam('checksum').document_auto_populated_param),
+    # UserData base64 encoding documentation customizations
+    ('docs.*.ec2.RunInstances.complete-section', document_base64_encoding()),
+    ('docs.*.autoscaling.CreateLaunchConfiguration.complete-section',
+     document_base64_encoding()),
+    # EC2 CopySnapshot documentation customizations
+    ('docs.*.ec2.CopySnapshot.complete-section',
+     AutoPopulatedParam('PresignedUrl').document_auto_populated_param),
+    ('docs.*.ec2.CopySnapshot.complete-section',
+     AutoPopulatedParam('DestinationRegion').document_auto_populated_param),
+    # S3 SSE documentation modifications
+    ('docs.*.s3.*.complete-section',
+     AutoPopulatedParam('SSECustomerKeyMD5').document_auto_populated_param),
+    # The following S3 operations cannot actually accept a ContentMD5
+    ('docs.*.s3.*.complete-section',
+     HideParamFromOperations(
+         's3', 'ContentMD5',
+         ['DeleteObjects', 'PutBucketAcl', 'PutBucketCors',
+          'PutBucketLifecycle', 'PutBucketLogging', 'PutBucketNotification',
+          'PutBucketPolicy', 'PutBucketReplication', 'PutBucketRequestPayment',
+          'PutBucketTagging', 'PutBucketVersioning', 'PutBucketWebsite',
+          'PutObjectAcl']).hide_param)
 ]
diff --git a/botocore/paginate.py b/botocore/paginate.py
index bec9f82b4..a83ce7095 100644
--- a/botocore/paginate.py
+++ b/botocore/paginate.py
@@ -125,32 +125,6 @@ class PageIterator(object):
                 self._inject_token_into_kwargs(current_kwargs, next_token)
                 previous_next_token = next_token
 
-    def search(self, expression):
-        """Applies a JMESPath expression to a paginator
-
-        Each page of results is searched using the provided JMESPath
-        expression. If the result is not a list, it is yielded
-        directly. If the result is a list, each element in the result
-        is yielded individually (essentially implementing a flatmap in
-        which the JMESPath search is the mapping function).
-
-        :type expression: str
-        :param expression: JMESPath expression to apply to each page.
-
-        :return: Returns an iterator that yields the individual
-            elements of applying a JMESPath expression to each page of
-            results.
-        """
-        compiled = jmespath.compile(expression)
-        for page in self:
-            results = compiled.search(page)
-            if isinstance(results, list):
-                for element in results:
-                    yield element
-            else:
-                # Yield result directly if it is not a list.
-                yield results
-
     def _make_request(self, current_kwargs):
         return self._method(**current_kwargs)
 
diff --git a/botocore/session.py b/botocore/session.py
index a34b0a353..4f6a29c80 100644
--- a/botocore/session.py
+++ b/botocore/session.py
@@ -837,7 +837,7 @@ class Session(object):
         client = client_creator.create_client(
             service_name, region_name, use_ssl, endpoint_url, verify,
             credentials, scoped_config=self.get_scoped_config(),
-            client_config=config, api_version=api_version)
+            client_config=config)
         return client
 
 
diff --git a/botocore/vendored/requests/__init__.py b/botocore/vendored/requests/__init__.py
index 4bca66e73..446500bfd 100644
--- a/botocore/vendored/requests/__init__.py
+++ b/botocore/vendored/requests/__init__.py
@@ -42,8 +42,8 @@ is at <http://python-requests.org>.
 """
 
 __title__ = 'requests'
-__version__ = '2.7.0'
-__build__ = 0x020700
+__version__ = '2.6.0'
+__build__ = 0x020503
 __author__ = 'Kenneth Reitz'
 __license__ = 'Apache 2.0'
 __copyright__ = 'Copyright 2015 Kenneth Reitz'
diff --git a/botocore/vendored/requests/api.py b/botocore/vendored/requests/api.py
index d40fa3804..98c92298e 100644
--- a/botocore/vendored/requests/api.py
+++ b/botocore/vendored/requests/api.py
@@ -55,18 +55,17 @@ def request(method, url, **kwargs):
     return response
 
 
-def get(url, params=None, **kwargs):
+def get(url, **kwargs):
     """Sends a GET request.
 
     :param url: URL for the new :class:`Request` object.
-    :param params: (optional) Dictionary or bytes to be sent in the query string for the :class:`Request`.
     :param \*\*kwargs: Optional arguments that ``request`` takes.
     :return: :class:`Response <Response>` object
     :rtype: requests.Response
     """
 
     kwargs.setdefault('allow_redirects', True)
-    return request('get', url, params=params, **kwargs)
+    return request('get', url, **kwargs)
 
 
 def options(url, **kwargs):
diff --git a/botocore/vendored/requests/auth.py b/botocore/vendored/requests/auth.py
index 0ff9c2987..d1c482517 100644
--- a/botocore/vendored/requests/auth.py
+++ b/botocore/vendored/requests/auth.py
@@ -103,8 +103,7 @@ class HTTPDigestAuth(AuthBase):
         # XXX not implemented yet
         entdig = None
         p_parsed = urlparse(url)
-        #: path is request-uri defined in RFC 2616 which should not be empty
-        path = p_parsed.path or "/"
+        path = p_parsed.path
         if p_parsed.query:
             path += '?' + p_parsed.query
 
diff --git a/botocore/vendored/requests/cookies.py b/botocore/vendored/requests/cookies.py
index 1fbc934c7..6969fe5cc 100644
--- a/botocore/vendored/requests/cookies.py
+++ b/botocore/vendored/requests/cookies.py
@@ -6,7 +6,6 @@ Compatibility code to be able to use `cookielib.CookieJar` with requests.
 requests.utils imports from here, so be careful with imports.
 """
 
-import copy
 import time
 import collections
 from .compat import cookielib, urlparse, urlunparse, Morsel
@@ -303,7 +302,7 @@ class RequestsCookieJar(cookielib.CookieJar, collections.MutableMapping):
         """Updates this jar with cookies from another CookieJar or dict-like"""
         if isinstance(other, cookielib.CookieJar):
             for cookie in other:
-                self.set_cookie(copy.copy(cookie))
+                self.set_cookie(cookie)
         else:
             super(RequestsCookieJar, self).update(other)
 
@@ -360,21 +359,6 @@ class RequestsCookieJar(cookielib.CookieJar, collections.MutableMapping):
         return new_cj
 
 
-def _copy_cookie_jar(jar):
-    if jar is None:
-        return None
-
-    if hasattr(jar, 'copy'):
-        # We're dealing with an instane of RequestsCookieJar
-        return jar.copy()
-    # We're dealing with a generic CookieJar instance
-    new_jar = copy.copy(jar)
-    new_jar.clear()
-    for cookie in jar:
-        new_jar.set_cookie(copy.copy(cookie))
-    return new_jar
-
-
 def create_cookie(name, value, **kwargs):
     """Make a cookie from underspecified parameters.
 
diff --git a/botocore/vendored/requests/models.py b/botocore/vendored/requests/models.py
index 45b3ea968..419cf0a8b 100644
--- a/botocore/vendored/requests/models.py
+++ b/botocore/vendored/requests/models.py
@@ -15,7 +15,7 @@ from .hooks import default_hooks
 from .structures import CaseInsensitiveDict
 
 from .auth import HTTPBasicAuth
-from .cookies import cookiejar_from_dict, get_cookie_header, _copy_cookie_jar
+from .cookies import cookiejar_from_dict, get_cookie_header
 from .packages.urllib3.fields import RequestField
 from .packages.urllib3.filepost import encode_multipart_formdata
 from .packages.urllib3.util import parse_url
@@ -320,7 +320,7 @@ class PreparedRequest(RequestEncodingMixin, RequestHooksMixin):
         p.method = self.method
         p.url = self.url
         p.headers = self.headers.copy() if self.headers is not None else None
-        p._cookies = _copy_cookie_jar(self._cookies)
+        p._cookies = self._cookies.copy() if self._cookies is not None else None
         p.body = self.body
         p.hooks = self.hooks
         return p
@@ -358,8 +358,7 @@ class PreparedRequest(RequestEncodingMixin, RequestHooksMixin):
 
         if not scheme:
             raise MissingSchema("Invalid URL {0!r}: No schema supplied. "
-                                "Perhaps you meant http://{0}?".format(
-                                    to_native_string(url, 'utf8')))
+                                "Perhaps you meant http://{0}?".format(url))
 
         if not host:
             raise InvalidURL("Invalid URL %r: No host supplied" % url)
@@ -502,15 +501,7 @@ class PreparedRequest(RequestEncodingMixin, RequestHooksMixin):
             self.prepare_content_length(self.body)
 
     def prepare_cookies(self, cookies):
-        """Prepares the given HTTP cookie data.
-
-        This function eventually generates a ``Cookie`` header from the
-        given cookies using cookielib. Due to cookielib's design, the header
-        will not be regenerated if it already exists, meaning this function
-        can only be called once for the life of the
-        :class:`PreparedRequest <PreparedRequest>` object. Any subsequent calls
-        to ``prepare_cookies`` will have no actual effect, unless the "Cookie"
-        header is removed beforehand."""
+        """Prepares the given HTTP cookie data."""
 
         if isinstance(cookies, cookielib.CookieJar):
             self._cookies = cookies
@@ -523,10 +514,6 @@ class PreparedRequest(RequestEncodingMixin, RequestHooksMixin):
 
     def prepare_hooks(self, hooks):
         """Prepares the given hooks."""
-        # hooks can be passed as None to the prepare method and to this
-        # method. To prevent iterating over None, simply use an empty list
-        # if hooks is False-y
-        hooks = hooks or []
         for event in hooks:
             self.register_hook(event, hooks[event])
 
@@ -586,11 +573,7 @@ class Response(object):
         self.cookies = cookiejar_from_dict({})
 
         #: The amount of time elapsed between sending the request
-        #: and the arrival of the response (as a timedelta).
-        #: This property specifically measures the time taken between sending
-        #: the first byte of the request and finishing parsing the headers. It
-        #: is therefore unaffected by consuming the response content or the
-        #: value of the ``stream`` keyword argument.
+        #: and the arrival of the response (as a timedelta)
         self.elapsed = datetime.timedelta(0)
 
         #: The :class:`PreparedRequest <PreparedRequest>` object to which this
diff --git a/botocore/vendored/requests/packages/__init__.py b/botocore/vendored/requests/packages/__init__.py
index d62c4b711..4dcf870f3 100644
--- a/botocore/vendored/requests/packages/__init__.py
+++ b/botocore/vendored/requests/packages/__init__.py
@@ -1,3 +1,107 @@
+"""
+Copyright (c) Donald Stufft, pip, and individual contributors
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+"""
 from __future__ import absolute_import
 
-from . import urllib3
+import sys
+
+
+class VendorAlias(object):
+
+    def __init__(self, package_names):
+        self._package_names = package_names
+        self._vendor_name = __name__
+        self._vendor_pkg = self._vendor_name + "."
+        self._vendor_pkgs = [
+            self._vendor_pkg + name for name in self._package_names
+        ]
+
+    def find_module(self, fullname, path=None):
+        if fullname.startswith(self._vendor_pkg):
+            return self
+
+    def load_module(self, name):
+        # Ensure that this only works for the vendored name
+        if not name.startswith(self._vendor_pkg):
+            raise ImportError(
+                "Cannot import %s, must be a subpackage of '%s'." % (
+                    name, self._vendor_name,
+                )
+            )
+
+        if not (name == self._vendor_name or
+                any(name.startswith(pkg) for pkg in self._vendor_pkgs)):
+            raise ImportError(
+                "Cannot import %s, must be one of %s." % (
+                    name, self._vendor_pkgs
+                )
+            )
+
+        # Check to see if we already have this item in sys.modules, if we do
+        # then simply return that.
+        if name in sys.modules:
+            return sys.modules[name]
+
+        # Check to see if we can import the vendor name
+        try:
+            # We do this dance here because we want to try and import this
+            # module without hitting a recursion error because of a bunch of
+            # VendorAlias instances on sys.meta_path
+            real_meta_path = sys.meta_path[:]
+            try:
+                sys.meta_path = [
+                    m for m in sys.meta_path
+                    if not isinstance(m, VendorAlias)
+                ]
+                __import__(name)
+                module = sys.modules[name]
+            finally:
+                # Re-add any additions to sys.meta_path that were made while
+                # during the import we just did, otherwise things like
+                # requests.packages.urllib3.poolmanager will fail.
+                for m in sys.meta_path:
+                    if m not in real_meta_path:
+                        real_meta_path.append(m)
+
+                # Restore sys.meta_path with any new items.
+                sys.meta_path = real_meta_path
+        except ImportError:
+            # We can't import the vendor name, so we'll try to import the
+            # "real" name.
+            real_name = name[len(self._vendor_pkg):]
+            try:
+                __import__(real_name)
+                module = sys.modules[real_name]
+            except ImportError:
+                raise ImportError("No module named '%s'" % (name,))
+
+        # If we've gotten here we've found the module we're looking for, either
+        # as part of our vendored package, or as the real name, so we'll add
+        # it to sys.modules as the vendored name so that we don't have to do
+        # the lookup again.
+        sys.modules[name] = module
+
+        # Finally, return the loaded module
+        return module
+
+
+sys.meta_path.append(VendorAlias(["urllib3", "chardet"]))
diff --git a/botocore/vendored/requests/packages/urllib3/__init__.py b/botocore/vendored/requests/packages/urllib3/__init__.py
index f48ac4af1..0660b9c83 100644
--- a/botocore/vendored/requests/packages/urllib3/__init__.py
+++ b/botocore/vendored/requests/packages/urllib3/__init__.py
@@ -4,7 +4,7 @@ urllib3 - Thread-safe connection pooling and re-using.
 
 __author__ = 'Andrey Petrov (andrey.petrov@shazow.net)'
 __license__ = 'MIT'
-__version__ = '1.10.4'
+__version__ = '1.10.2'
 
 
 from .connectionpool import (
@@ -55,12 +55,9 @@ def add_stderr_logger(level=logging.DEBUG):
 del NullHandler
 
 
+# Set security warning to always go off by default.
 import warnings
-# SecurityWarning's always go off by default.
-warnings.simplefilter('always', exceptions.SecurityWarning, append=True)
-# InsecurePlatformWarning's don't vary between requests, so we keep it default.
-warnings.simplefilter('default', exceptions.InsecurePlatformWarning,
-                      append=True)
+warnings.simplefilter('always', exceptions.SecurityWarning)
 
 def disable_warnings(category=exceptions.HTTPWarning):
     """
diff --git a/botocore/vendored/requests/packages/urllib3/_collections.py b/botocore/vendored/requests/packages/urllib3/_collections.py
index 279416ce6..cc424de0f 100644
--- a/botocore/vendored/requests/packages/urllib3/_collections.py
+++ b/botocore/vendored/requests/packages/urllib3/_collections.py
@@ -227,20 +227,20 @@ class HTTPHeaderDict(dict):
                 # Need to convert the tuple to list for further extension
                 _dict_setitem(self, key_lower, [vals[0], vals[1], val])
 
-    def extend(self, *args, **kwargs):
+    def extend(*args, **kwargs):
         """Generic import function for any type of header-like object.
         Adapted version of MutableMapping.update in order to insert items
         with self.add instead of self.__setitem__
         """
-        if len(args) > 1:
-            raise TypeError("extend() takes at most 1 positional "
+        if len(args) > 2:
+            raise TypeError("update() takes at most 2 positional "
                             "arguments ({} given)".format(len(args)))
-        other = args[0] if len(args) >= 1 else ()
+        elif not args:
+            raise TypeError("update() takes at least 1 argument (0 given)")
+        self = args[0]
+        other = args[1] if len(args) >= 2 else ()
         
-        if isinstance(other, HTTPHeaderDict):
-            for key, val in other.iteritems():
-                self.add(key, val)
-        elif isinstance(other, Mapping):
+        if isinstance(other, Mapping):
             for key in other:
                 self.add(key, other[key])
         elif hasattr(other, "keys"):
@@ -304,20 +304,17 @@ class HTTPHeaderDict(dict):
         return list(self.iteritems())
 
     @classmethod
-    def from_httplib(cls, message): # Python 2
+    def from_httplib(cls, message, duplicates=('set-cookie',)): # Python 2
         """Read headers from a Python 2 httplib message object."""
-        # python2.7 does not expose a proper API for exporting multiheaders
-        # efficiently. This function re-reads raw lines from the message 
-        # object and extracts the multiheaders properly.
-        headers = []
-         
-        for line in message.headers:
-            if line.startswith((' ', '\t')):
-                key, value = headers[-1]
-                headers[-1] = (key, value + '\r\n' + line.rstrip())
-                continue
-    
-            key, value = line.split(':', 1)
-            headers.append((key, value.strip()))
-
-        return cls(headers)
+        ret = cls(message.items())
+        # ret now contains only the last header line for each duplicate.
+        # Importing with all duplicates would be nice, but this would
+        # mean to repeat most of the raw parsing already done, when the
+        # message object was created. Extracting only the headers of interest 
+        # separately, the cookies, should be faster and requires less
+        # extra code.
+        for key in duplicates:
+            ret.discard(key)
+            for val in message.getheaders(key):
+                ret.add(key, val)
+            return ret
diff --git a/botocore/vendored/requests/packages/urllib3/connection.py b/botocore/vendored/requests/packages/urllib3/connection.py
index 2a8c35967..e5de769d8 100644
--- a/botocore/vendored/requests/packages/urllib3/connection.py
+++ b/botocore/vendored/requests/packages/urllib3/connection.py
@@ -260,5 +260,3 @@ if ssl:
     # Make a copy for testing.
     UnverifiedHTTPSConnection = HTTPSConnection
     HTTPSConnection = VerifiedHTTPSConnection
-else:
-    HTTPSConnection = DummyConnection
diff --git a/botocore/vendored/requests/packages/urllib3/connectionpool.py b/botocore/vendored/requests/packages/urllib3/connectionpool.py
index 117269aca..0085345c4 100644
--- a/botocore/vendored/requests/packages/urllib3/connectionpool.py
+++ b/botocore/vendored/requests/packages/urllib3/connectionpool.py
@@ -735,6 +735,7 @@ class HTTPSConnectionPool(HTTPConnectionPool):
                  % (self.num_connections, self.host))
 
         if not self.ConnectionCls or self.ConnectionCls is DummyConnection:
+            # Platform-specific: Python without ssl
             raise SSLError("Can't connect to HTTPS URL because the SSL "
                            "module is not available.")
 
diff --git a/botocore/vendored/requests/packages/urllib3/contrib/pyopenssl.py b/botocore/vendored/requests/packages/urllib3/contrib/pyopenssl.py
index b2c34a892..ee657fb3f 100644
--- a/botocore/vendored/requests/packages/urllib3/contrib/pyopenssl.py
+++ b/botocore/vendored/requests/packages/urllib3/contrib/pyopenssl.py
@@ -38,6 +38,8 @@ Module Variables
 ----------------
 
 :var DEFAULT_SSL_CIPHER_LIST: The list of supported SSL/TLS cipher suites.
+    Default: ``ECDH+AESGCM:DH+AESGCM:ECDH+AES256:DH+AES256:ECDH+AES128:DH+AES:
+    ECDH+3DES:DH+3DES:RSA+AESGCM:RSA+AES:RSA+3DES:!aNULL:!MD5:!DSS``
 
 .. _sni: https://en.wikipedia.org/wiki/Server_Name_Indication
 .. _crime attack: https://en.wikipedia.org/wiki/CRIME_(security_exploit)
@@ -83,7 +85,22 @@ _openssl_verify = {
                        + OpenSSL.SSL.VERIFY_FAIL_IF_NO_PEER_CERT,
 }
 
-DEFAULT_SSL_CIPHER_LIST = util.ssl_.DEFAULT_CIPHERS
+# A secure default.
+# Sources for more information on TLS ciphers:
+#
+# - https://wiki.mozilla.org/Security/Server_Side_TLS
+# - https://www.ssllabs.com/projects/best-practices/index.html
+# - https://hynek.me/articles/hardening-your-web-servers-ssl-ciphers/
+#
+# The general intent is:
+# - Prefer cipher suites that offer perfect forward secrecy (DHE/ECDHE),
+# - prefer ECDHE over DHE for better performance,
+# - prefer any AES-GCM over any AES-CBC for better performance and security,
+# - use 3DES as fallback which is secure but slow,
+# - disable NULL authentication, MD5 MACs and DSS for security reasons.
+DEFAULT_SSL_CIPHER_LIST = "ECDH+AESGCM:DH+AESGCM:ECDH+AES256:DH+AES256:" + \
+    "ECDH+AES128:DH+AES:ECDH+3DES:DH+3DES:RSA+AESGCM:RSA+AES:RSA+3DES:" + \
+    "!aNULL:!MD5:!DSS"
 
 
 orig_util_HAS_SNI = util.HAS_SNI
@@ -282,9 +299,7 @@ def ssl_wrap_socket(sock, keyfile=None, certfile=None, cert_reqs=None,
         try:
             cnx.do_handshake()
         except OpenSSL.SSL.WantReadError:
-            rd, _, _ = select.select([sock], [], [], sock.gettimeout())
-            if not rd:
-                raise timeout('select timed out')
+            select.select([sock], [], [])
             continue
         except OpenSSL.SSL.Error as e:
             raise ssl.SSLError('bad handshake', e)
diff --git a/botocore/vendored/requests/packages/urllib3/exceptions.py b/botocore/vendored/requests/packages/urllib3/exceptions.py
index 31bda1c07..5d5230112 100644
--- a/botocore/vendored/requests/packages/urllib3/exceptions.py
+++ b/botocore/vendored/requests/packages/urllib3/exceptions.py
@@ -162,8 +162,3 @@ class SystemTimeWarning(SecurityWarning):
 class InsecurePlatformWarning(SecurityWarning):
     "Warned when certain SSL configuration is not available on a platform."
     pass
-
-
-class ResponseNotChunked(ProtocolError, ValueError):
-    "Response needs to be chunked in order to read it as chunks."
-    pass
diff --git a/botocore/vendored/requests/packages/urllib3/response.py b/botocore/vendored/requests/packages/urllib3/response.py
index 24140c4cd..34cd3d705 100644
--- a/botocore/vendored/requests/packages/urllib3/response.py
+++ b/botocore/vendored/requests/packages/urllib3/response.py
@@ -1,15 +1,9 @@
-try:
-    import http.client as httplib
-except ImportError:
-    import httplib
 import zlib
 import io
 from socket import timeout as SocketTimeout
 
 from ._collections import HTTPHeaderDict
-from .exceptions import (
-    ProtocolError, DecodeError, ReadTimeoutError, ResponseNotChunked
-)
+from .exceptions import ProtocolError, DecodeError, ReadTimeoutError
 from .packages.six import string_types as basestring, binary_type, PY3
 from .connection import HTTPException, BaseSSLError
 from .util.response import is_fp_closed
@@ -123,17 +117,7 @@ class HTTPResponse(io.IOBase):
         if hasattr(body, 'read'):
             self._fp = body
 
-        # Are we using the chunked-style of transfer encoding?
-        self.chunked = False
-        self.chunk_left = None
-        tr_enc = self.headers.get('transfer-encoding', '').lower()
-        # Don't incur the penalty of creating a list and then discarding it
-        encodings = (enc.strip() for enc in tr_enc.split(","))
-        if "chunked" in encodings:
-            self.chunked = True
-
-        # We certainly don't want to preload content when the response is chunked.
-        if not self.chunked and preload_content and not self._body:
+        if preload_content and not self._body:
             self._body = self.read(decode_content=decode_content)
 
     def get_redirect_location(self):
@@ -173,35 +157,6 @@ class HTTPResponse(io.IOBase):
         """
         return self._fp_bytes_read
 
-    def _init_decoder(self):
-        """
-        Set-up the _decoder attribute if necessar.
-        """
-        # Note: content-encoding value should be case-insensitive, per RFC 7230
-        # Section 3.2
-        content_encoding = self.headers.get('content-encoding', '').lower()
-        if self._decoder is None and content_encoding in self.CONTENT_DECODERS:
-            self._decoder = _get_decoder(content_encoding)
-
-    def _decode(self, data, decode_content, flush_decoder):
-        """
-        Decode the data passed in and potentially flush the decoder.
-        """
-        try:
-            if decode_content and self._decoder:
-                data = self._decoder.decompress(data)
-        except (IOError, zlib.error) as e:
-            content_encoding = self.headers.get('content-encoding', '').lower()
-            raise DecodeError(
-                "Received response with content-encoding: %s, but "
-                "failed to decode it." % content_encoding, e)
-
-        if flush_decoder and decode_content and self._decoder:
-            buf = self._decoder.decompress(binary_type())
-            data += buf + self._decoder.flush()
-
-        return data
-
     def read(self, amt=None, decode_content=None, cache_content=False):
         """
         Similar to :meth:`httplib.HTTPResponse.read`, but with two additional
@@ -223,7 +178,12 @@ class HTTPResponse(io.IOBase):
             after having ``.read()`` the file object. (Overridden if ``amt`` is
             set.)
         """
-        self._init_decoder()
+        # Note: content-encoding value should be case-insensitive, per RFC 7230
+        # Section 3.2
+        content_encoding = self.headers.get('content-encoding', '').lower()
+        if self._decoder is None:
+            if content_encoding in self.CONTENT_DECODERS:
+                self._decoder = _get_decoder(content_encoding)
         if decode_content is None:
             decode_content = self.decode_content
 
@@ -272,7 +232,17 @@ class HTTPResponse(io.IOBase):
 
             self._fp_bytes_read += len(data)
 
-            data = self._decode(data, decode_content, flush_decoder)
+            try:
+                if decode_content and self._decoder:
+                    data = self._decoder.decompress(data)
+            except (IOError, zlib.error) as e:
+                raise DecodeError(
+                    "Received response with content-encoding: %s, but "
+                    "failed to decode it." % content_encoding, e)
+
+            if flush_decoder and decode_content and self._decoder:
+                buf = self._decoder.decompress(binary_type())
+                data += buf + self._decoder.flush()
 
             if cache_content:
                 self._body = data
@@ -299,15 +269,11 @@ class HTTPResponse(io.IOBase):
             If True, will attempt to decode the body based on the
             'content-encoding' header.
         """
-        if self.chunked:
-            for line in self.read_chunked(amt, decode_content=decode_content):
-                yield line
-        else:
-            while not is_fp_closed(self._fp):
-                data = self.read(amt=amt, decode_content=decode_content)
+        while not is_fp_closed(self._fp):
+            data = self.read(amt=amt, decode_content=decode_content)
 
-                if data:
-                    yield data
+            if data:
+                yield data
 
     @classmethod
     def from_httplib(ResponseCls, r, **response_kw):
@@ -385,82 +351,3 @@ class HTTPResponse(io.IOBase):
         else:
             b[:len(temp)] = temp
             return len(temp)
-
-    def _update_chunk_length(self):
-        # First, we'll figure out length of a chunk and then
-        # we'll try to read it from socket.
-        if self.chunk_left is not None:
-            return
-        line = self._fp.fp.readline()
-        line = line.split(b';', 1)[0]
-        try:
-            self.chunk_left = int(line, 16)
-        except ValueError:
-            # Invalid chunked protocol response, abort.
-            self.close()
-            raise httplib.IncompleteRead(line)
-
-    def _handle_chunk(self, amt):
-        returned_chunk = None
-        if amt is None:
-            chunk = self._fp._safe_read(self.chunk_left)
-            returned_chunk = chunk
-            self._fp._safe_read(2)  # Toss the CRLF at the end of the chunk.
-            self.chunk_left = None
-        elif amt < self.chunk_left:
-            value = self._fp._safe_read(amt)
-            self.chunk_left = self.chunk_left - amt
-            returned_chunk = value
-        elif amt == self.chunk_left:
-            value = self._fp._safe_read(amt)
-            self._fp._safe_read(2)  # Toss the CRLF at the end of the chunk.
-            self.chunk_left = None
-            returned_chunk = value
-        else:  # amt > self.chunk_left
-            returned_chunk = self._fp._safe_read(self.chunk_left)
-            self._fp._safe_read(2)  # Toss the CRLF at the end of the chunk.
-            self.chunk_left = None
-        return returned_chunk
-
-    def read_chunked(self, amt=None, decode_content=None):
-        """
-        Similar to :meth:`HTTPResponse.read`, but with an additional
-        parameter: ``decode_content``.
-
-        :param decode_content:
-            If True, will attempt to decode the body based on the
-            'content-encoding' header.
-        """
-        self._init_decoder()
-        # FIXME: Rewrite this method and make it a class with a better structured logic.
-        if not self.chunked:
-            raise ResponseNotChunked("Response is not chunked. "
-                "Header 'transfer-encoding: chunked' is missing.")
-
-        if self._original_response and self._original_response._method.upper() == 'HEAD':
-            # Don't bother reading the body of a HEAD request.
-            # FIXME: Can we do this somehow without accessing private httplib _method?
-            self._original_response.close()
-            return
-
-        while True:
-            self._update_chunk_length()
-            if self.chunk_left == 0:
-                break
-            chunk = self._handle_chunk(amt)
-            yield self._decode(chunk, decode_content=decode_content,
-                               flush_decoder=True)
-
-        # Chunk content ends with \r\n: discard it.
-        while True:
-            line = self._fp.fp.readline()
-            if not line:
-                # Some sites may not end with '\r\n'.
-                break
-            if line == b'\r\n':
-                break
-
-        # We read everything; close the "file".
-        if self._original_response:
-            self._original_response.close()
-        self.release_conn()
diff --git a/botocore/vendored/requests/packages/urllib3/util/ssl_.py b/botocore/vendored/requests/packages/urllib3/util/ssl_.py
index b846d42c2..e7e7dfae1 100644
--- a/botocore/vendored/requests/packages/urllib3/util/ssl_.py
+++ b/botocore/vendored/requests/packages/urllib3/util/ssl_.py
@@ -9,10 +9,10 @@ HAS_SNI = False
 create_default_context = None
 
 import errno
+import ssl
 import warnings
 
 try:  # Test for SSL features
-    import ssl
     from ssl import wrap_socket, CERT_NONE, PROTOCOL_SSLv23
     from ssl import HAS_SNI  # Has SNI?
 except ImportError:
@@ -25,24 +25,14 @@ except ImportError:
     OP_NO_SSLv2, OP_NO_SSLv3 = 0x1000000, 0x2000000
     OP_NO_COMPRESSION = 0x20000
 
-# A secure default.
-# Sources for more information on TLS ciphers:
-#
-# - https://wiki.mozilla.org/Security/Server_Side_TLS
-# - https://www.ssllabs.com/projects/best-practices/index.html
-# - https://hynek.me/articles/hardening-your-web-servers-ssl-ciphers/
-#
-# The general intent is:
-# - Prefer cipher suites that offer perfect forward secrecy (DHE/ECDHE),
-# - prefer ECDHE over DHE for better performance,
-# - prefer any AES-GCM over any AES-CBC for better performance and security,
-# - use 3DES as fallback which is secure but slow,
-# - disable NULL authentication, MD5 MACs and DSS for security reasons.
-DEFAULT_CIPHERS = (
-    'ECDH+AESGCM:DH+AESGCM:ECDH+AES256:DH+AES256:ECDH+AES128:DH+AES:ECDH+HIGH:'
-    'DH+HIGH:ECDH+3DES:DH+3DES:RSA+AESGCM:RSA+AES:RSA+HIGH:RSA+3DES:!aNULL:'
-    '!eNULL:!MD5'
-)
+try:
+    from ssl import _DEFAULT_CIPHERS
+except ImportError:
+    _DEFAULT_CIPHERS = (
+        'ECDH+AESGCM:DH+AESGCM:ECDH+AES256:DH+AES256:ECDH+AES128:DH+AES:ECDH+HIGH:'
+        'DH+HIGH:ECDH+3DES:DH+3DES:RSA+AESGCM:RSA+AES:RSA+HIGH:RSA+3DES:!aNULL:'
+        '!eNULL:!MD5'
+    )
 
 try:
     from ssl import SSLContext  # Modern SSL?
@@ -50,8 +40,7 @@ except ImportError:
     import sys
 
     class SSLContext(object):  # Platform-specific: Python 2 & 3.1
-        supports_set_ciphers = ((2, 7) <= sys.version_info < (3,) or
-                                (3, 2) <= sys.version_info)
+        supports_set_ciphers = sys.version_info >= (2, 7)
 
         def __init__(self, protocol_version):
             self.protocol = protocol_version
@@ -178,7 +167,7 @@ def resolve_ssl_version(candidate):
     return candidate
 
 
-def create_urllib3_context(ssl_version=None, cert_reqs=None,
+def create_urllib3_context(ssl_version=None, cert_reqs=ssl.CERT_REQUIRED,
                            options=None, ciphers=None):
     """All arguments have the same meaning as ``ssl_wrap_socket``.
 
@@ -215,9 +204,6 @@ def create_urllib3_context(ssl_version=None, cert_reqs=None,
     """
     context = SSLContext(ssl_version or ssl.PROTOCOL_SSLv23)
 
-    # Setting the default here, as we may have no ssl module on import
-    cert_reqs = ssl.CERT_REQUIRED if cert_reqs is None else cert_reqs
-
     if options is None:
         options = 0
         # SSLv2 is easily broken and is considered harmful and dangerous
@@ -231,7 +217,7 @@ def create_urllib3_context(ssl_version=None, cert_reqs=None,
     context.options |= options
 
     if getattr(context, 'supports_set_ciphers', True):  # Platform-specific: Python 2.6
-        context.set_ciphers(ciphers or DEFAULT_CIPHERS)
+        context.set_ciphers(ciphers or _DEFAULT_CIPHERS)
 
     context.verify_mode = cert_reqs
     if getattr(context, 'check_hostname', None) is not None:  # Platform-specific: Python 3.2
diff --git a/botocore/vendored/requests/packages/urllib3/util/url.py b/botocore/vendored/requests/packages/urllib3/util/url.py
index e58050cd7..b2ec834fe 100644
--- a/botocore/vendored/requests/packages/urllib3/util/url.py
+++ b/botocore/vendored/requests/packages/urllib3/util/url.py
@@ -15,8 +15,6 @@ class Url(namedtuple('Url', url_attrs)):
 
     def __new__(cls, scheme=None, auth=None, host=None, port=None, path=None,
                 query=None, fragment=None):
-        if path and not path.startswith('/'):
-            path = '/' + path
         return super(Url, cls).__new__(cls, scheme, auth, host, port, path,
                                        query, fragment)
 
diff --git a/botocore/vendored/requests/sessions.py b/botocore/vendored/requests/sessions.py
index 820919ee8..ef3f22bc5 100644
--- a/botocore/vendored/requests/sessions.py
+++ b/botocore/vendored/requests/sessions.py
@@ -90,7 +90,7 @@ def merge_hooks(request_hooks, session_hooks, dict_class=OrderedDict):
 
 class SessionRedirectMixin(object):
     def resolve_redirects(self, resp, req, stream=False, timeout=None,
-                          verify=True, cert=None, proxies=None, **adapter_kwargs):
+                          verify=True, cert=None, proxies=None):
         """Receives a Response. Returns a generator of Responses."""
 
         i = 0
@@ -193,7 +193,6 @@ class SessionRedirectMixin(object):
                 cert=cert,
                 proxies=proxies,
                 allow_redirects=False,
-                **adapter_kwargs
             )
 
             extract_cookies_to_jar(self.cookies, prepared_request, resp.raw)
@@ -561,6 +560,10 @@ class Session(SessionRedirectMixin):
         # Set up variables needed for resolve_redirects and dispatching of hooks
         allow_redirects = kwargs.pop('allow_redirects', True)
         stream = kwargs.get('stream')
+        timeout = kwargs.get('timeout')
+        verify = kwargs.get('verify')
+        cert = kwargs.get('cert')
+        proxies = kwargs.get('proxies')
         hooks = request.hooks
 
         # Get the appropriate adapter to use
@@ -588,7 +591,12 @@ class Session(SessionRedirectMixin):
         extract_cookies_to_jar(self.cookies, request, r.raw)
 
         # Redirect resolving generator.
-        gen = self.resolve_redirects(r, request, **kwargs)
+        gen = self.resolve_redirects(r, request,
+            stream=stream,
+            timeout=timeout,
+            verify=verify,
+            cert=cert,
+            proxies=proxies)
 
         # Resolve redirects if allowed.
         history = [resp for resp in gen] if allow_redirects else []
diff --git a/setup.py b/setup.py
index 789570f44..83e13b17f 100644
--- a/setup.py
+++ b/setup.py
@@ -5,7 +5,7 @@ from setuptools import setup, find_packages
 
 
 requires = ['jmespath==0.7.1',
-            'bcdoc>=0.16.0,<0.17.0',
+            'bcdoc>=0.15.0,<0.16.0',
             'python-dateutil>=2.1,<3.0.0']
 
 


Test patch:
diff --git a/tests/functional/docs/__init__.py b/tests/functional/docs/__init__.py
index fd84baa56..242b47914 100644
--- a/tests/functional/docs/__init__.py
+++ b/tests/functional/docs/__init__.py
@@ -20,8 +20,8 @@ class BaseDocsFunctionalTest(unittest.TestCase):
         self.assertIn(line, contents)
 
     def assert_contains_lines_in_order(self, lines, contents):
-       contents = contents.decode('utf-8')
-       for line in lines:
+        contents = contents.decode('utf-8')
+        for line in lines:
             self.assertIn(line, contents)
             beginning = contents.find(line)
             contents = contents[(beginning + len(line)):]
@@ -42,7 +42,7 @@ class BaseDocsFunctionalTest(unittest.TestCase):
         self.assertNotEqual(start_index, -1, 'Method is not found in contents')
         contents = contents[start_index:]
         end_index = contents.find(
-            '  ..py:method::', len(start_method_document))
+            '  .. py:method::', len(start_method_document))
         contents = contents[:end_index]
         return contents.encode('utf-8')
 
@@ -56,9 +56,17 @@ class BaseDocsFunctionalTest(unittest.TestCase):
         contents = contents[:end_index]
         return contents.encode('utf-8')
 
-    def assert_is_documented_as_autopopulated_param(
+    def get_parameter_documentation_from_service(
             self, service_name, method_name, param_name):
         contents = ServiceDocumenter(service_name).document_service()
+        method_contents = self.get_method_document_block(
+            method_name, contents)
+        return self.get_parameter_document_block(
+            param_name, method_contents)
+
+    def assert_is_documented_as_autopopulated_param(
+            self, service_name, method_name, param_name, doc_string=None):
+        contents = ServiceDocumenter(service_name).document_service()
         # Pick an arbitrary method that uses AccountId.
         method_contents = self.get_method_document_block(
             method_name, contents)
@@ -75,5 +83,6 @@ class BaseDocsFunctionalTest(unittest.TestCase):
         self.assert_not_contains_line('REQUIRED', param_contents)
 
         # Ensure the note about autopopulation was added.
-        self.assert_contains_line(
-            'Note this parameter is autopopulated', param_contents) 
+        if doc_string is None:
+            doc_string = 'Please note that this parameter is automatically'
+        self.assert_contains_line(doc_string, param_contents)
diff --git a/tests/functional/docs/test_autoscaling.py b/tests/functional/docs/test_autoscaling.py
new file mode 100644
index 000000000..d6e47db27
--- /dev/null
+++ b/tests/functional/docs/test_autoscaling.py
@@ -0,0 +1,20 @@
+# 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.functional.docs import BaseDocsFunctionalTest
+
+
+class TestAutoscalingDocs(BaseDocsFunctionalTest):
+    def test_documents_encoding_of_user_data(self):
+        docs = self.get_parameter_documentation_from_service(
+            'autoscaling', 'create_launch_configuration', 'UserData')
+        self.assertIn('automatically base64', docs.decode('utf-8'))
diff --git a/tests/functional/docs/test_ec2.py b/tests/functional/docs/test_ec2.py
new file mode 100644
index 000000000..e69fa2e6f
--- /dev/null
+++ b/tests/functional/docs/test_ec2.py
@@ -0,0 +1,32 @@
+# 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.functional.docs import BaseDocsFunctionalTest
+
+
+class TestEc2Docs(BaseDocsFunctionalTest):
+    def test_documents_encoding_of_user_data(self):
+        docs = self.get_parameter_documentation_from_service(
+            'ec2', 'run_instances', 'UserData')
+        self.assertIn('automatically base64', docs.decode('utf-8'))
+
+    def test_copy_snapshot_presigned_url_is_autopopulated(self):
+        self.assert_is_documented_as_autopopulated_param(
+            service_name='ec2',
+            method_name='copy_snapshot',
+            param_name='PresignedUrl')
+
+    def test_copy_snapshot_destination_region_is_autopopulated(self):
+        self.assert_is_documented_as_autopopulated_param(
+            service_name='ec2',
+            method_name='copy_snapshot',
+            param_name='DestinationRegion')
diff --git a/tests/functional/docs/test_glacier.py b/tests/functional/docs/test_glacier.py
index 62942a217..6667ba81f 100644
--- a/tests/functional/docs/test_glacier.py
+++ b/tests/functional/docs/test_glacier.py
@@ -12,12 +12,14 @@
 # language governing permissions and limitations under the License.
 from tests.functional.docs import BaseDocsFunctionalTest
 
+
 class TestGlacierDocs(BaseDocsFunctionalTest):
     def test_account_id(self):
         self.assert_is_documented_as_autopopulated_param(
             service_name='glacier',
             method_name='abort_multipart_upload',
-            param_name='accountId')
+            param_name='accountId',
+            doc_string='Note: this parameter is set to "-"')
 
     def test_checksum(self):
         self.assert_is_documented_as_autopopulated_param(
diff --git a/tests/functional/docs/test_s3.py b/tests/functional/docs/test_s3.py
new file mode 100644
index 000000000..6ad92405f
--- /dev/null
+++ b/tests/functional/docs/test_s3.py
@@ -0,0 +1,37 @@
+# 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.functional.docs import BaseDocsFunctionalTest
+from botocore.docs.service import ServiceDocumenter
+
+
+class TestS3Docs(BaseDocsFunctionalTest):
+    def test_auto_populates_sse_customer_key_md5(self):
+        self.assert_is_documented_as_autopopulated_param(
+            service_name='s3',
+            method_name='put_object',
+            param_name='SSECustomerKeyMD5')
+
+    def test_hides_content_md5_when_impossible_to_provide(self):
+        modified_methods = ['delete_objects', 'put_bucket_acl',
+                            'put_bucket_cors', 'put_bucket_lifecycle',
+                            'put_bucket_logging', 'put_bucket_policy',
+                            'put_bucket_notification', 'put_bucket_tagging',
+                            'put_bucket_replication', 'put_bucket_website',
+                            'put_bucket_request_payment', 'put_object_acl',
+                            'put_bucket_versioning']
+        service_contents = ServiceDocumenter('s3').document_service()
+        for method_name in modified_methods:
+            method_contents = self.get_method_document_block(
+                method_name, service_contents)
+            self.assertNotIn('ContentMD5=\'string\'',
+                             method_contents.decode('utf-8'))
diff --git a/tests/unit/docs/test_utils.py b/tests/unit/docs/test_utils.py
index 294dcb6d1..ff273a000 100644
--- a/tests/unit/docs/test_utils.py
+++ b/tests/unit/docs/test_utils.py
@@ -18,6 +18,8 @@ from botocore.docs.utils import py_type_name
 from botocore.docs.utils import py_default
 from botocore.docs.utils import get_official_service_name
 from botocore.docs.utils import AutoPopulatedParam
+from botocore.docs.utils import HideParamFromOperations
+from botocore.docs.utils import AppendParamDocumentation
 
 
 class TestPythonTypeName(unittest.TestCase):
@@ -124,8 +126,7 @@ class TestAutopopulatedParam(BaseDocsTest):
         self.param.document_auto_populated_param(
             'docs.request-params', self.doc_structure)
         self.assert_contains_line(
-            ('Note this parameter is autopopulated. There is no need '
-             'to include in method call'))
+            'this parameter is automatically populated')
 
     def test_request_param_required(self):
         section = self.doc_structure.add_new_section(self.name)
@@ -136,8 +137,7 @@ class TestAutopopulatedParam(BaseDocsTest):
             'docs.request-params', self.doc_structure)
         self.assert_not_contains_line('**[REQUIRED]**')
         self.assert_contains_line(
-            ('Note this parameter is autopopulated. There is no need '
-             'to include in method call'))
+            'this parameter is automatically populated')
 
     def test_non_default_param_description(self):
         description = 'This is a custom description'
@@ -174,3 +174,48 @@ class TestAutopopulatedParam(BaseDocsTest):
         self.param.document_auto_populated_param(
             'docs.request-example', self.doc_structure)
         self.assert_contains_line(example)
+
+
+class TestHideParamFromOperations(BaseDocsTest):
+    def setUp(self):
+        super(TestHideParamFromOperations, self).setUp()
+        self.name = 'MyMember'
+        self.param = HideParamFromOperations(
+            's3', self.name, ['SampleOperation'])
+
+    def test_hides_params_from_doc_string(self):
+        section = self.doc_structure.add_new_section(self.name)
+        param_signature = ':param %s: ' % self.name
+        section.write(param_signature)
+        self.assert_contains_line(param_signature)
+        self.param.hide_param(
+            'docs.request-params.s3.SampleOperation.complete-section',
+            self.doc_structure)
+        self.assert_not_contains_line(param_signature)
+
+    def test_hides_param_from_example(self):
+        structure = self.doc_structure.add_new_section('structure-value')
+        section = structure.add_new_section(self.name)
+        example = '%s: \'string\'' % self.name
+        section.write(example)
+        self.assert_contains_line(example)
+        self.param.hide_param(
+            'docs.request-example.s3.SampleOperation.complete-section',
+            self.doc_structure)
+        self.assert_not_contains_line(example)
+
+
+class TestAppendParamDocumentation(BaseDocsTest):
+    def setUp(self):
+        super(TestAppendParamDocumentation, self).setUp()
+        self.name = 'MyMember'
+        self.param = AppendParamDocumentation(self.name, 'hello!')
+
+    def test_appends_documentation(self):
+        section = self.doc_structure.add_new_section(self.name)
+        param_section = section.add_new_section('param-documentation')
+        param_section.writeln('foo')
+        self.param.append_documentation(
+            'docs.request-params', self.doc_structure)
+        self.assert_contains_line('foo\n')
+        self.assert_contains_line('hello!')
diff --git a/tests/unit/test_client.py b/tests/unit/test_client.py
index d1f7eac5e..4a3123803 100644
--- a/tests/unit/test_client.py
+++ b/tests/unit/test_client.py
@@ -139,29 +139,6 @@ class TestAutoGeneratedClient(unittest.TestCase):
         self.assertEqual(service_client.meta.endpoint_url,
                          'https://foo.bar')
 
-    def test_api_version_is_passed_to_loader_if_provided(self):
-        creator = self.create_client_creator()
-        self.endpoint.host = 'https://foo.bar'
-        specific_api_version = '2014-03-01'
-        creator.create_client(
-            'myservice', 'us-west-2', credentials=self.credentials,
-            api_version=specific_api_version)
-        self.loader.load_service_model.assert_called_with(
-            'myservice', 'service-2', api_version=specific_api_version)
-
-    def test_create_client_class_creates_class(self):
-        creator = self.create_client_creator()
-        client_class = creator.create_client_class('myservice')
-        self.assertTrue(hasattr(client_class, 'test_operation'))
-
-    def test_create_client_class_forwards_api_version(self):
-        creator = self.create_client_creator()
-        specific_api_version = '2014-03-01'
-        creator.create_client_class('myservice',
-                                    api_version=specific_api_version)
-        self.loader.load_service_model.assert_called_with(
-            'myservice', 'service-2', api_version=specific_api_version)
-
     def test_client_uses_region_from_client_config(self):
         client_config = client.Config()
         client_config.region_name = 'us-west-1'
diff --git a/tests/unit/test_paginate.py b/tests/unit/test_paginate.py
index 32fa880a2..f0dca587a 100644
--- a/tests/unit/test_paginate.py
+++ b/tests/unit/test_paginate.py
@@ -749,41 +749,5 @@ class TestIncludeNonResultKeys(unittest.TestCase):
         self.assertEqual(actual, expected)
 
 
-class TestSearchOverResults(unittest.TestCase):
-    def setUp(self):
-        self.method = mock.Mock()
-        self.paginate_config = {
-            'more_results': 'IsTruncated',
-            'output_token': 'NextToken',
-            'input_token': 'NextToken',
-            'result_key': 'Foo',
-        }
-        self.paginator = Paginator(self.method, self.paginate_config)
-        responses = [
-            {'Foo': [{'a': 1}, {'b': 2}],
-             'IsTruncated': True, 'NextToken': '1'},
-            {'Foo': [{'a': 3}, {'b': 4}],
-             'IsTruncated': True, 'NextToken': '2'},
-            {'Foo': [{'a': 5}], 'IsTruncated': False, 'NextToken': '3'}
-        ]
-        self.method.side_effect = responses
-
-    def test_yields_non_list_values(self):
-        result = list(self.paginator.paginate().search('Foo[0].a'))
-        self.assertEqual([1, 3, 5], result)
-
-    def test_yields_individual_list_values(self):
-        result = list(self.paginator.paginate().search('Foo[].*[]'))
-        self.assertEqual([1, 2, 3, 4, 5], result)
-
-    def test_empty_when_no_match(self):
-        result = list(self.paginator.paginate().search('Foo[].qux'))
-        self.assertEqual([], result)
-
-    def test_no_yield_when_no_match_on_page(self):
-        result = list(self.paginator.paginate().search('Foo[].b'))
-        self.assertEqual([2, 4], result)
-
-
 if __name__ == '__main__':
     unittest.main()
diff --git a/tests/unit/test_session.py b/tests/unit/test_session.py
index 287706025..d9a3dbc2d 100644
--- a/tests/unit/test_session.py
+++ b/tests/unit/test_session.py
@@ -412,8 +412,7 @@ class TestCreateClient(BaseSessionTest):
 
         client_creator.return_value.create_client.assert_called_with(
             mock.ANY, mock.ANY, mock.ANY, mock.ANY, mock.ANY, mock.ANY,
-            scoped_config=mock.ANY, client_config=config,
-            api_version=mock.ANY)
+            scoped_config=mock.ANY, client_config=config)
 
     def test_create_client_with_region(self):
         ec2_client = self.session.create_client(