Repository: boto/botocore
Pull request #570 (2015-06-17 21:59:31)

Fix custom method documentation
Also, extracted logic to new method and fixed a doc string in the presigner methods.

The old documentation looked like this:
![screen shot 2015-06-17 at 2 53 41 pm](https://cloud.githubusercontent.com/assets/4605355/8219761/28667798-1501-11e5-84c1-da4b02389ba4.png)

The new documentation looks like this:
![screen shot 2015-06-17 at 2 56 41 pm](https://cloud.githubusercontent.com/assets/4605355/8219766/350e76c6-1501-11e5-8127-05f3c3695041.png)

The main difference being the docstring is fixed on the returns and there is no more paragraph block surrounding the summary of the doc string.

cc @jamesls @mtdowling 



Patch:
diff --git a/botocore/docs/client.py b/botocore/docs/client.py
index 748a39ee3..dc5779116 100644
--- a/botocore/docs/client.py
+++ b/botocore/docs/client.py
@@ -13,7 +13,7 @@
 import inspect
 
 from botocore.docs.utils import get_official_service_name
-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
 
@@ -83,12 +83,7 @@ class ClientDocumenter(object):
         return method_name not in self._client.meta.method_to_api_mapping
 
     def _add_custom_method(self, section, method_name, method):
-        document_custom_signature(
-            section, method_name, method)
-        method_intro_section = section.add_new_section('method-intro')
-        doc_string = inspect.getdoc(method)
-        if doc_string is not None:
-            method_intro_section.style.write_py_doc_string(doc_string)
+        document_custom_method(section, method_name, method)
 
     def _add_model_driven_method(self, section, method_name):
         service_model = self._client.meta.service_model
diff --git a/botocore/docs/method.py b/botocore/docs/method.py
index 43724abbc..8c50e3b60 100644
--- a/botocore/docs/method.py
+++ b/botocore/docs/method.py
@@ -108,6 +108,24 @@ def document_custom_signature(section, name, method,
     section.style.start_sphinx_py_method(name, signature_params)
 
 
+def document_custom_method(section, method_name, method):
+    """Documents a non-data driven method
+
+    :param section: The section to write the documentation to.
+
+    :param method_name: The name of the method
+
+    :param method: The handle to the method being documented
+    """
+    document_custom_signature(
+        section, method_name, method)
+    method_intro_section = section.add_new_section('method-intro')
+    method_intro_section.writeln('')
+    doc_string = inspect.getdoc(method)
+    if doc_string is not None:
+        method_intro_section.style.write_py_doc_string(doc_string)
+
+
 def document_model_driven_method(section, method_name, operation_model,
                                  event_emitter, method_description=None,
                                  example_prefix=None, include_input=None,
diff --git a/botocore/exceptions.py b/botocore/exceptions.py
index 871504e59..9b99bd87c 100644
--- a/botocore/exceptions.py
+++ b/botocore/exceptions.py
@@ -236,6 +236,15 @@ class OperationNotPageableError(BotoCoreError):
     fmt = 'Operation cannot be paginated: {operation_name}'
 
 
+class EventNotFound(BotoCoreError):
+    """
+    The specified event name is unknown to the system.
+
+    :ivar event_name: The name of the event the user attempted to use.
+    """
+    fmt = 'The event ({event_name}) is not known'
+
+
 class ChecksumError(BotoCoreError):
     """The expected checksum did not match the calculated checksum.
 
diff --git a/botocore/session.py b/botocore/session.py
index 30327db98..a34b0a353 100644
--- a/botocore/session.py
+++ b/botocore/session.py
@@ -20,12 +20,16 @@ import copy
 import logging
 import os
 import platform
+import shlex
+import warnings
 
 from botocore import __version__
 import botocore.config
 import botocore.credentials
 import botocore.client
-from botocore.exceptions import ConfigNotFound, ProfileNotFound
+from botocore.endpoint import EndpointCreator
+from botocore.exceptions import EventNotFound, ConfigNotFound, ProfileNotFound
+from botocore.exceptions import ImminentRemovalWarning
 from botocore import handlers
 from botocore.hooks import HierarchicalEmitter, first_non_none_response
 from botocore.loaders import create_loader
@@ -49,57 +53,76 @@ class Session(object):
     :ivar profile: The current profile.
     """
 
-    #: A default dictionary that maps the logical names for session variables
-    #: to the specific environment variables and configuration file names
-    #: that contain the values for these variables.
-    #: When creating a new Session object, you can pass in your own dictionary
-    #: to remap the logical names or to add new logical names.  You can then
-    #: get the current value for these variables by using the
-    #: ``get_config_variable`` method of the :class:`botocore.session.Session`
-    #: class.
-    #: These form the keys of the dictionary.  The values in the dictionary
-    #: are tuples of (<config_name>, <environment variable>, <default value>,
-    #: <conversion func>).
-    #: The conversion func is a function that takes the configuration value
-    #: as an arugment and returns the converted value.  If this value is
-    #: None, then the configuration value is returned unmodified.  This
-    #: conversion function that be used to type convert config values to
-    #: values other than the default values of strings.
-    #: The ``profile`` and ``config_file`` variables should always have a
-    #: None value for the first entry in the tuple because it doesn't make
-    #: sense to look inside the config file for the location of the config
-    #: file or for the default profile to use.
-    #: The ``config_name`` is the name to look for in the configuration file,
-    #: the ``env var`` is the OS environment variable (``os.environ``) to
-    #: use, and ``default_value`` is the value to use if no value is otherwise
-    #: found.
-    SESSION_VARIABLES = {
-        # logical:  config_file, env_var,        default_value, conversion_func
-        'profile': (None, ['AWS_DEFAULT_PROFILE', 'AWS_PROFILE'], None, None),
-        'region': ('region', 'AWS_DEFAULT_REGION', None, None),
-        'data_path': ('data_path', 'AWS_DATA_PATH', None, None),
-        'config_file': (None, 'AWS_CONFIG_FILE', '~/.aws/config', None),
-        'provider': ('provider', 'BOTO_PROVIDER_NAME', 'aws', None),
+    AllEvents = {
+        'after-call': '.%s.%s',
+        'after-parsed': '.%s.%s.%s.%s',
+        'before-parameter-build': '.%s.%s',
+        'before-call': '.%s.%s',
+        'service-created': '',
+        'service-data-loaded': '.%s',
+        'creating-endpoint': '.%s',
+        'before-auth': '.%s',
+        'needs-retry': '.%s.%s',
+    }
+    """
+    A dictionary where each key is an event name and the value
+    is the formatting string used to construct a new event.
+    """
+
+    SessionVariables = {
+        # logical:  config_file, env_var,        default_value
+        'profile': (None, ['AWS_DEFAULT_PROFILE', 'AWS_PROFILE'], None),
+        'region': ('region', 'AWS_DEFAULT_REGION', None),
+        'data_path': ('data_path', 'AWS_DATA_PATH', None),
+        'config_file': (None, 'AWS_CONFIG_FILE', '~/.aws/config'),
+        'provider': ('provider', 'BOTO_PROVIDER_NAME', 'aws'),
 
         # These variables are intended for internal use so don't have any
         # user settable values.
         # This is the shared credentials file amongst sdks.
-        'credentials_file': (None, None, '~/.aws/credentials', None),
+        'credentials_file': (None, None, '~/.aws/credentials'),
 
         # These variables only exist in the config file.
 
         # This is the number of seconds until we time out a request to
         # the instance metadata service.
-        'metadata_service_timeout': ('metadata_service_timeout',
-                                     None, 1, int),
+        'metadata_service_timeout': ('metadata_service_timeout', None, 1),
         # This is the number of request attempts we make until we give
         # up trying to retrieve data from the instance metadata service.
         'metadata_service_num_attempts': ('metadata_service_num_attempts',
-                                          None, 1, int),
-    }
+                                          None, 1),
+        }
+    """
+    A default dictionary that maps the logical names for session variables
+    to the specific environment variables and configuration file names
+    that contain the values for these variables.
+
+    When creating a new Session object, you can pass in your own dictionary to
+    remap the logical names or to add new logical names.  You can then get the
+    current value for these variables by using the ``get_config_variable``
+    method of the :class:`botocore.session.Session` class.
+    The default set of logical variable names are:
+
+    * profile - Default profile name you want to use.
+    * region - Default region name to use, if not otherwise specified.
+    * data_path - Additional directories to search for data files.
+    * config_file - Location of a Boto config file.
+    * provider - The name of the service provider (e.g. aws)
+
+    These form the keys of the dictionary.  The values in the dictionary
+    are tuples of (<config_name>, <environment variable>, <default value).
+    The ``profile`` and ``config_file`` variables should always have a
+    None value for the first entry in the tuple because it doesn't make
+    sense to look inside the config file for the location of the config
+    file or for the default profile to use.
+
+    The ``config_name`` is the name to look for in the configuration file,
+    the ``env var`` is the OS environment variable (``os.environ``) to
+    use, and ``default_value`` is the value to use if no value is otherwise
+    found.
+    """
 
-    #: The default format string to use when configuring the botocore logger.
-    LOG_FORMAT = '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
+    FmtString = '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
 
     def __init__(self, session_vars=None, event_hooks=None,
                  include_builtin_handlers=True, loader=None):
@@ -110,7 +133,7 @@ class Session(object):
         :param session_vars: A dictionary that is used to override some or all
             of the environment variables associated with this session.  The
             key/value pairs defined in this dictionary will override the
-            corresponding variables defined in ``SESSION_VARIABLES``.
+            corresponding variables defined in ``SessionVariables``.
 
         :type event_hooks: BaseEventHooks
         :param event_hooks: The event hooks object to use. If one is not
@@ -121,7 +144,7 @@ class Session(object):
         :param include_builtin_handlers: Indicates whether or not to
             automatically register builtin handlers.
         """
-        self.session_var_map = copy.copy(self.SESSION_VARIABLES)
+        self.session_var_map = copy.copy(self.SessionVariables)
         if session_vars:
             self.session_var_map.update(session_vars)
         if event_hooks is None:
@@ -189,7 +212,7 @@ class Session(object):
                 event_name, handler, register_type = spec
                 if register_type is handlers.REGISTER_FIRST:
                     self._events.register_first(event_name, handler)
-                elif register_type is handlers.REGISTER_LAST:
+                elif register_first is handlers.REGISTER_LAST:
                     self._events.register_last(event_name, handler)
 
     @property
@@ -225,7 +248,8 @@ class Session(object):
         self._reset_components()
 
     def get_config_variable(self, logical_name,
-                            methods=('instance', 'env', 'config')):
+                            methods=('instance', 'env', 'config'),
+                            default=None):
         """
         Retrieve the value associated with the specified logical_name
         from the environment or the config file.  Values found in the
@@ -245,48 +269,50 @@ class Session(object):
             by supplying a different value to this parameter.
             Valid choices are: instance|env|config
 
-        :returns: value of variable or None if not defined.
+        :param default: The default value to return if there is no
+            value associated with the config file.  This value will
+            override any default value specified in ``SessionVariables``.
 
-        """
-        # Handle all the short circuit special cases first.
-        if logical_name not in self.session_var_map:
-            return
-        if logical_name == 'profile' and self._profile:
-            return self._profile
+        :returns: str value of variable of None if not defined.
 
-        # Do the actual lookups.  We need to handle
-        # 'instance', 'env', and 'config' locations, in that order.
+        """
         value = None
-        var_config = self.session_var_map[logical_name]
-        if self._found_in_instance_vars(methods, logical_name):
-            return self._session_instance_vars[logical_name]
-        elif self._found_in_env(methods, var_config):
-            value = self._retrieve_from_env(var_config[1], os.environ)
-        elif self._found_in_config_file(methods, var_config):
-            value = self.get_scoped_config()[var_config[0]]
-        if value is None:
-            value = var_config[2]
-        if var_config[3] is not None:
-            value = var_config[3](value)
+        # There's two types of defaults here.  One if the
+        # default value specified in the SessionVariables.
+        # The second is an explicit default value passed into this
+        # function (the default parameter).
+        # config_default is tracking the default value specified
+        # in the SessionVariables.
+        config_default = None
+        if logical_name in self.session_var_map:
+            # Short circuit case, check if the var has been explicitly
+            # overriden via set_config_variable.
+            if 'instance' in methods and \
+                    logical_name in self._session_instance_vars:
+                return self._session_instance_vars[logical_name]
+            config_name, envvar_name, config_default = self.session_var_map[
+                logical_name]
+            if logical_name in ('config_file', 'profile'):
+                config_name = None
+            if logical_name == 'profile' and self._profile:
+                value = self._profile
+            elif 'env' in methods and envvar_name and self._handle_env_vars(
+                    envvar_name, os.environ) is not None:
+                value = self._handle_env_vars(envvar_name, os.environ)
+            elif 'config' in methods:
+                if config_name:
+                    config = self.get_scoped_config()
+                    value = config.get(config_name)
+        # If we don't have a value at this point, we need to try to assign
+        # a default value.  An explicit default argument will win over the
+        # default value from SessionVariables.
+        if value is None and default is not None:
+            value = default
+        if value is None and config_default is not None:
+            value = config_default
         return value
 
-    def _found_in_instance_vars(self, methods, logical_name):
-        if 'instance' in methods:
-            return logical_name in self._session_instance_vars
-        return False
-
-    def _found_in_env(self, methods, var_config):
-        return (
-            'env' in methods and
-            var_config[1] is not None and
-            self._retrieve_from_env(var_config[1], os.environ) is not None)
-
-    def _found_in_config_file(self, methods, var_config):
-        if 'config' in methods and var_config[0] is not None:
-            return var_config[0] in self.get_scoped_config()
-        return False
-
-    def _retrieve_from_env(self, names, environ):
+    def _handle_env_vars(self, names, environ):
         # We need to handle the case where names is either
         # a single value or a list of variables.
         if not isinstance(names, list):
@@ -316,12 +342,13 @@ class Session(object):
 
         :type logical_name: str
         :param logical_name: The logical name of the session variable
-            you want to set.  These are the keys in ``SESSION_VARIABLES``.
+            you want to set.  These are the keys in ``SessionVariables``.
         :param value: The value to associate with the config variable.
 
         """
         self._session_instance_vars[logical_name] = value
 
+
     def get_scoped_config(self):
         """
         Returns the config values from the config file scoped to the current
@@ -507,8 +534,8 @@ class Session(object):
             type_name='service-2',
             api_version=api_version
         )
-        self._events.emit('service-data-loaded.%s' % service_name,
-                          service_data=service_data,
+        event_name = self.create_event('service-data-loaded', service_name)
+        self._events.emit(event_name, service_data=service_data,
                           service_name=service_name, session=self)
         return service_data
 
@@ -546,7 +573,7 @@ class Session(object):
         :type format_string: str
         :param format_string: The format string to use for the log
             formatter.  If none is provided this will default to
-            ``self.LOG_FORMAT``.
+            ``self.FmtString``.
 
         """
         log = logging.getLogger(logger_name)
@@ -557,7 +584,7 @@ class Session(object):
 
         # create formatter
         if format_string is None:
-            format_string = self.LOG_FORMAT
+            format_string = self.FmtString
         formatter = logging.Formatter(format_string)
 
         # add formatter to ch
@@ -586,7 +613,7 @@ class Session(object):
         ch.setLevel(log_level)
 
         # create formatter
-        formatter = logging.Formatter(self.LOG_FORMAT)
+        formatter = logging.Formatter(self.FmtString)
 
         # add formatter to ch
         ch.setFormatter(formatter)
@@ -665,6 +692,44 @@ class Session(object):
                                 unique_id=unique_id,
                                 unique_id_uses_count=unique_id_uses_count)
 
+    def register_event(self, event_name, fmtstr):
+        """
+        Register a new event.  The event will be added to ``AllEvents``
+        and will then be able to be created using ``create_event``.
+
+        :type event_name: str
+        :param event_name: The base name of the event.
+
+        :type fmtstr: str
+        :param fmtstr: The formatting string for the event.
+        """
+        if event_name not in self.AllEvents:
+            self.AllEvents[event_name] = fmtstr
+
+    def create_event(self, event_name, *fmtargs):
+        """
+        Creates a new event string that can then be emitted.
+        You could just create it manually, since it's just
+        a string but this helps to define the range of known events.
+
+        :type event_name: str
+        :param event_name: The base name of the new event.
+
+        :type fmtargs: tuple
+        :param fmtargs: A tuple of values that will be used as the
+            arguments pass to the string formatting operation.  The
+            actual values passed depend on the type of event you
+            are creating.
+        """
+        if event_name in self.AllEvents:
+            fmt_string = self.AllEvents[event_name]
+            if fmt_string:
+                event = event_name + (fmt_string % fmtargs)
+            else:
+                event = event_name
+            return event
+        raise EventNotFound(event_name=event_name)
+
     def emit(self, event_name, **kwargs):
         return self._events.emit(event_name, **kwargs)
 
@@ -703,28 +768,26 @@ class Session(object):
             of the client.
 
         :type use_ssl: boolean
-        :param use_ssl: Whether or not to use SSL.  By default, SSL is used.
-            Note that not all services support non-ssl connections.
+        :param use_ssl: Whether or not to use SSL.  By default, SSL is used.  Note that
+            not all services support non-ssl connections.
 
         :type verify: boolean/string
-        :param verify: Whether or not to verify SSL certificates.
-            By default SSL certificates are verified.  You can provide the
-            following values:
+        :param verify: Whether or not to verify SSL certificates.  By default SSL certificates
+            are verified.  You can provide the following values:
 
             * False - do not validate SSL certificates.  SSL will still be
               used (unless use_ssl is False), but SSL certificates
               will not be verified.
             * path/to/cert/bundle.pem - A filename of the CA cert bundle to
-              uses.  You can specify this argument if you want to use a
-              different CA cert bundle than the one used by botocore.
+              uses.  You can specify this argument if you want to use a different
+              CA cert bundle than the one used by botocore.
 
         :type endpoint_url: string
-        :param endpoint_url: The complete URL to use for the constructed
-            client.  Normally, botocore will automatically construct the
-            appropriate URL to use when communicating with a service.  You can
-            specify a complete URL (including the "http/https" scheme) to
-            override this behavior.  If this value is provided, then
-            ``use_ssl`` is ignored.
+        :param endpoint_url: The complete URL to use for the constructed client.
+            Normally, botocore will automatically construct the appropriate URL
+            to use when communicating with a service.  You can specify a
+            complete URL (including the "http/https" scheme) to override this
+            behavior.  If this value is provided, then ``use_ssl`` is ignored.
 
         :type aws_access_key_id: string
         :param aws_access_key_id: The access key to use when creating
diff --git a/botocore/signers.py b/botocore/signers.py
index 3c22215b0..2395984d9 100644
--- a/botocore/signers.py
+++ b/botocore/signers.py
@@ -164,7 +164,7 @@ class RequestSigner(object):
         :type region_name: string
         :param region_name: The region name to sign the presigned url.
 
-        returns: The presigned url
+        :returns: The presigned url
         """
         if region_name is None:
             region_name = self._region_name
@@ -318,7 +318,7 @@ def generate_presigned_url(self, ClientMethod, Params=None, ExpiresIn=3600,
     :param HttpMethod: The http method to use on the generated url. By
         default, the http method is whatever is used in the method's model.
 
-    returns: The presigned url
+    :returns: The presigned url
     """
     client_method = ClientMethod
     params = Params
diff --git a/docs/source/index.rst b/docs/source/index.rst
index 21ca6ab6f..24e4ab004 100644
--- a/docs/source/index.rst
+++ b/docs/source/index.rst
@@ -35,14 +35,6 @@ Contents:
 Upgrade Notes
 =============
 
-Upgrading to 1.0.0rc1
----------------------
-
-* The ``default`` argument to ``session.get_config_variable()`` has been
-  removed.  If you need this functionality you can use::
-
-        value = session.get_config_variable() or 'default value'
-
 Upgrading to 0.104.0
 --------------------
 


Test patch:
diff --git a/tests/unit/docs/test_method.py b/tests/unit/docs/test_method.py
index 0d7e10e90..d1351b6f9 100644
--- a/tests/unit/docs/test_method.py
+++ b/tests/unit/docs/test_method.py
@@ -15,6 +15,7 @@ from tests.unit.docs import BaseDocsTest
 from botocore.hooks import HierarchicalEmitter
 from botocore.docs.method import document_model_driven_signature
 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.utils import DocumentedShape
@@ -81,6 +82,26 @@ class TestDocumentCustomSignature(BaseDocsTest):
             '.. py:method:: my_method(foo, bar=\'bar\', baz=None)')
 
 
+class TestDocumentCustomMethod(BaseDocsTest):
+    def custom_method(self, foo):
+        """This is a custom method
+
+        :type foo: string
+        :param foo: The foo parameter
+        """
+        pass
+
+    def test_document_custom_signature(self):
+        document_custom_method(
+            self.doc_structure, 'my_method', self.custom_method)
+        self.assert_contains_lines_in_order([
+            '.. py:method:: my_method(foo)',
+            '  This is a custom method',
+            '  :type foo: string',
+            '  :param foo: The foo parameter'
+        ])
+
+
 class TestDocumentModelDrivenMethod(BaseDocsTest):
     def setUp(self):
         super(TestDocumentModelDrivenMethod, self).setUp()
diff --git a/tests/unit/test_handlers.py b/tests/unit/test_handlers.py
index 6331e0817..2eedfa625 100644
--- a/tests/unit/test_handlers.py
+++ b/tests/unit/test_handlers.py
@@ -19,8 +19,9 @@ import copy
 
 import botocore
 import botocore.session
-from botocore.compat import quote, six
+from botocore.hooks import first_non_none_response
 from botocore.awsrequest import AWSRequest
+from botocore.compat import quote, six
 from botocore.model import OperationModel, ServiceModel
 from botocore.signers import RequestSigner
 from botocore.credentials import Credentials
@@ -54,7 +55,8 @@ class TestHandlers(BaseSessionTest):
 
     def test_quote_source_header(self):
         for op in ('UploadPartCopy', 'CopyObject'):
-            event = 'before-call.s3.%s' % op
+            event = self.session.create_event(
+                'before-call', 's3', op)
             params = {'headers': {'x-amz-copy-source': 'foo++bar.txt'}}
             m = mock.Mock()
             self.session.emit(event, params=params, model=m)
@@ -146,7 +148,8 @@ class TestHandlers(BaseSessionTest):
     def test_sse_params(self):
         for op in ('HeadObject', 'GetObject', 'PutObject', 'CopyObject',
                    'CreateMultipartUpload', 'UploadPart', 'UploadPartCopy'):
-            event = 'before-parameter-build.s3.%s' % op
+            event = self.session.create_event(
+                'before-parameter-build', 's3', op)
             params = {'SSECustomerKey': b'bar',
                       'SSECustomerAlgorithm': 'AES256'}
             self.session.emit(event, params=params, model=mock.Mock())
@@ -155,16 +158,18 @@ class TestHandlers(BaseSessionTest):
                              'N7UdGUp1E+RbVvZSTy1R8g==')
 
     def test_sse_params_as_str(self):
-        event = 'before-parameter-build.s3.PutObject'
+        event = self.session.create_event(
+            'before-parameter-build', 's3', 'PutObject')
         params = {'SSECustomerKey': 'bar',
                   'SSECustomerAlgorithm': 'AES256'}
         self.session.emit(event, params=params, model=mock.Mock())
         self.assertEqual(params['SSECustomerKey'], 'YmFy')
         self.assertEqual(params['SSECustomerKeyMD5'],
-                         'N7UdGUp1E+RbVvZSTy1R8g==')
+                            'N7UdGUp1E+RbVvZSTy1R8g==')
 
     def test_route53_resource_id(self):
-        event = 'before-parameter-build.route53.GetHostedZone'
+        event = self.session.create_event(
+            'before-parameter-build', 'route53', 'GetHostedZone')
         params = {'Id': '/hostedzone/ABC123',
                   'HostedZoneId': '/hostedzone/ABC123',
                   'ResourceId': '/hostedzone/DEF456',
@@ -222,8 +227,9 @@ class TestHandlers(BaseSessionTest):
         self.assertEqual(params['Other'], '/hostedzone/foo')
 
     def test_route53_resource_id_missing_input_shape(self):
-        event = 'before-parameter-build.route53.GetHostedZone'
-        params = {'HostedZoneId': '/hostedzone/ABC123'}
+        event = self.session.create_event(
+            'before-parameter-build', 'route53', 'GetHostedZone')
+        params = {'HostedZoneId': '/hostedzone/ABC123',}
         operation_def = {
             'name': 'GetHostedZone'
         }
@@ -239,7 +245,8 @@ class TestHandlers(BaseSessionTest):
     def test_run_instances_userdata(self):
         user_data = 'This is a test'
         b64_user_data = base64.b64encode(six.b(user_data)).decode('utf-8')
-        event = 'before-parameter-build.ec2.RunInstances'
+        event = self.session.create_event(
+            'before-parameter-build', 'ec2', 'RunInstances')
         params = dict(ImageId='img-12345678',
                       MinCount=1, MaxCount=5, UserData=user_data)
         self.session.emit(event, params=params)
@@ -255,7 +262,8 @@ class TestHandlers(BaseSessionTest):
         # user data.
         user_data = b'\xc7\xa9This is a test'
         b64_user_data = base64.b64encode(user_data).decode('utf-8')
-        event = 'before-parameter-build.ec2.RunInstances'
+        event = self.session.create_event(
+            'before-parameter-build', 'ec2', 'RunInstances')
         params = dict(ImageId='img-12345678',
                       MinCount=1, MaxCount=5, UserData=user_data)
         self.session.emit(event, params=params)
@@ -461,3 +469,7 @@ class TestRetryHandlerOrder(BaseSessionTest):
         self.assertTrue(s3_200_handler < general_retry_handler,
                         "S3 200 error handler was supposed to be before "
                         "the general retry handler, but it was not.")
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/tests/unit/test_session.py b/tests/unit/test_session.py
index 6808a325e..287706025 100644
--- a/tests/unit/test_session.py
+++ b/tests/unit/test_session.py
@@ -35,11 +35,11 @@ class BaseSessionTest(unittest.TestCase):
 
     def setUp(self):
         self.env_vars = {
-            'profile': (None, 'FOO_PROFILE', None, None),
-            'region': ('foo_region', 'FOO_REGION', None, None),
-            'data_path': ('data_path', 'FOO_DATA_PATH', None, None),
-            'config_file': (None, 'FOO_CONFIG_FILE', None, None),
-            'credentials_file': (None, None, '/tmp/nowhere', None),
+            'profile': (None, 'FOO_PROFILE', None),
+            'region': ('foo_region', 'FOO_REGION', None),
+            'data_path': ('data_path', 'FOO_DATA_PATH', None),
+            'config_file': (None, 'FOO_CONFIG_FILE', None),
+            'credentials_file': (None, None, '/tmp/nowhere'),
         }
         self.environ = {}
         self.environ_patch = mock.patch('os.environ', self.environ)
@@ -78,8 +78,7 @@ class SessionTest(BaseSessionTest):
 
     def test_supports_multiple_env_vars_for_single_logical_name(self):
         env_vars = {
-            'profile': (None, ['BAR_DEFAULT_PROFILE', 'BAR_PROFILE'],
-                        None, None),
+            'profile': (None, ['BAR_DEFAULT_PROFILE', 'BAR_PROFILE'], None),
         }
         session = create_session(session_vars=env_vars)
         self.environ['BAR_DEFAULT_PROFILE'] = 'first'
@@ -88,8 +87,7 @@ class SessionTest(BaseSessionTest):
 
     def test_multiple_env_vars_uses_second_var(self):
         env_vars = {
-            'profile': (None, ['BAR_DEFAULT_PROFILE', 'BAR_PROFILE'],
-                        None, None),
+            'profile': (None, ['BAR_DEFAULT_PROFILE', 'BAR_PROFILE'], None),
         }
         session = create_session(session_vars=env_vars)
         self.environ.pop('BAR_DEFAULT_PROFILE', None)
@@ -135,19 +133,6 @@ class SessionTest(BaseSessionTest):
         # file should be present in the loaded config dict.
         self.assertIn('aws_access_key_id', config)
 
-    def test_type_conversions_occur_when_specified(self):
-        # Specify that we can retrieve the var from the
-        # FOO_TIMEOUT env var, with a conversion function
-        # of int().
-        self.env_vars['metadata_service_timeout'] = (
-            None, 'FOO_TIMEOUT', None, int)
-        # Environment variables are always strings.
-        self.environ['FOO_TIMEOUT'] = '10'
-        session = create_session(session_vars=self.env_vars)
-        # But we should type convert this to a string.
-        self.assertEqual(
-            session.get_config_variable('metadata_service_timeout'), 10)
-
     def test_default_profile_specified_raises_exception(self):
         # If you explicity set the default profile and you don't
         # have that in your config file, an exception is raised.
@@ -232,6 +217,19 @@ class SessionTest(BaseSessionTest):
         response = session.emit_first_non_none_response('foo')
         self.assertEqual(response, 'first')
 
+    def test_create_events(self):
+        event = self.session.create_event('before-call', 'foo', 'bar')
+        self.assertEqual(event, 'before-call.foo.bar')
+        event = self.session.create_event('after-call', 'foo', 'bar')
+        self.assertEqual(event, 'after-call.foo.bar')
+        event = self.session.create_event('after-parsed', 'foo',
+                                          'bar', 'fie', 'baz')
+        self.assertEqual(event, 'after-parsed.foo.bar.fie.baz')
+        event = self.session.create_event('service-created')
+        self.assertEqual(event, 'service-created')
+        self.assertRaises(botocore.exceptions.EventNotFound,
+                          self.session.create_event, 'foo-bar')
+
     @mock.patch('logging.getLogger')
     @mock.patch('logging.FileHandler')
     def test_logger_name_can_be_passed_in(self, file_handler, get_logger):
@@ -290,8 +288,7 @@ class TestBuiltinEventHandlers(BaseSessionTest):
 
 class TestSessionConfigurationVars(BaseSessionTest):
     def test_per_session_config_vars(self):
-        self.session.session_var_map['foobar'] = (None, 'FOOBAR',
-                                                  'default', None)
+        self.session.session_var_map['foobar'] = (None, 'FOOBAR', 'default')
         # Default value.
         self.assertEqual(self.session.get_config_variable('foobar'), 'default')
         # Retrieve from os environment variable.
@@ -309,9 +306,13 @@ class TestSessionConfigurationVars(BaseSessionTest):
             'foobar', methods=('env', 'config')), 'default')
 
     def test_default_value_can_be_overriden(self):
-        self.session.session_var_map['foobar'] = (None, 'FOOBAR', 'default',
-                                                  None)
+        self.session.session_var_map['foobar'] = (None, 'FOOBAR', 'default')
+        # Default value.
         self.assertEqual(self.session.get_config_variable('foobar'), 'default')
+        self.assertEqual(
+            self.session.get_config_variable('foobar',
+                                             default='per-call-default'),
+            'per-call-default')
 
 
 class TestSessionUserAgent(BaseSessionTest):